diff --git a/archify.zip b/archify.zip index 4249d32..89a085d 100644 Binary files a/archify.zip and b/archify.zip differ diff --git a/archify/assets/template.html b/archify/assets/template.html index 9942250..5a4a37d 100644 --- a/archify/assets/template.html +++ b/archify/assets/template.html @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -6536,7 +6543,7 @@

Card Title 3

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6641,6 +6648,698 @@

Card Title 3

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6658,6 +7357,30 @@

Card Title 3

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/archify/bin/archify.mjs b/archify/bin/archify.mjs index 1475b43..c7b189a 100755 --- a/archify/bin/archify.mjs +++ b/archify/bin/archify.mjs @@ -26,6 +26,7 @@ function usage() { archify examples archify doctor archify demo [output-directory] + archify export-drawio [output.drawio] [--strict] [--json] Types: architecture, workflow, sequence, dataflow, lifecycle @@ -1156,6 +1157,41 @@ function commandCheck(args) { if (result.status !== 0) exitFrom(result); } +function commandExportDrawio(args) { + const json = args.includes('--json'); + const strict = args.includes('--strict'); + const knownOptions = new Set(['--json', '--strict']); + const unknown = args.filter((arg) => arg.startsWith('--') && !knownOptions.has(arg)); + if (unknown.length) fail(`Unknown export-drawio option "${unknown[0]}".`); + const positional = args.filter((arg) => !knownOptions.has(arg)); + const [type, input, output] = positional; + if (!type || !input || positional.length > 3) fail(usage()); + rendererPath(type); // validates type against TYPES, fails(2) on unknown. + const exporter = path.join(skillRoot, 'scripts/export-drawio.mjs'); + const result = runNode( + [exporter, type, input, ...(output ? [output] : []), ...(strict ? ['--strict'] : [])], + { stdio: json ? 'pipe' : 'inherit' }, + ); + if (result.status !== 0) { + if (json) { + console.log(JSON.stringify({ + schemaVersion: 1, ok: false, command: 'export-drawio', + type, input: path.resolve(input), + error: 'Drawio export failed.', + }, null, 2)); + } + exitFrom(result); + } + if (json) { + console.log(JSON.stringify({ + schemaVersion: 1, ok: true, command: 'export-drawio', + type, input: path.resolve(input), + ...(strict ? { strict: true } : {}), + ...(output ? { output: path.resolve(output) } : {}), + }, null, 2)); + } +} + async function commandVisualCheck(args) { const json = args.includes('--json'); const knownOptions = new Set(['--json']); @@ -1578,6 +1614,9 @@ switch (command) { case 'demo': commandDemo(args); break; + case 'export-drawio': + commandExportDrawio(args); + break; default: fail(`Unknown command "${command}".\n\n${usage()}`); } diff --git a/archify/examples/dataflow-product-analytics.html b/archify/examples/dataflow-product-analytics.html index 9904584..1810bd2 100644 --- a/archify/examples/dataflow-product-analytics.html +++ b/archify/examples/dataflow-product-analytics.html @@ -1,5 +1,5 @@ - + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -6647,7 +6654,7 @@

Derived Consumers

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6752,6 +6759,698 @@

Derived Consumers

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6769,6 +7468,30 @@

Derived Consumers

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/archify/examples/lifecycle-agent-run.html b/archify/examples/lifecycle-agent-run.html index 0bb6297..e8371bb 100644 --- a/archify/examples/lifecycle-agent-run.html +++ b/archify/examples/lifecycle-agent-run.html @@ -1,5 +1,5 @@ - + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -6596,7 +6603,7 @@

Terminal + Recovery

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6701,6 +6708,698 @@

Terminal + Recovery

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6718,6 +7417,30 @@

Terminal + Recovery

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/archify/examples/sequence-cache-miss-request.html b/archify/examples/sequence-cache-miss-request.html index 2582199..94f34d1 100644 --- a/archify/examples/sequence-cache-miss-request.html +++ b/archify/examples/sequence-cache-miss-request.html @@ -1,5 +1,5 @@ - + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -4925,18 +4932,18 @@

Cache Miss Request Sequence

- - - - - - - - - - - - + + + + + + + + + + + + @@ -6662,7 +6669,7 @@

Async Trace

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6767,6 +6774,698 @@

Async Trace

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6784,6 +7483,30 @@

Async Trace

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/archify/examples/web-app-rendered.html b/archify/examples/web-app-rendered.html index b454244..d3d6579 100644 --- a/archify/examples/web-app-rendered.html +++ b/archify/examples/web-app-rendered.html @@ -1,5 +1,5 @@ - + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -6602,7 +6609,7 @@

Security

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6707,6 +6714,698 @@

Security

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6724,6 +7423,30 @@

Security

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/archify/examples/workflow-agent-tool-call-rendered.html b/archify/examples/workflow-agent-tool-call-rendered.html index 4732bcd..a0c7379 100644 --- a/archify/examples/workflow-agent-tool-call-rendered.html +++ b/archify/examples/workflow-agent-tool-call-rendered.html @@ -1,5 +1,5 @@ - + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -6644,7 +6651,7 @@

Why It Matters

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6749,6 +6756,698 @@

Why It Matters

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6766,6 +7465,30 @@

Why It Matters

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/archify/renderers/sequence/render-sequence.mjs b/archify/renderers/sequence/render-sequence.mjs index 84c9d6c..c3a2025 100644 --- a/archify/renderers/sequence/render-sequence.mjs +++ b/archify/renderers/sequence/render-sequence.mjs @@ -346,8 +346,9 @@ function renderActivation(activation) { const fill = componentFill[activation.type] || componentFill[participant.type] || 'c-external'; const x = participant.cx - 5; const height = activation.to - activation.from; - return ` - `; + const identity = ` data-graph-role="activation" data-activation-participant="${esc(activation.participant)}"`; + return ` + `; } function messageLabel(message, x1, x2) { diff --git a/archify/renderers/shared/cli.mjs b/archify/renderers/shared/cli.mjs index 2198a83..ef07aa6 100644 --- a/archify/renderers/shared/cli.mjs +++ b/archify/renderers/shared/cli.mjs @@ -52,6 +52,7 @@ export function writeDiagram({ outPath, template, diagramType, meta, svg, cards, visualPreset: meta.visual_preset || 'classic', guidedViews: meta.views || [], sourceEvidence, + diagramType, })); outputPathGuards.delete(outPath); console.log(outPath); @@ -166,6 +167,10 @@ export function animateAttr(meta, kind, step) { // Stable semantic hooks for the standalone HTML explorer. IDs already pass // the schema's conservative identifier pattern; escape again at the markup // boundary so these helpers remain safe if that contract expands later. +// Attribute-order contract: `id` must stay FIRST with data-node-* after — +// the draw.io exporters regex-parse these groups and anchor on that order +// (NODE_G_RE in renderers/shared/svg-to-drawio.mjs and the viewer's +// drawioParseSvg in assets/template.html). export function focusNodeAttrs(id, label, metadata = {}) { const optional = [ ['data-node-kind', metadata.kind], diff --git a/archify/renderers/shared/svg-to-drawio.mjs b/archify/renderers/shared/svg-to-drawio.mjs new file mode 100644 index 0000000..806ece6 --- /dev/null +++ b/archify/renderers/shared/svg-to-drawio.mjs @@ -0,0 +1,1203 @@ +/** + * Archify SVG → draw.io converter. + * + * Converts a rendered Archify SVG (embedded in the standalone HTML artifact) + * into draw.io mxGraphModel XML. The SVG is a fully self-describing source: + * every node carries `data-node-id/kind/label`, every edge carries + * `data-edge-from/to/id` plus `data-composition-points` (the complete routed + * polyline including computed anchors), and every boundary carries + * `data-graph-role="structural-frame"`. + * + * Design goals: + * - Preserve the exact rendered geometry (coordinates come from the SVG, + * never recomputed). + * - Real topology: draw.io edges bind source/target to vertex cell ids so + * dragging a box keeps its connections intact. + * - Fixed waypoints: the authored/computed route is pinned via + * `` so the line shape is preserved. + * - Zero runtime dependencies (pure Node ESM, no DOM/cheerio/jsdom). + */ + +// ─── XML helpers ──────────────────────────────────────────────────────────── + +const ATTR_ESCAPE = { '&': '&', '"': '"', '<': '<', '>': '>', "'": ''' }; + +function escapeAttr(value) { + return String(value ?? '').replace(/[&"<>']/g, (ch) => ATTR_ESCAPE[ch]); +} + +/** + * Extract the value of a `name="value"` attribute from an SVG element tag. + * Returns null when absent. + */ +function attr(tag, name) { + const re = new RegExp(`\\s${name}="([^"]*)"`); + const match = tag.match(re); + return match ? match[1] : null; +} + +// ─── SVG parsing ──────────────────────────────────────────────────────────── + +// Attribute-order contract: node groups are emitted by nodeGroup() in +// renderers/shared/cli.mjs (and mirrored by the viewer's drawioParseSvg in +// assets/template.html) with `id` FIRST and the data-node-* attributes after. +// The regexes below anchor on that order — keep the three call sites aligned +// when adding or reordering attributes. +const NODE_G_RE = /]*data-node-id="[^"]*"[^>]*>/g; +const RECT_RE = /]*>/g; +const NUM = '([-\\d.]+)'; + +function parseRect(tag) { + const x = parseFloat(attr(tag, 'x') || '0'); + const y = parseFloat(attr(tag, 'y') || '0'); + const width = parseFloat(attr(tag, 'width') || '0'); + const height = parseFloat(attr(tag, 'height') || '0'); + return { x, y, width, height }; +} + +/** + * Parse a `data-composition-points="x,y;x,y;..."` value into [[x,y],...]. + */ +function parsePoints(value) { + if (!value) return []; + return value + .split(';') + .filter(Boolean) + .map((pair) => { + const [x, y] = pair.split(',').map(Number); + return [x, y]; + }); +} + +/** + * Find the first child of a ... block and return its box. + * The node's geometry lives in the first rect (the c-mask background rect, + * which shares x/y/width/height with the fill rect on top of it). + */ +function firstRectInGroup(gOpenTag, groupCloseIndex, svg) { + const searchStart = gOpenTag.index + gOpenTag[0].length; + const inner = svg.slice(searchStart, groupCloseIndex); + const rectMatch = inner.match(RECT_RE); + if (!rectMatch) return null; + return parseRect(rectMatch[0]); +} + +/** + * Among the rects inside a node , find the FILL rect: the c-* rect that is + * not the opaque c-mask backdrop. Sigil decoration rects are smaller, so the + * largest non-mask c- rect wins. Returns the full rect tag (for class/rx/ + * stroke-width) or null. + */ +function fillRectInGroup(gOpenTag, groupCloseIndex, svg) { + const searchStart = gOpenTag.index + gOpenTag[0].length; + const inner = svg.slice(searchStart, groupCloseIndex); + let best = null; + let bestArea = -1; + for (const m of inner.matchAll(RECT_RE)) { + const cls = attr(m[0], 'class') || ''; + if (!cls.startsWith('c-') || cls === 'c-mask') continue; + const box = parseRect(m[0]); + const area = box.width * box.height; + if (area > bestArea) { + bestArea = area; + best = m[0]; + } + } + return best; +} + +/** + * Measure the corner rounding radius from an SVG path's `d` attribute. The + * renderers round polyline corners with quadratic curves ("L a b Q cx cy d e"); + * the radius is the distance from the incoming point to the control point. + * Returns 0 for sharp polylines. + */ +function cornerRadiusFromD(d) { + if (!d) return 0; + const m = d.match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + if (!m) return 0; + const dx = parseFloat(m[3]) - parseFloat(m[1]); + const dy = parseFloat(m[4]) - parseFloat(m[2]); + return Math.round(Math.hypot(dx, dy)); +} + +/** + * Parse edge label groups: `` + * wrapping a mask rect plus a positioned . The text x/y anchor gives the + * exact label position (y is the text baseline; the visual center sits ~4px + * above it at font-size 8). + */ +const EDGE_LABEL_RE = /]*\bdata-detail="context"[^>]*\bdata-edge-from="([^"]*)"[^>]*\bdata-edge-to="([^"]*)"[^>]*>[\s\S]*?]*\bx="(-?[\d.]+)"[^>]*\by="(-?[\d.]+)"[^>]*\bclass="([^"]*)"[^>]*>([^<]*)<\/text>/g; + +function parseEdgeLabels(svg) { + const labels = []; + for (const m of svg.matchAll(EDGE_LABEL_RE)) { + labels.push({ + from: m[1], + to: m[2], + x: parseFloat(m[3]), + y: parseFloat(m[4]) - 4, + className: m[5], + label: m[6], + }); + } + return labels; +} + +/** + * Locate the matching for a opening tag (naive depth scan). + */ +function findGroupClose(svg, openIndex) { + let depth = 1; + let i = openIndex; + while (depth > 0 && i < svg.length) { + const nextOpen = svg.indexOf('', i + 1); + if (nextClose === -1) return -1; + if (nextOpen !== -1 && nextOpen < nextClose) { + depth += 1; + i = nextOpen; + } else { + depth -= 1; + i = nextClose; + } + } + return i; +} + +/** + * Parse all nodes from the SVG: each `` + * carries identity + the first inner carries geometry. + */ +function parseNodes(svg) { + const nodes = []; + const matches = [...svg.matchAll(NODE_G_RE)]; + for (const m of matches) { + const openTag = m[0]; + const closeIndex = findGroupClose(svg, m.index); + const gOpen = { index: m.index, [0]: openTag }; + const box = firstRectInGroup(gOpen, closeIndex, svg); + if (!box) continue; + const fillTag = fillRectInGroup(gOpen, closeIndex, svg) || ''; + nodes.push({ + id: attr(openTag, 'data-node-id'), + kind: attr(openTag, 'data-node-kind') || 'backend', + label: attr(openTag, 'data-node-label') || '', + sublabel: attr(openTag, 'data-node-sublabel'), + tag: attr(openTag, 'data-node-tag'), + x: box.x, + y: box.y, + width: box.width, + height: box.height, + // Strict-mode visual fields (all optional; fall back gracefully). + fillClass: fillTag ? attr(fillTag, 'class') : null, + rx: fillTag ? parseFloat(attr(fillTag, 'rx') || '0') : 0, + strokeWidth: fillTag ? parseFloat(attr(fillTag, 'stroke-width') || '1.5') : 1.5, + }); + } + return nodes; +} + +/** + * Parse all edges from the SVG. Each rendered connection is a `` that + * carries `data-composition-points` (the complete routed polyline). Two + * attribute-prefix conventions exist across renderers: + * (a) architecture/workflow/dataflow/lifecycle — + * `` + * (b) sequence — the outer `` holds the label, while the + * inner `` + * holds the geometry. + * + * We anchor exclusively on paths that carry `data-composition-points`: this + * naturally skips the label-wrapper `` duplicates (which have no points) + * and yields exactly one entry per connection. + */ +const EDGE_POINTS_RE = /]*\bdata-composition-points="([^"]*)"[^>]*>/g; + +/** + * Message labels for sequence diagrams live on the FOCUS GROUP wrapping the + * path (``), not on the path itself. + * Index those labels so parseEdges can fall back to them. + */ +function outerEdgeLabels(svg) { + const byId = new Map(); + const byPair = new Map(); + const re = /]*\bdata-edge-label="([^"]*)"[^>]*>/g; + for (const m of svg.matchAll(re)) { + const from = attr(m[0], 'data-edge-from'); + const to = attr(m[0], 'data-edge-to'); + const id = attr(m[0], 'data-edge-id'); + if (id) byId.set(id, m[1]); + if (from && to) byPair.set(`${from}\u0000${to}`, m[1]); + } + return { byId, byPair }; +} + +function parseEdges(svg) { + const edges = []; + const outer = outerEdgeLabels(svg); + const matches = [...svg.matchAll(EDGE_POINTS_RE)]; + for (const m of matches) { + const tag = m[0]; + const points = parsePoints(m[1]); + if (points.length < 2) continue; + // Accept either prefix; sequence uses data-composition-edge-*. + const from = attr(tag, 'data-edge-from') || attr(tag, 'data-composition-edge-from'); + const to = attr(tag, 'data-edge-to') || attr(tag, 'data-composition-edge-to'); + if (!from || !to) continue; + const id = attr(tag, 'data-edge-id') || attr(tag, 'data-composition-edge-id'); + const label = attr(tag, 'data-edge-label') || attr(tag, 'data-composition-edge-label') + || (id ? outer.byId.get(id) : undefined) || outer.byPair.get(`${from}\u0000${to}`); + edges.push({ + from, to, label: label ?? null, id, points, + // Strict-mode visual fields. + strokeClass: attr(tag, 'class'), + strokeWidth: parseFloat(attr(tag, 'stroke-width') || '1.5'), + radius: cornerRadiusFromD(attr(tag, 'd')), + // Sequence return messages carry their dash as an inline attribute. + inlineDash: attr(tag, 'stroke-dasharray'), + }); + } + return edges; +} + +/** + * Parse structural frames (boundaries, lanes, stages, segments) that act as + * containers. Only the geometry + label come from the SVG; the membership + * (which nodes belong inside) is resolved from the JSON IR by the caller. + */ +function parseBoundaries(svg) { + const boundaries = []; + const re = /]*\bdata-graph-role="structural-frame"[^>]*>/g; + const matches = [...svg.matchAll(re)]; + for (const m of matches) { + const tag = m[0]; + const kind = attr(tag, 'data-composition-frame-kind') || 'boundary'; + const frameId = attr(tag, 'data-composition-frame-id'); + const box = parseRect(tag); + if (box.width <= 0 || box.height <= 0) continue; + // The boundary label lives in the immediately following the rect. + let label = ''; + let labelClass = null; + const afterRect = svg.slice(m.index + tag.length, m.index + tag.length + 300); + const textMatch = afterRect.match(/]*)>([^<]*)<\/text>/); + if (textMatch) { + label = textMatch[2].trim(); + labelClass = attr(``, 'class'); + } + boundaries.push({ + kind, frameId, label, ...box, + // Strict-mode visual fields. + fillClass: attr(tag, 'class'), + rx: parseFloat(attr(tag, 'rx') || '0'), + strokeWidth: parseFloat(attr(tag, 'stroke-width') || '1'), + labelClass, + }); + } + return boundaries; +} + +/** + * Parse sequence lifelines: vertical dashed ``. + * These are rendered as standalone vertical lines (not connected edges). + */ +function parseLifelines(svg) { + const lifelines = []; + // The stroke-dasharray guard keeps solid vertical edges (e.g. lifecycle + // drop transitions) from being mistaken for sequence lifelines. + const re = /]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray[^>]*>/g; + const matches = [...svg.matchAll(re)]; + for (const m of matches) { + lifelines.push({ + x: parseFloat(m[1]), + y1: parseFloat(m[2]), + y2: parseFloat(m[3]), + strokeClass: attr(m[0], 'class'), + strokeWidth: parseFloat(attr(m[0], 'stroke-width') || '0.8'), + }); + } + return lifelines; +} + +/** + * Parse sequence activation bars: kind-colored rects carrying + * `data-graph-role="activation"` + `data-activation-participant`. The twin + * `c-mask` backdrop (same identity, no color info) is skipped. + */ +const ACTIVATION_RE = /]*\bdata-graph-role="activation"[^>]*>/g; + +function parseActivations(svg) { + const activations = []; + for (const m of svg.matchAll(ACTIVATION_RE)) { + const tag = m[0]; + const cls = attr(tag, 'class') || ''; + if (!cls || cls === 'c-mask') continue; + const box = parseRect(tag); + if (box.width <= 0 || box.height <= 0) continue; + activations.push({ + participant: attr(tag, 'data-activation-participant'), + fillClass: cls, + rx: parseFloat(attr(tag, 'rx') || '0'), + strokeWidth: parseFloat(attr(tag, 'stroke-width') || '1'), + ...box, + }); + } + return activations; +} + +/** + * Sequence segment bands (Request/Fallback/…) render their labels as separate + * `g[data-graph-role="segment-label"]` groups (they dodge message labels), so + * match them back onto the boundary by segment id. + */ +const SEGMENT_LABEL_RE = /]*\bdata-graph-role="segment-label"[^>]*\bdata-segment-id="([^"]*)"[^>]*>[\s\S]*?]*>([^<]*)<\/text>/g; + +function applySegmentLabels(boundaries, svg) { + for (const m of svg.matchAll(SEGMENT_LABEL_RE)) { + const boundary = boundaries.find((b) => b.frameId === m[1]); + if (boundary) boundary.label = m[2].trim(); + } +} + +/** + * Extract the full self-describing intermediate representation from an + * Archify SVG string. + */ +export function parseArchifySvg(svg, _diagramType) { + const viewBoxMatch = svg.match(/]*\bviewBox="0 0 (\d+\.?\d*) (\d+\.?\d*)"/); + const viewBox = viewBoxMatch + ? [parseFloat(viewBoxMatch[1]), parseFloat(viewBoxMatch[2])] + : [1000, 700]; + const boundaries = parseBoundaries(svg); + applySegmentLabels(boundaries, svg); + return { + viewBox, + nodes: parseNodes(svg), + edges: parseEdges(svg), + boundaries, + lifelines: parseLifelines(svg), + activations: parseActivations(svg), + edgeLabels: parseEdgeLabels(svg), + }; +} + +// ─── draw.io style mapping ────────────────────────────────────────────────── + +/** + * Map an Archify component kind to a draw.io base style string. + * Uses draw.io's built-in shapes so each type reads with correct semantics. + */ +const KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;', +}; + +/** + * Map lifecycle state types (start/active/decision/...) to draw.io styles. + */ +const STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;', +}; + +/** + * Map boundary/frame kinds to container styles. + */ +const FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', +}; + +const VARIANT_EDGE_STYLE = { + default: 'edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;', + emphasis: 'edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#0891b2;strokeWidth=2;', + security: 'edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#d79b00;strokeWidth=2;', + dashed: 'edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;dashed=1;', + return: 'edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#82b366;dashed=1;', +}; + +/** + * Pick a node style. Lifecycle states use their own type enum; everything + * else uses the componentType map. + */ +function nodeStyle(node, diagramType) { + if (diagramType === 'lifecycle' && STATE_STYLE[node.kind]) { + return STATE_STYLE[node.kind]; + } + return KIND_STYLE[node.kind] || KIND_STYLE.backend; +} + +// ─── draw.io XML generation ───────────────────────────────────────────────── + +const LIFELINE_STYLE = 'endArrow=none;html=1;strokeColor=#999999;dashed=1;dashPattern=3 7;'; + +/** + * Resolve which boundary each node belongs to. Membership is taken from the + * JSON IR (the `wraps` field on architecture boundaries) when available; + * otherwise we fall back to geometric containment (node center inside frame). + * + * Returns a Map where offsetX/Y is the + * parent container's top-left corner — needed because draw.io child cell + * coordinates are RELATIVE to their parent, not absolute. + */ +function resolveContainerParents(parsed, diagram) { + const parents = new Map(); + const boundaries = parsed.boundaries; + if (!boundaries.length) return parents; + + // Prefer explicit wraps from the JSON IR (architecture). The architecture + // renderer stamps each JSON boundary with its array index as + // data-composition-frame-id, so resolve membership by that id rather than + // by position: a boundary the SVG parser skipped (zero-size frames) then + // only loses its own wraps instead of shifting every later boundary onto + // the wrong frame — and onto a parent cell id that does not exist. + // Synthetic inputs without frame-ids keep the positional pairing. + const explicit = Array.isArray(diagram?.boundaries); + if (explicit) { + const stamped = boundaries.some((b) => b.frameId != null); + const parsedIndexOf = new Map(boundaries.map((b, i) => [b.frameId, i])); + for (let i = 0; i < diagram.boundaries.length; i += 1) { + const b = diagram.boundaries[i]; + const parsedIndex = stamped ? parsedIndexOf.get(String(i)) : i; + if (parsedIndex === undefined) continue; + const frame = boundaries[parsedIndex]; + const drawioId = `boundary-${parsedIndex}`; + for (const wrappedId of b.wraps || []) { + // Innermost wins: a node wrapped by multiple boundaries (e.g. + // region + security-group) parents to the last/most-specific one. + parents.set(wrappedId, { drawioId, offsetX: frame.x, offsetY: frame.y }); + } + } + return parents; + } + + // Geometric fallback: assign each node to the smallest boundary whose rect + // contains the node's center. + const candidates = boundaries + .map((b, i) => ({ ...b, drawioId: `boundary-${i}` })) + .sort((a, b) => a.width * a.height - b.width * b.height); + for (const node of parsed.nodes) { + const cx = node.x + node.width / 2; + const cy = node.y + node.height / 2; + for (const b of candidates) { + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents.set(node.id, { drawioId: b.drawioId, offsetX: b.x, offsetY: b.y }); + break; + } + } + } + return parents; +} + +/** + * Build the mxGraphModel XML from the parsed SVG representation. + * `diagram` is the original JSON IR (used only for boundary `wraps`). + */ +export function buildDrawioXml(parsed, diagramType, diagram = null) { + const [vw, vh] = parsed.viewBox; + const nodeParents = resolveContainerParents(parsed, diagram); + const nodeIdOf = (id) => `node-${id}`; + + const lines = []; + lines.push(''); + lines.push( + ``, + ); + lines.push(' '); + lines.push(' '); + lines.push(' '); + + // Boundaries first (they become parent containers). + for (let i = 0; i < parsed.boundaries.length; i += 1) { + const b = parsed.boundaries[i]; + const id = `boundary-${i}`; + const style = FRAME_STYLE[b.kind] || FRAME_STYLE.region; + lines.push( + ` `, + ); + lines.push( + ` `, + ); + lines.push(' '); + } + + // Nodes. + for (const node of parsed.nodes) { + const id = nodeIdOf(node.id); + const style = nodeStyle(node, diagramType); + const parentInfo = nodeParents.get(node.id); + const parentId = parentInfo ? parentInfo.drawioId : '1'; + // draw.io child cells use coordinates RELATIVE to their parent container. + // Subtract the parent's origin so the node lands at its authored position. + const relX = node.x - (parentInfo ? parentInfo.offsetX : 0); + const relY = node.y - (parentInfo ? parentInfo.offsetY : 0); + const labelParts = [node.label, node.sublabel, node.tag].filter(Boolean); + // Build the draw.io label as an HTML fragment. The raw text is inserted + // without pre-escaping (Archify labels are already user-facing copy, not + // markup); the whole fragment is then XML-attribute-escaped once so that + // the HTML tags survive as entities and draw.io decodes+renders them. + const htmlValue = labelParts.length > 1 + ? `${labelParts[0]}
${labelParts.slice(1).join(' · ')}` + : node.label; + const value = escapeAttr(htmlValue); + lines.push( + ` `, + ); + lines.push( + ` `, + ); + lines.push(' '); + } + + // Sequence lifelines (standalone vertical dashed edges, no source/target). + for (const ll of parsed.lifelines) { + const id = `lifeline-${Math.round(ll.x)}-${Math.round(ll.y1)}`; + lines.push( + ` `, + ); + lines.push(' '); + lines.push( + ` `, + ); + lines.push( + ` `, + ); + lines.push(' '); + lines.push(' '); + } + + // Edges: real source/target binding + fixed waypoints. + for (let i = 0; i < parsed.edges.length; i += 1) { + const edge = parsed.edges[i]; + const sourceId = nodeIdOf(edge.from); + const targetId = nodeIdOf(edge.to); + const id = edge.id ? `edge-${edge.id}` : `edge-${i}`; + const style = VARIANT_EDGE_STYLE.default; + const value = edge.label ? escapeAttr(edge.label) : ''; + const hasValidBinding = parsed.nodes.some((n) => n.id === edge.from) + && parsed.nodes.some((n) => n.id === edge.to); + if (!hasValidBinding) continue; + lines.push( + ` `, + ); + lines.push(' '); + if (edge.points && edge.points.length >= 2) { + // Drop the first and last points: draw.io computes them from the + // boundied source/target vertices. Keep the interior waypoints so the + // authored route shape is preserved. + const interior = edge.points.slice(1, -1); + if (interior.length) { + lines.push(' '); + for (const [x, y] of interior) { + lines.push(` `); + } + lines.push(' '); + } + } + lines.push(' '); + lines.push(' '); + } + + lines.push('
'); + lines.push('
'); + return lines.join('\n'); +} + +// ─── Sequence builder: timeline semantics via UML lifelines ──────────────── + +/** draw.io kind palette for the non-strict sequence variant. */ +function kindColorPair(kind) { + const style = KIND_STYLE[kind] || KIND_STYLE.backend; + return { + fill: style.match(/fillColor=(#[0-9a-f]{6})/i)?.[1], + stroke: style.match(/strokeColor=(#[0-9a-f]{6})/i)?.[1], + }; +} + +/** + * Build the sequence mxGraphModel with draw.io's native UML lifeline model: + * - participants become `shape=umlLifeline` cells spanning header→timeline + * foot (rendered as header box + dashed line), so a message's authored y + * survives as exitY/entryY fractions of the full timeline; + * - activation bars become thin child rects of their lifeline; + * - segment bands become labeled background frames; + * - messages bind lifeline→lifeline with exact exit/entry y and keep their + * labels and return-message dashes. + * With `strict: false` the same topology uses draw.io's default palette. + */ +export function buildDrawioXmlSequence(parsed, palette, diagram = null, { strict = true } = {}) { + const [vw, vh] = parsed.viewBox; + const nodeById = new Map(parsed.nodes.map((n) => [n.id, n])); + const FONT_STACK = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + // Timeline foot: deepest lifeline end, else just below the last header, + // else a sane default when the artifact carries neither (viewer parity). + const footY = Math.max(0, ...parsed.lifelines.map((l) => l.y2)) + || Math.max(0, ...parsed.nodes.map((n) => n.y + n.height + 60)) + || 400; + const colors = (className, base) => ( + strict && className && palette ? palette.paletteFor(className, base) : null + ); + const dashOf = (inline, classDash) => normalizeDash(inline) || normalizeDash(classDash); + + const lines = []; + lines.push(''); + lines.push( + ``, + ); + lines.push(' '); + lines.push(' '); + lines.push(' '); + + // Segment bands (background frames, never parents of participants). + for (let i = 0; i < parsed.boundaries.length; i += 1) { + const b = parsed.boundaries[i]; + const lane = colors(b.fillClass, 'bg'); + const rx = Number.isFinite(b.rx) && b.rx > 0 ? Math.round(b.rx) : 0; + const style = [ + 'rounded=' + (rx > 0 ? 1 : 0), + ...(rx > 0 ? ['absoluteArcSize=1', `arcSize=${rx}`] : []), + `fillColor=${lane?.fill || 'none'}`, + `strokeColor=${lane?.stroke || '#b0b0b0'}`, + `strokeWidth=${b.strokeWidth || 1}`, + ...(dashOf(null, lane?.dash) ? ['dashed=1', `dashPattern=${dashOf(null, lane?.dash)}`] : []), + `fontColor=${strict ? palette.textMuted : '#666666'}`, + 'fontSize=9', + `fontFamily=${FONT_STACK}`, + 'whiteSpace=wrap', + 'html=1', + 'verticalAlign=top', + 'align=left', + 'spacingLeft=8', + ].join(';') + ';'; + lines.push( + ` `, + ); + lines.push( + ` `, + ); + lines.push(' '); + } + + // Participants as UML lifelines spanning the full timeline. + for (const node of parsed.nodes) { + const c = colors(node.fillClass, 'mask') || (strict ? null : kindColorPair(node.kind)); + const labelParts = [node.label, node.sublabel].filter(Boolean); + const htmlValue = labelParts.length > 1 + ? `${labelParts[0]}
${labelParts.slice(1).join(' · ')}` + : node.label; + const style = [ + 'shape=umlLifeline', + 'perimeter=lifelinePerimeter', + `size=${Math.round(node.height)}`, + 'container=1', + 'collapsible=0', + 'recursiveResize=0', + 'outlineConnect=0', + ...(c?.fill ? [`fillColor=${c.fill}`] : []), + ...(c?.stroke ? [`strokeColor=${c.stroke}`] : []), + `strokeWidth=${node.strokeWidth || 1.5}`, + `fontColor=${strict ? palette.text : '#000000'}`, + 'fontSize=11', + `fontFamily=${FONT_STACK}`, + 'whiteSpace=wrap', + 'html=1', + ].join(';') + ';'; + lines.push( + ` `, + ); + lines.push( + ` `, + ); + lines.push(' '); + } + + // Activation bars: thin child rects of their lifeline. + for (let i = 0; i < parsed.activations.length; i += 1) { + const act = parsed.activations[i]; + const participant = act.participant ? nodeById.get(act.participant) : null; + const c = colors(act.fillClass, 'mask') || (strict ? null : kindColorPair(participant?.kind)); + if (!participant) continue; + const rx = Number.isFinite(act.rx) && act.rx > 0 ? Math.round(act.rx) : 0; + const style = [ + 'rounded=' + (rx > 0 ? 1 : 0), + ...(rx > 0 ? ['absoluteArcSize=1', `arcSize=${rx}`] : []), + `fillColor=${c?.fill || '#ffffff'}`, + `strokeColor=${c?.stroke || '#999999'}`, + `strokeWidth=${act.strokeWidth || 1}`, + 'html=1', + ].join(';') + ';'; + lines.push( + ` `, + ); + lines.push( + ` `, + ); + lines.push(' '); + } + + // Messages: lifeline→lifeline edges with the authored y as exit/entry + // fractions of each endpoint's full timeline height. + for (let i = 0; i < parsed.edges.length; i += 1) { + const edge = parsed.edges[i]; + const src = nodeById.get(edge.from); + const tgt = nodeById.get(edge.to); + if (!src || !tgt) continue; + const c = colors(edge.strokeClass, 'bg'); + const sw = Number.isFinite(edge.strokeWidth) ? edge.strokeWidth : 1.5; + const labelText = parsed.edgeLabels?.find( + (l) => l.from === edge.from && l.to === edge.to && (l.label || '') === (edge.label || ''), + ) || parsed.edgeLabels?.find((l) => l.from === edge.from && l.to === edge.to); + const labelColors = strict && labelText?.className ? palette.paletteFor(labelText.className, 'bg') : null; + const dash = dashOf(edge.inlineDash, c?.dash); + const fracY = (y, cell) => Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))); + const exitY = fracY(edge.points[0][1], src); + const entryY = fracY(edge.points[edge.points.length - 1][1], tgt); + const style = [ + 'edgeStyle=none', + 'rounded=0', + `strokeColor=${c?.stroke || '#666666'}`, + `strokeWidth=${sw}`, + ...(dash ? ['dashed=1', `dashPattern=${dash}`] : []), + 'endArrow=block', + 'endFill=1', + `endSize=${Math.max(4, Math.round(10 * sw))}`, + `fontColor=${labelColors?.fill || (strict ? palette.textMuted : '#666666')}`, + 'fontSize=8', + `fontFamily=${FONT_STACK}`, + `labelBackgroundColor=${strict ? palette.mask : '#ffffff'}`, + 'html=1', + ].join(';') + + `;exitX=0.5;exitY=${exitY.toFixed(3)};exitDx=0;exitDy=0` + + `;entryX=0.5;entryY=${entryY.toFixed(3)};entryDx=0;entryDy=0;`; + const id = edge.id ? `edge-${edge.id}` : `edge-${i}`; + lines.push( + ` `, + ); + // Label geometry y=-1 lifts the label one label-height above the line, + // matching the Archify artifact (labels sit above their message). + lines.push(' '); + const interior = edge.points.slice(1, -1); + if (interior.length) { + lines.push(' '); + for (const [x, y] of interior) { + lines.push(` `); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + } + + lines.push('
'); + lines.push('
'); + return lines.join('\n'); +} + +/** + * One-shot convenience: parse an Archify SVG and emit draw.io XML. + * Pass `{ strict: true, css }` to emit the 1:1 visual-fidelity variant. + * Sequence diagrams always route through the dedicated sequence builder: + * their geometry is a timeline (lifelines + messages at authored y), not a + * node graph, so generic border-anchored edges would collapse the timeline. + */ +export function convertArchifyToDrawio(svg, diagramType, diagram = null, options = {}) { + const parsed = parseArchifySvg(svg, diagramType); + if (options.strict) { + const palette = extractPalette(options.css || ''); + return diagramType === 'sequence' + ? buildDrawioXmlSequence(parsed, palette, diagram, { strict: true }) + : buildDrawioXmlStrict(parsed, palette, diagramType, diagram); + } + return diagramType === 'sequence' + ? buildDrawioXmlSequence(parsed, null, diagram, { strict: false }) + : buildDrawioXml(parsed, diagramType, diagram); +} + +/** + * Extract the inner `...` fragment from a rendered Archify HTML + * artifact. Returns the SVG string (without the surrounding HTML). + */ +export function extractSvgFromHtml(html) { + const start = html.indexOf(''); + if (start === -1 || end === -1) { + throw new Error('No element found in the rendered HTML artifact.'); + } + return html.slice(start, end + ''.length); +} + +// ─── Strict palette: resolve Archify CSS to concrete colors ──────────────── + +function parseColorValue(value) { + const v = String(value).trim(); + const rgba = v.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (rgba) { + return { r: parseFloat(rgba[1]), g: parseFloat(rgba[2]), b: parseFloat(rgba[3]), a: rgba[4] === undefined ? 1 : parseFloat(rgba[4]) }; + } + const hex = v.match(/^#([0-9a-f]{6})$/i); + if (hex) { + return { + r: parseInt(hex[1].slice(0, 2), 16), + g: parseInt(hex[1].slice(2, 4), 16), + b: parseInt(hex[1].slice(4, 6), 16), + a: 1, + }; + } + return null; +} + +function colorToHex(c) { + const h = (n) => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, '0'); + return `#${h(c.r)}${h(c.g)}${h(c.b)}`; +} + +/** + * Alpha-composite `fg` over an opaque `bg`. Semi-transparent Archify fills + * (e.g. rgba(8, 51, 68, 0.4)) sit on an opaque mask or page background, so the + * pre-composited hex reproduces the rendered pixel exactly in draw.io, which + * has no two-layer shape fill. + */ +function blendOver(fg, bg) { + const a = fg.a; + return { + r: fg.r * a + bg.r * (1 - a), + g: fg.g * a + bg.g * (1 - a), + b: fg.b * a + bg.b * (1 - a), + a: 1, + }; +} + +/** + * Extract the visual palette from the rendered artifact's CSS. Variables + * always resolve from the LIGHT theme block so the exported diagram is a + * stable light-themed file regardless of the artifact's dark default or any + * viewer/browser theme preference. The dark `:root` block is only a fallback + * for custom templates that define no light theme. Preset-scoped rules such as + * `svg[data-preset=…] .c-x` are intentionally skipped because their selectors + * do not start at a class name. + */ +export function extractPalette(css) { + // Comments inside declaration blocks would break per-`;` splitting. + const cleanCss = String(css || '').replace(/\/\*[\s\S]*?\*\//g, ''); + // Variable blocks. The light selector is anchored to line start so compound + // selectors (`[data-preset=…][data-theme="light"]`, + // `:root, [data-theme="dark"], [data-theme="light"]`) never match. + const vars = {}; + const varBlock = cleanCss.match(/^[ \t]*\[data-theme="light"\]\s*\{([^}]*)\}/m)?.[1] + // Artifact default, used only when no light theme exists. + ?? cleanCss.match(/:root\s*,\s*\[data-theme="dark"\]\s*\{([^}]*)\}/)?.[1] + ?? ''; + if (varBlock) { + for (const rawDecl of varBlock.split(';')) { + const m = rawDecl.trim().match(/^([\w-]+)\s*:\s*(.+)$/); + if (m) vars[m[1]] = m[2].trim(); + } + } + const resolveVar = (value) => { + let v = String(value || '').trim(); + for (let depth = 0; depth < 8; depth += 1) { + const m = v.match(/^var\(\s*([\w-]+)\s*(?:,\s*([^)]+))?\)$/); + if (!m) return v; + v = vars[m[1]] ?? (m[2] ? m[2].trim() : ''); + } + return v; + }; + // Plain class rules only (line-start selectors). + const rules = {}; + const ruleRe = /^[ \t]*\.([A-Za-z][\w-]*)\s*\{([^}]*)\}/gm; + for (const m of cleanCss.matchAll(ruleRe)) { + const decls = {}; + for (const rawDecl of m[2].split(';')) { + const d = rawDecl.trim().match(/^([\w-]+)\s*:\s*(.+)$/); + if (d) decls[d[1]] = d[2].trim(); + } + rules[m[1]] = decls; + } + const maskColor = parseColorValue(resolveVar(vars['--mask'] || '#ffffff')) || { r: 255, g: 255, b: 255, a: 1 }; + const bgColor = parseColorValue(resolveVar(vars['--bg'] || '#ffffff')) || { r: 255, g: 255, b: 255, a: 1 }; + const textColor = parseColorValue(resolveVar(vars['--text'] || '#000000')) || { r: 0, g: 0, b: 0, a: 1 }; + const mutedColor = parseColorValue(resolveVar(vars['--text-muted'] || '#888888')) || { r: 136, g: 136, b: 136, a: 1 }; + + /** + * Resolve a class to concrete colors. `over` picks the compositing base: + * 'mask' for node fills (mask + translucent fill), 'bg' for frames. + */ + const paletteFor = (className, over = 'mask') => { + const decls = rules[className]; + if (!decls) return null; + const base = over === 'mask' ? maskColor : bgColor; + const out = {}; + if (decls.fill) { + const raw = parseColorValue(resolveVar(decls.fill)); + if (raw) out.fill = colorToHex(raw.a >= 1 ? raw : blendOver(raw, base)); + } + if (decls.stroke) { + const raw = parseColorValue(resolveVar(decls.stroke)); + if (raw) out.stroke = colorToHex(raw.a >= 1 ? raw : blendOver(raw, base)); + } + if (decls['stroke-dasharray']) { + out.dash = resolveVar(decls['stroke-dasharray']).replace(/\s+/g, ' ').trim(); + } + return out; + }; + + return { + paletteFor, + mask: colorToHex(maskColor), + bg: colorToHex(bgColor), + text: colorToHex(textColor), + textMuted: colorToHex(mutedColor), + }; +} + +// ─── Strict builder: 1:1 shape / color / corner fidelity ─────────────────── + +const FALLBACK_NODE_FILL = '#d5e8d4'; +const FALLBACK_STROKE = '#82b366'; + +function normalizeDash(dash) { + if (!dash) return null; + const parts = dash.split(/[\s,]+/).filter(Boolean).map(Number); + if (!parts.length || parts.some((n) => !Number.isFinite(n) || n <= 0)) return null; + return parts.join(' '); +} + +/** Pin the exact authored connection point on a node border (0..1 floats). */ +function borderFraction(point, node) { + if (!node || !node.width || !node.height) return null; + const fx = (point[0] - node.x) / node.width; + const fy = (point[1] - node.y) / node.height; + // Snap near-border values so draw.io treats them as side anchors. + const snap = (v) => (v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100); + return [snap(fx), snap(fy)]; +} + +/** + * Fraction t∈[0,1] along the polyline where the point closest to `target` + * sits (used to position edge labels at their authored location). + */ +function fractionAlongPolyline(points, target) { + let total = 0; + const segs = []; + for (let i = 0; i < points.length - 1; i += 1) { + const [ax, ay] = points[i]; + const [bx, by] = points[i + 1]; + const len = Math.hypot(bx - ax, by - ay) || 1; + segs.push({ ax, ay, dx: bx - ax, dy: by - ay, len, start: total }); + total += len; + } + let best = Infinity; + let result = 0.5; + for (const s of segs) { + const t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + const px = s.ax + t * s.dx; + const py = s.ay + t * s.dy; + const dist = Math.hypot(target[0] - px, target[1] - py); + if (dist < best) { + best = dist; + result = (s.start + t * s.len) / (total || 1); + } + } + return Math.max(0, Math.min(1, result)); +} + +/** + * Build the strict (1:1) mxGraphModel: every node keeps the Archify rounded + * rectangle with its exact corner radius, exact composited fill/stroke colors, + * exact stroke width and dash pattern; edges keep their exact routed polyline + * with the measured corner radius (`arcSize` = 2 × radius in draw.io) and their + * exact anchor points pinned via exitX/exitY / entryX/entryY. + */ +export function buildDrawioXmlStrict(parsed, palette, diagramType, diagram = null) { + const [vw, vh] = parsed.viewBox; + const nodeParents = resolveContainerParents(parsed, diagram); + const nodeById = new Map(parsed.nodes.map((n) => [n.id, n])); + const FONT_STACK = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + + const strictNodeStyle = (node) => { + const colors = node.fillClass ? palette.paletteFor(node.fillClass, 'mask') : null; + const rx = Number.isFinite(node.rx) && node.rx > 0 ? Math.round(node.rx) : 0; + const sw = Number.isFinite(node.strokeWidth) ? node.strokeWidth : 1.5; + return [ + 'rounded=' + (rx > 0 ? 1 : 0), + ...(rx > 0 ? ['absoluteArcSize=1', `arcSize=${rx}`] : []), + `fillColor=${colors?.fill || FALLBACK_NODE_FILL}`, + `strokeColor=${colors?.stroke || FALLBACK_STROKE}`, + `strokeWidth=${sw}`, + `fontColor=${palette.text}`, + 'fontSize=11', + `fontFamily=${FONT_STACK}`, + 'whiteSpace=wrap', + 'html=1', + 'verticalAlign=middle', + ].join(';') + ';'; + }; + + const strictBoundaryStyle = (b) => { + const colors = b.fillClass ? palette.paletteFor(b.fillClass, 'bg') : null; + const rx = Number.isFinite(b.rx) && b.rx > 0 ? Math.round(b.rx) : 0; + const dash = normalizeDash(colors?.dash); + const labelColors = b.labelClass ? palette.paletteFor(b.labelClass, 'bg') : null; + const alignLeft = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + return [ + 'rounded=' + (rx > 0 ? 1 : 0), + ...(rx > 0 ? ['absoluteArcSize=1', `arcSize=${rx}`] : []), + `fillColor=${colors?.fill || 'none'}`, + `strokeColor=${colors?.stroke || '#666666'}`, + `strokeWidth=${b.strokeWidth || 1}`, + ...(dash ? ['dashed=1', `dashPattern=${dash}`] : []), + `fontColor=${labelColors?.fill || palette.textMuted}`, + 'fontSize=9', + `fontFamily=${FONT_STACK}`, + 'whiteSpace=wrap', + 'html=1', + 'verticalAlign=top', + ...(alignLeft ? ['align=left', 'spacingLeft=8'] : []), + ].join(';') + ';'; + }; + + const labelFor = (edge, index) => parsed.edgeLabels?.find( + (l) => l.from === edge.from && l.to === edge.to && (l.label || '') === (edge.label || ''), + ) || parsed.edgeLabels?.find((l) => l.from === edge.from && l.to === edge.to); + + const lines = []; + lines.push(''); + lines.push( + ``, + ); + lines.push(' '); + lines.push(' '); + lines.push(' '); + + // Boundaries. + for (let i = 0; i < parsed.boundaries.length; i += 1) { + const b = parsed.boundaries[i]; + lines.push( + ` `, + ); + lines.push( + ` `, + ); + lines.push(' '); + } + + // Nodes. + for (const node of parsed.nodes) { + const parentInfo = nodeParents.get(node.id); + const parentId = parentInfo ? parentInfo.drawioId : '1'; + const relX = node.x - (parentInfo ? parentInfo.offsetX : 0); + const relY = node.y - (parentInfo ? parentInfo.offsetY : 0); + const labelParts = [node.label, node.sublabel, node.tag].filter(Boolean); + const tagColor = node.fillClass ? (palette.paletteFor(node.fillClass, 'mask')?.stroke || palette.text) : palette.text; + const htmlValue = labelParts.length > 1 + ? `${node.label}
` + + (node.sublabel ? `${node.sublabel}` : '') + + (node.sublabel && node.tag ? '
' : '') + + (node.tag ? `${node.tag}` : '') + : node.label; + lines.push( + ` `, + ); + lines.push( + ` `, + ); + lines.push(' '); + } + + // Sequence lifelines. + for (const ll of parsed.lifelines) { + const id = `lifeline-${Math.round(ll.x)}-${Math.round(ll.y1)}`; + const colors = ll.strokeClass ? palette.paletteFor(ll.strokeClass, 'bg') : null; + const style = [ + 'endArrow=none', + 'html=1', + `strokeColor=${colors?.stroke || '#999999'}`, + `strokeWidth=${ll.strokeWidth || 0.8}`, + 'dashed=1', + 'dashPattern=3 7', + ].join(';') + ';'; + lines.push(` `); + lines.push(' '); + lines.push(` `); + lines.push(` `); + lines.push(' '); + lines.push(' '); + } + + // Edges. + for (let i = 0; i < parsed.edges.length; i += 1) { + const edge = parsed.edges[i]; + const src = nodeById.get(edge.from); + const tgt = nodeById.get(edge.to); + if (!src || !tgt) continue; + const colors = edge.strokeClass ? palette.paletteFor(edge.strokeClass, 'bg') : null; + const sw = Number.isFinite(edge.strokeWidth) ? edge.strokeWidth : 1.5; + const dash = normalizeDash(colors?.dash); + const radius = Number.isFinite(edge.radius) ? edge.radius : 0; + const labelText = labelFor(edge, i); + const labelColors = labelText?.className ? palette.paletteFor(labelText.className, 'bg') : null; + const style = [ + 'edgeStyle=none', + ...(radius > 0 ? ['rounded=1', `arcSize=${radius * 2}`] : ['rounded=0']), + `strokeColor=${colors?.stroke || '#666666'}`, + `strokeWidth=${sw}`, + ...(dash ? ['dashed=1', `dashPattern=${dash}`] : []), + 'endArrow=block', + 'endFill=1', + `endSize=${Math.max(4, Math.round(10 * sw))}`, + `fontColor=${labelColors?.fill || palette.textMuted}`, + 'fontSize=8', + `fontFamily=${FONT_STACK}`, + `labelBackgroundColor=${palette.mask}`, + 'html=1', + ].join(';') + ';'; + + const exit = borderFraction(edge.points[0], src); + const entry = borderFraction(edge.points[edge.points.length - 1], tgt); + const styleAttrs = [ + ...(exit ? [`exitX=${exit[0]}`, `exitY=${exit[1]}`, 'exitDx=0', 'exitDy=0'] : []), + ...(entry ? [`entryX=${entry[0]}`, `entryY=${entry[1]}`, 'entryDx=0', 'entryDy=0'] : []), + ].join(';'); + const id = edge.id ? `edge-${edge.id}` : `edge-${i}`; + lines.push( + ` `, + ); + lines.push(' '); + if (labelText && edge.label) { + const t = fractionAlongPolyline(edge.points, [labelText.x, labelText.y]); + lines.push(` `); + } + const interior = edge.points.slice(1, -1); + if (interior.length) { + lines.push(' '); + for (const [x, y] of interior) { + lines.push(` `); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + } + + lines.push('
'); + lines.push('
'); + return lines.join('\n'); +} diff --git a/archify/renderers/shared/utils.mjs b/archify/renderers/shared/utils.mjs index 739a8e6..99c86ad 100644 --- a/archify/renderers/shared/utils.mjs +++ b/archify/renderers/shared/utils.mjs @@ -111,7 +111,7 @@ const TEMPLATE_PLACEHOLDERS = [ GUIDED_VIEWS_PLACEHOLDER, ]; -export function applyTemplate(template, { title, subtitle, svg, cards, visualPreset = 'classic', guidedViews = [], sourceEvidence = null }) { +export function applyTemplate(template, { title, subtitle, svg, cards, visualPreset = 'classic', guidedViews = [], sourceEvidence = null, diagramType = '' }) { if (!SVG_SLOT_RE.test(template)) { throw new Error('applyTemplate: template missing ARCHIFY:SVG_SLOT sentinel'); } @@ -146,7 +146,7 @@ export function applyTemplate(template, { title, subtitle, svg, cards, visualPre ? `

${esc(subtitle)}

` : ''; return template - .replace(TEMPLATE_PLACEHOLDERS[0], () => ``) + .replace(TEMPLATE_PLACEHOLDERS[0], () => ``) .replace(TEMPLATE_PLACEHOLDERS[1], () => `${esc(title)} Diagram`) .replace(TEMPLATE_PLACEHOLDERS[2], () => `

${esc(title)}

`) .replace(SUBTITLE_SLOT_RE, (_match, indent, newline = '') => renderedSubtitle diff --git a/archify/scripts/export-drawio.mjs b/archify/scripts/export-drawio.mjs new file mode 100644 index 0000000..e523cbf --- /dev/null +++ b/archify/scripts/export-drawio.mjs @@ -0,0 +1,86 @@ +#!/usr/bin/env node +/** + * Archify → draw.io exporter. + * + * Pipeline: JSON IR --render--> HTML artifact --extract SVG--> draw.io XML. + * + * The SVG is the geometry source (it already contains the renderer's computed + * layout, routing, and port spreading); the JSON IR contributes semantic + * relationships that are not in the SVG (architecture boundary `wraps`). + * + * Usage: node scripts/export-drawio.mjs [output.drawio] [--strict] + */ + +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { extractSvgFromHtml, convertArchifyToDrawio } from '../renderers/shared/svg-to-drawio.mjs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const skillRoot = path.resolve(__dirname, '..'); + +const TYPES = new Set(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle']); +const DEFAULT_EXAMPLE = { + architecture: 'web-app.architecture.json', + workflow: 'agent-tool-call.workflow.json', + sequence: 'cache-miss-request.sequence.json', + dataflow: 'event-stream.dataflow.json', + lifecycle: 'agent-run.lifecycle.json', +}; + +function fail(message, code = 2) { + console.error(message); + process.exit(code); +} + +const argv = process.argv.slice(2).filter((arg) => arg !== '--strict'); +const strict = process.argv.includes('--strict'); +const [diagramType, inputArg, outputArg] = argv; + +if (!diagramType || !TYPES.has(diagramType)) { + fail(`Unknown diagram type "${diagramType}". Expected one of: ${[...TYPES].join(', ')}`); +} + +const inputPath = path.resolve(inputArg || path.join(skillRoot, 'examples', DEFAULT_EXAMPLE[diagramType])); +if (!fs.existsSync(inputPath)) { + fail(`Input file not found: ${inputPath}`, 1); +} + +// Resolve output path: explicit arg, or replace .json/.html with .drawio +// (strict appends "-strict", matching the viewer button's filename, so both +// variants can coexist). +const outputPath = outputArg + ? path.resolve(outputArg) + : inputPath.replace(/\.(json|html)$/, strict ? '-strict.drawio' : '.drawio'); + +// 1. Read + validate the JSON IR (light parse; full schema validation happens +// inside the renderer via loadDiagram). +const diagram = JSON.parse(fs.readFileSync(inputPath, 'utf8')); + +// 2. Render the HTML artifact via the type's renderer (reuse the existing, +// validated rendering pipeline). Output to a temp file. +const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-drawio-')); +const tmpHtml = path.join(tmpDir, 'rendered.html'); +try { + const rendererScript = path.join(skillRoot, `renderers/${diagramType}/render-${diagramType}.mjs`); + execFileSync(process.execPath, [rendererScript, inputPath, tmpHtml], { stdio: 'inherit' }); + + // 3. Extract the SVG and convert to draw.io XML. Strict mode additionally + // feeds the artifact CSS so shapes keep their exact Archify colors, + // corner radii, and dash patterns — always resolved from the light theme + // so the .drawio file reads correctly under any draw.io theme. + const html = fs.readFileSync(tmpHtml, 'utf8'); + const svg = extractSvgFromHtml(html); + const css = html.match(/]*>([\s\S]*?)<\/style>/)?.[1] || ''; + const drawioXml = convertArchifyToDrawio(svg, diagramType, diagram, { strict, css }); + + // 4. Write the .drawio file. + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, drawioXml); + console.log(outputPath); +} finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); +} diff --git a/archify/test/animation.test.mjs b/archify/test/animation.test.mjs index dffb995..92cf3f6 100644 --- a/archify/test/animation.test.mjs +++ b/archify/test/animation.test.mjs @@ -54,13 +54,13 @@ test('static output omits animation attributes', () => { test('classic preset remains the default for existing diagrams', () => { const html = render('architecture', CASES.architecture, null, null); - assert.match(html, //); + assert.match(html, / { const html = render('workflow', CASES.workflow, 'trace', 'signal-flow'); - assert.match(html, //); + assert.match(html, / { const html = render('architecture', CASES.architecture, null, 'blueprint'); - assert.match(html, //); + assert.match(html, / { test('editorial preset reaches every visual surface and all five typed renderers', () => { for (const [mode, example] of Object.entries(CASES)) { const html = render(mode, example, null, 'editorial'); - assert.match(html, //, mode); + assert.match(html, / { + const svg = ` + + + + + `; + const parsed = parseArchifySvg(svg, 'architecture'); + assert.equal(parsed.nodes.length, 1); + const node = parsed.nodes[0]; + assert.equal(node.id, 'a'); + assert.equal(node.kind, 'database'); + assert.equal(node.label, 'Alpha'); + assert.equal(node.x, 10); + assert.equal(node.y, 20); + assert.equal(node.width, 60); + assert.equal(node.height, 40); +}); + +test('parseArchifySvg extracts edges with composition points (path pattern)', () => { + const svg = ` + + `; + const parsed = parseArchifySvg(svg, 'architecture'); + assert.equal(parsed.edges.length, 1); + const edge = parsed.edges[0]; + assert.equal(edge.from, 'a'); + assert.equal(edge.to, 'b'); + assert.equal(edge.label, 'flow'); + assert.equal(edge.id, 'ab'); + assert.deepEqual(edge.points, [[70, 40], [130, 40]]); +}); + +test('parseArchifySvg extracts sequence edges (composition-edge prefix in g>path)', () => { + const svg = ` + + + + `; + const parsed = parseArchifySvg(svg, 'sequence'); + assert.equal(parsed.edges.length, 1); + assert.equal(parsed.edges[0].from, 'a'); + assert.equal(parsed.edges[0].to, 'b'); + assert.equal(parsed.edges[0].points.length, 2); +}); + +test('parseArchifySvg dedupes label-wrapper g duplicates', () => { + // The real SVG renders each edge twice: once as a with points, once + // as a label without points. Only the path should be captured. + const svg = ` + + + + + `; + const parsed = parseArchifySvg(svg, 'architecture'); + assert.equal(parsed.edges.length, 1); +}); + +test('parseArchifySvg extracts boundaries with kind and label', () => { + const svg = ` + + My Region + `; + const parsed = parseArchifySvg(svg, 'architecture'); + assert.equal(parsed.boundaries.length, 1); + assert.equal(parsed.boundaries[0].kind, 'region'); + assert.equal(parsed.boundaries[0].label, 'My Region'); +}); + +test('parseArchifySvg extracts sequence lifelines', () => { + const svg = ` + + + `; + const parsed = parseArchifySvg(svg, 'sequence'); + assert.equal(parsed.lifelines.length, 2); + assert.equal(parsed.lifelines[0].x, 50); +}); + +test('extractSvgFromHtml throws when no svg present', () => { + assert.throws(() => extractSvgFromHtml('no diagram'), /No /); +}); + +// ─── Unit: draw.io XML generation ────────────────────────────────────────── + +test('buildDrawioXml emits valid mxGraphModel with root cells', () => { + const parsed = { + viewBox: [400, 300], + nodes: [{ id: 'a', kind: 'backend', label: 'A', x: 10, y: 10, width: 80, height: 40 }], + edges: [{ from: 'a', to: 'a', points: [[50, 30], [50, 30]] }], + boundaries: [], + lifelines: [], + }; + const xml = buildDrawioXml(parsed, 'architecture'); + assert.match(xml, //); + assert.match(xml, //); + assert.match(xml, //); +}); + +test('buildDrawioXml binds edges to real vertex cell ids (not floating)', () => { + const parsed = { + viewBox: [400, 300], + nodes: [ + { id: 'a', kind: 'backend', label: 'A', x: 10, y: 10, width: 80, height: 40 }, + { id: 'b', kind: 'backend', label: 'B', x: 200, y: 10, width: 80, height: 40 }, + ], + edges: [{ from: 'a', to: 'b', label: 'call', id: 'ab', points: [[90, 30], [200, 30]] }], + boundaries: [], + lifelines: [], + }; + const xml = buildDrawioXml(parsed, 'architecture'); + // The core guarantee: source/target point to node vertex ids. + assert.match(xml, /edge="1"[^>]*source="node-a"[^>]*target="node-b"/); +}); + +test('buildDrawioXml preserves interior waypoints', () => { + const parsed = { + viewBox: [600, 300], + nodes: [ + { id: 'a', kind: 'backend', label: 'A', x: 10, y: 10, width: 80, height: 40 }, + { id: 'b', kind: 'backend', label: 'B', x: 400, y: 200, width: 80, height: 40 }, + ], + edges: [{ + from: 'a', to: 'b', id: 'ab', + points: [[90, 30], [200, 30], [200, 220], [400, 220]], + }], + boundaries: [], + lifelines: [], + }; + const xml = buildDrawioXml(parsed, 'architecture'); + assert.match(xml, //); + // First and last points are dropped (draw.io computes them from vertices). + assert.match(xml, //); + assert.match(xml, //); + // Endpoints should NOT appear as waypoints. + assert.doesNotMatch(xml, //); +}); + +test('buildDrawioXml maps node kind to draw.io shape styles', () => { + const parsed = { + viewBox: [400, 300], + nodes: [ + { id: 'db', kind: 'database', label: 'DB', x: 10, y: 10, width: 80, height: 40 }, + { id: 'cl', kind: 'cloud', label: 'Cloud', x: 100, y: 10, width: 80, height: 40 }, + ], + edges: [], + boundaries: [], + lifelines: [], + }; + const xml = buildDrawioXml(parsed, 'architecture'); + assert.match(xml, /shape=cylinder3/); + assert.match(xml, /shape=cloud/); +}); + +test('buildDrawioXml maps lifecycle state types to shapes', () => { + const parsed = { + viewBox: [400, 300], + nodes: [ + { id: 's', kind: 'start', label: 'Start', x: 10, y: 10, width: 80, height: 40 }, + { id: 'd', kind: 'decision', label: 'Decide', x: 100, y: 10, width: 80, height: 40 }, + ], + edges: [], + boundaries: [], + lifelines: [], + }; + const xml = buildDrawioXml(parsed, 'lifecycle'); + assert.match(xml, /ellipse;whiteSpace/); // start state + assert.match(xml, /rhombus;whiteSpace/); // decision state +}); + +test('buildDrawioXml parents nodes into boundaries via JSON wraps', () => { + const parsed = { + viewBox: [400, 300], + nodes: [ + { id: 'inner', kind: 'backend', label: 'Inner', x: 50, y: 50, width: 80, height: 40 }, + ], + edges: [], + boundaries: [{ kind: 'region', label: 'Box', x: 10, y: 10, width: 200, height: 200 }], + lifelines: [], + }; + const diagram = { boundaries: [{ wraps: ['inner'] }] }; + const xml = buildDrawioXml(parsed, 'architecture', diagram); + assert.match(xml, /id="node-inner"[^>]*parent="boundary-0"/); +}); + +test('buildDrawioXml skips edges with unresolvable endpoints', () => { + const parsed = { + viewBox: [400, 300], + nodes: [{ id: 'a', kind: 'backend', label: 'A', x: 10, y: 10, width: 80, height: 40 }], + edges: [ + { from: 'a', to: 'ghost', points: [[50, 30], [60, 30]] }, // ghost missing + ], + boundaries: [], + lifelines: [], + }; + const xml = buildDrawioXml(parsed, 'architecture'); + assert.doesNotMatch(xml, /edge="1"/); +}); + +test('buildDrawioXml produces XML-parseable output', () => { + const parsed = { + viewBox: [400, 300], + nodes: [ + { id: 'a', kind: 'backend', label: 'A&B', x: 10, y: 10, width: 80, height: 40 }, + ], + edges: [], + boundaries: [], + lifelines: [], + }; + const xml = buildDrawioXml(parsed, 'architecture'); + // The ampersand in the label must survive as a single & (not double-escaped). + assert.match(xml, /value="A&B"/); + assert.doesNotMatch(xml, /A&amp;/); +}); + +test('convertArchifyToDrawio chains parse + build', () => { + const svg = ` + + + + `; + const xml = convertArchifyToDrawio(svg, 'architecture'); + assert.match(xml, / { + const input = path.join(skillRoot, 'examples', file); + const output = path.join(tmp, `${type}.drawio`); + const result = run(['export-drawio', type, input, output]); + assert.equal(result.status, 0, result.stderr); + assert.ok(fs.existsSync(output), 'output file created'); + const xml = fs.readFileSync(output, 'utf8'); + assert.match(xml, / m[1]); + const edgeTargets = [...xml.matchAll(/target="(node-[^"]*)"/g)].map((m) => m[1]); + assert.ok(edgeSources.length > 0, `${type} has at least one bound edge`); + const vertexIds = new Set([...xml.matchAll(/id="(node-[^"]*)"[^>]*vertex="1"/g)].map((m) => m[1])); + for (const id of [...edgeSources, ...edgeTargets]) { + assert.ok(vertexIds.has(id), `${type}: edge endpoint ${id} resolves to a real vertex`); + } + }); +} + +test('cli: export-drawio architecture preserves waypoints for routed edges', () => { + const input = path.join(skillRoot, 'examples/web-app.architecture.json'); + const output = path.join(tmp, 'waypoints.drawio'); + const result = run(['export-drawio', 'architecture', input, output]); + assert.equal(result.status, 0, result.stderr); + const xml = fs.readFileSync(output, 'utf8'); + // The web-app example has multi-point routes (e.g. jwt-verification). + assert.match(xml, //); +}); + +test('cli: export-drawio --json emits structured receipt', () => { + const input = path.join(skillRoot, 'examples/web-app.architecture.json'); + const output = path.join(tmp, 'json-receipt.drawio'); + const result = run(['export-drawio', 'architecture', input, output, '--json']); + assert.equal(result.status, 0, result.stderr); + const receipt = JSON.parse(result.stdout); + assert.equal(receipt.ok, true); + assert.equal(receipt.command, 'export-drawio'); + assert.equal(receipt.type, 'architecture'); +}); + +test('cli: export-drawio rejects unknown type with exit 2', () => { + const input = path.join(skillRoot, 'examples/web-app.architecture.json'); + const result = run(['export-drawio', 'bogus', input]); + assert.equal(result.status, 2); + assert.match(result.stderr, /Unknown diagram type/); +}); + +test('cli: export-drawio rejects unknown option with exit 2', () => { + const input = path.join(skillRoot, 'examples/web-app.architecture.json'); + const result = run(['export-drawio', 'architecture', input, '--bogus']); + assert.equal(result.status, 2); +}); + +test('cli: help lists export-drawio', () => { + const result = run(['--help']); + assert.equal(result.status, 0); + assert.match(result.stdout, /archify export-drawio /); +}); + +// ─── Strict mode: 1:1 shape/color/corner fidelity ────────────────────────── + +test('extractPalette resolves CSS vars, class rules, and blends translucent fills (dark fallback)', () => { + // No light block: custom templates keep working through the dark fallback. + const css = ` + :root, [data-theme="dark"] { + --bg: #020617; + --mask: #0f172a; + --text: #ffffff; + --backend-fill: rgba(6, 78, 59, 0.4); + /* a comment that must not break parsing */ + --backend-stroke: #34d399; + } + .c-backend { fill: var(--backend-fill); stroke: var(--backend-stroke); } + .c-region { fill: rgba(251, 191, 36, 0.05); stroke: #fbbf24; stroke-dasharray: 8,4; } + `; + const palette = extractPalette(css); + assert.equal(palette.bg, '#020617'); + assert.equal(palette.mask, '#0f172a'); + assert.equal(palette.text, '#ffffff'); + // rgba(6,78,59,0.4) over mask #0f172a → exact composite. + const backend = palette.paletteFor('c-backend', 'mask'); + assert.equal(backend.fill, '#0b2d31'); + assert.equal(backend.stroke, '#34d399'); + const region = palette.paletteFor('c-region', 'bg'); + assert.equal(region.dash, '8,4'); + assert.ok(region.fill.startsWith('#')); +}); + +test('extractPalette always resolves the light theme over the dark default', () => { + const css = ` + :root, [data-theme="dark"] { + --bg: #020617; --mask: #0f172a; --text: #ffffff; --text-muted: #94a3b8; + --backend-fill: rgba(6, 78, 59, 0.4); --backend-stroke: #34d399; + } + [data-theme="light"] { + --bg: #f8fafc; --mask: #ffffff; --text: #0f172a; --text-muted: #64748b; + --backend-fill: rgba(52, 211, 153, 0.18); --backend-stroke: #059669; + } + [data-preset="signal-flow"][data-theme="light"] { --bg: #030711; } + .c-backend { fill: var(--backend-fill); stroke: var(--backend-stroke); } + `; + const palette = extractPalette(css); + // Light values win; the preset-scoped light block must not be picked up. + assert.equal(palette.bg, '#f8fafc'); + assert.equal(palette.mask, '#ffffff'); + assert.equal(palette.text, '#0f172a'); + assert.equal(palette.textMuted, '#64748b'); + // rgba(52,211,153,0.18) over the white light-theme mask → light composite. + const backend = palette.paletteFor('c-backend', 'mask'); + assert.equal(backend.fill, '#daf7ed'); + assert.equal(backend.stroke, '#059669'); +}); + +test('strict build keeps rounded rectangles, exact radii, and colors', () => { + const parsed = { + viewBox: [400, 300], + nodes: [{ id: 'a', kind: 'database', label: 'DB', x: 10, y: 10, width: 80, height: 40, rx: 6, strokeWidth: 1.5, fillClass: 'c-database' }], + edges: [{ + from: 'a', to: 'a', label: null, points: [[50, 30], [50, 30]], radius: 8, strokeWidth: 1.5, strokeClass: 'a-security', + }], + boundaries: [{ kind: 'region', label: 'R', x: 0, y: 0, width: 300, height: 200, rx: 12, strokeWidth: 1, fillClass: 'c-region', labelClass: 't-cloud' }], + lifelines: [], + edgeLabels: [], + }; + const palette = extractPalette(` + :root, [data-theme="dark"] { + --bg: #020617; --mask: #0f172a; --text: #ffffff; --text-muted: #94a3b8; + --database-fill: rgba(76, 29, 149, 0.4); --database-stroke: #a78bfa; + --cloud-stroke: #fbbf24; --security-stroke: #fb7185; + } + [data-theme="light"] { + --bg: #f8fafc; --mask: #ffffff; --text: #0f172a; --text-muted: #64748b; + --database-fill: rgba(167, 139, 250, 0.2); --database-stroke: #7c3aed; + --cloud-stroke: #d97706; --security-stroke: #e11d48; + } + .c-database { fill: var(--database-fill); stroke: var(--database-stroke); } + .c-region { fill: rgba(251,191,36,0.05); stroke: var(--cloud-stroke); stroke-dasharray: 8,4; } + .t-cloud { fill: var(--cloud-stroke); } + .a-security { stroke: var(--security-stroke); stroke-dasharray: 5,5; } + `); + const xml = buildDrawioXmlStrict(parsed, palette, 'architecture'); + // No draw.io built-in shape substitutions — plain rounded rects. + assert.doesNotMatch(xml, /shape=cylinder|shape=cloud|shape=shield/); + // Exact corner radius: rx=6 → absoluteArcSize=1;arcSize=6. + assert.match(xml, /absoluteArcSize=1;arcSize=6;/); + // Boundary keeps rx=12 and its dashed pattern from CSS. + assert.match(xml, /arcSize=12;/); + assert.match(xml, /dashPattern=8 4/); + // Edge corner radius: measured 8 → arcSize=16. + assert.match(xml, /arcSize=16/); + // Edge keeps the security variant color and dash. + assert.match(xml, /strokeColor=#e11d48/); + assert.match(xml, /dashPattern=5 5/); + // Composited database fill over the light-theme white mask. + assert.match(xml, /fillColor=#ede8fe/); + // Always-light export: light page background and dark ink. + assert.match(xml, /background="#f8fafc"/); + assert.match(xml, /fontColor=#0f172a/); +}); + +test('cli: export-drawio --strict emits 1:1 styles for all five types', () => { + for (const [type, file] of EXAMPLES) { + const input = path.join(skillRoot, 'examples', file); + const output = path.join(tmp, `${type}-strict.drawio`); + const result = run(['export-drawio', type, input, output, '--strict']); + assert.equal(result.status, 0, result.stderr); + const xml = fs.readFileSync(output, 'utf8'); + // Always-light palette: light page background and dark ink, exact radii, + // no built-in shape mapping. + assert.match(xml, /background="#f8fafc"/, type); + assert.match(xml, /fontColor=#0f172a/, type); + assert.doesNotMatch(xml, /background="#020617"/, type); + assert.match(xml, /absoluteArcSize=1/, type); + assert.doesNotMatch(xml, /shape=cylinder3|shape=cloud|shape=shield/, type); + // Real edge bindings survive strict mode. + const sources = [...xml.matchAll(/source="(node-[^"]*)"/g)].length; + assert.ok(sources > 0, `${type} keeps bound edges`); + } +}); + +test('cli: export-drawio --strict receipt reports strict mode', () => { + const input = path.join(skillRoot, 'examples/web-app.architecture.json'); + const output = path.join(tmp, 'strict-receipt.drawio'); + const result = run(['export-drawio', 'architecture', input, output, '--strict', '--json']); + assert.equal(result.status, 0, result.stderr); + const receipt = JSON.parse(result.stdout); + assert.equal(receipt.ok, true); + assert.equal(receipt.strict, true); +}); + +// ─── Sequence: timeline semantics via UML lifelines ──────────────────────── + +test('parseArchifySvg enriches sequence artifacts (labels, activations, segments, dash)', () => { + const svg = ` + + + open page + + + + + + + + Request + + + + + `; + const parsed = parseArchifySvg(svg, 'sequence'); + assert.equal(parsed.edges.length, 2); + // Message label comes from the outer focus group, not the path. + assert.equal(parsed.edges[0].label, 'open page'); + // Return-message dash lives as an inline path attribute. + assert.equal(parsed.edges[1].inlineDash, '3,5'); + // One activation (the opaque c-mask twin is skipped), with identity. + assert.equal(parsed.activations.length, 1); + assert.equal(parsed.activations[0].participant, 'web'); + assert.equal(parsed.activations[0].fillClass, 'c-frontend'); + assert.equal(parsed.activations[0].height, 448); + // Segment label matched back onto the boundary by segment id. + assert.equal(parsed.boundaries.length, 1); + assert.equal(parsed.boundaries[0].label, 'Request'); +}); + +test('sequence build maps the timeline onto UML lifelines', () => { + const parsed = { + viewBox: [820, 760], + nodes: [ + { id: 'user', kind: 'external', label: 'user', x: 19, y: 72, width: 86, height: 54, rx: 6, strokeWidth: 1.5, fillClass: 'c-external' }, + { id: 'web', kind: 'frontend', label: 'web', x: 127, y: 72, width: 86, height: 54, rx: 6, strokeWidth: 1.5, fillClass: 'c-frontend' }, + ], + edges: [ + { from: 'user', to: 'web', label: 'open page', id: 'open-page', points: [[69, 185], [163, 185]], strokeClass: 'a-default', strokeWidth: 1.4, radius: 0 }, + { from: 'web', to: 'user', label: 'render', id: 'render', points: [[163, 625], [69, 625]], strokeClass: 'a-default', strokeWidth: 1.4, radius: 0, inlineDash: '3,5' }, + ], + boundaries: [{ kind: 'segment', frameId: '0', label: 'Request', x: 48, y: 150, width: 724, height: 145, rx: 10, strokeWidth: 1, fillClass: 'c-lane' }], + lifelines: [{ x: 62, y1: 142, y2: 695 }, { x: 170, y1: 142, y2: 695 }], + activations: [{ participant: 'web', fillClass: 'c-frontend', rx: 3, strokeWidth: 1, x: 165, y: 220, width: 10, height: 448 }], + edgeLabels: [], + }; + const palette = extractPalette(` + [data-theme="light"] { + --bg: #f8fafc; --mask: #ffffff; --text: #0f172a; --text-muted: #64748b; + --frontend-fill: rgba(34, 211, 238, 0.15); --frontend-stroke: #0891b2; + --external-fill: rgba(148, 163, 184, 0.18); --external-stroke: #64748b; + } + .c-frontend { fill: var(--frontend-fill); stroke: var(--frontend-stroke); } + .c-external { fill: var(--external-fill); stroke: var(--external-stroke); } + .c-lane { fill: rgba(248, 250, 252, 0.65); stroke: #cbd5e1; stroke-dasharray: 6,6; } + .a-default { stroke: #94a3b8; } + `); + const xml = buildDrawioXmlSequence(parsed, palette, 'sequence', { strict: true }); + // Participants become full-timeline UML lifelines (foot 695 - top 72 = 623) + // with the header height preserved via size. + assert.match(xml, /shape=umlLifeline;perimeter=lifelinePerimeter/); + assert.match(xml, /size=54/); + assert.match(xml, /height="623"/); + // The authored message y survives as an exit fraction: (185-72)/623. + assert.match(xml, /exitX=0\.5;exitY=0\.181/); + assert.match(xml, /value="open page"/); + // Return message keeps its inline dash. + assert.match(xml, /dashPattern=3 5/); + // Activation is a kind-colored child of its lifeline. + assert.match(xml, /id="activation-0"[^>]*parent="node-web"/); + assert.match(xml, /fillColor=#def8fc/); + // Segment band keeps its authored label and lane dash. + assert.match(xml, /id="boundary-0" value="Request"/); + assert.match(xml, /dashPattern=6 6/); + // No floating lifeline edges remain. + assert.doesNotMatch(xml, /endArrow=none/); + + // Non-strict keeps the same topology with draw.io's default palette. + const plain = buildDrawioXmlSequence(parsed, null, 'sequence', { strict: false }); + assert.match(plain, /shape=umlLifeline/); + assert.match(plain, /exitX=0\.5;exitY=0\.181/); + assert.doesNotMatch(plain, /background="/); +}); + +test('cli: export-drawio sequence --strict restores the timeline', () => { + const input = path.join(skillRoot, 'examples/cache-miss-request.sequence.json'); + const output = path.join(tmp, 'sequence-timeline.drawio'); + const result = run(['export-drawio', 'sequence', input, output, '--strict']); + assert.equal(result.status, 0, result.stderr); + const xml = fs.readFileSync(output, 'utf8'); + assert.match(xml, /shape=umlLifeline/); + assert.match(xml, /value="open page"/); + assert.match(xml, /exitX=0\.5;exitY=0\.181/); + assert.match(xml, /parent="node-web"/); + assert.match(xml, /value="Request"/); + assert.doesNotMatch(xml, /endArrow=none/); +}); + +// ─── Regressions from the draw.io export code review ──────────────────────── + +test('buildDrawioXml escapes quotes in edge labels (raw-input callers)', () => { + const parsed = { + viewBox: [400, 300], + nodes: [ + { id: 'a', kind: 'backend', label: 'A', x: 10, y: 10, width: 80, height: 40 }, + { id: 'b', kind: 'backend', label: 'B', x: 200, y: 10, width: 80, height: 40 }, + ], + edges: [{ from: 'a', to: 'b', label: 'say "hi" now', points: [[90, 30], [200, 30]] }], + boundaries: [], + lifelines: [], + }; + const xml = buildDrawioXml(parsed, 'architecture'); + // Attribute-safe: the quote survives as a single entity, never a raw ", + // so direct builder calls with un-escaped labels stay well-formed XML. + assert.match(xml, /value="say "hi" now"/); +}); + +test('wraps parenting survives a boundary skipped by the SVG parser', () => { + const parsed = { + viewBox: [400, 300], + nodes: [{ id: 'kept', kind: 'backend', label: 'Kept', x: 50, y: 50, width: 60, height: 30 }], + edges: [], + // Only the SECOND JSON boundary rendered; it kept its authored frame-id, + // so the parsed list is shorter than diagram.boundaries. + boundaries: [{ kind: 'region', label: 'Inner', frameId: '1', x: 40, y: 40, width: 200, height: 120 }], + lifelines: [], + }; + const diagram = { boundaries: [{ wraps: ['ghost'] }, { wraps: ['kept'] }] }; + const xml = buildDrawioXml(parsed, 'architecture', diagram); + // The surviving wrap resolves to the REAL parsed cell (boundary-0) instead + // of a positional boundary-1 that was never emitted. + assert.match(xml, /id="node-kept"[^>]*parent="boundary-0"/); + assert.doesNotMatch(xml, /parent="boundary-1"/); +}); + +test('sequence builder tolerates an empty participant set (no NaN geometry)', () => { + const parsed = { viewBox: [820, 760], nodes: [], edges: [], boundaries: [], lifelines: [], activations: [], edgeLabels: [] }; + const palette = extractPalette('[data-theme="light"] { --bg: #f8fafc; --mask: #ffffff; --text: #0f172a; --text-muted: #64748b; }'); + const xml = buildDrawioXmlSequence(parsed, palette, null, { strict: true }); + assert.match(xml, / { + const input = path.join(tmp, 'naming.json'); + fs.copyFileSync(path.join(skillRoot, 'examples/web-app.architecture.json'), input); + const result = run(['export-drawio', 'architecture', input, '--strict']); + assert.equal(result.status, 0, result.stderr); + assert.ok( + fs.existsSync(path.join(tmp, 'naming-strict.drawio')), + 'default strict name matches the viewer button (-strict.drawio)', + ); +}); + +// ─── Parity: viewer and Node converters stay in lockstep ──────────────────── + +// The viewer's inline converter (assets/template.html) and the Node converter +// (renderers/shared/svg-to-drawio.mjs) are hand-mirrored implementations of +// the non-strict export. Extract the viewer section from the live template +// and assert both produce byte-identical XML for a real artifact, so editing +// one side without the other fails here instead of drifting silently. +// (The strict and sequence paths need a live DOM for computed styles and so +// cannot run under Node; this guards the shared regex-parsing core.) +function viewerConverterFromTemplate() { + const template = fs.readFileSync(path.join(skillRoot, 'assets/template.html'), 'utf8'); + const start = template.indexOf('var DRAWIO_KIND_STYLE'); + const end = template.indexOf('// ---- draw.io strict export'); + assert.ok(start !== -1 && end !== -1 && start < end, 'template.html drawio converter section not found — anchor moved?'); + const documentShim = { documentElement: { getAttribute: () => 'architecture' } }; + return new Function('document', `"use strict";\n${template.slice(start, end)}\nreturn archifySvgToDrawio;`)(documentShim); +} + +test('viewer and Node non-strict converters emit identical XML', () => { + const htmlOut = path.join(tmp, 'parity.html'); + const rendered = run(['render', 'architecture', path.join(skillRoot, 'examples/web-app.architecture.json'), htmlOut]); + assert.equal(rendered.status, 0, rendered.stderr); + const svg = extractSvgFromHtml(fs.readFileSync(htmlOut, 'utf8')); + const nodeXml = convertArchifyToDrawio(svg, 'architecture'); + const viewerXml = viewerConverterFromTemplate()(svg); + assert.equal(viewerXml, nodeXml); +}); diff --git a/archify/test/preset-tryon.test.mjs b/archify/test/preset-tryon.test.mjs index 37c55dd..fef8694 100644 --- a/archify/test/preset-tryon.test.mjs +++ b/archify/test/preset-tryon.test.mjs @@ -68,7 +68,7 @@ test('style selection synchronizes page, picker, and canonical SVG without touch test('omitted visual preset opens as Classic and theme switching cannot change it', () => { const html = render('architecture'); const themeRuntime = html.match(/Archify\.theme = \(function \(\) \{[\s\S]*?\n \}\)\(\);/)?.[0] || ''; - assert.match(html, //); + assert.match(html, /]* data-preset="classic"/); assert.match(themeRuntime, /html\.setAttribute\('data-theme', theme\)/); assert.doesNotMatch(themeRuntime, /data-preset|Archify\.preset/); diff --git a/docs/assets/archify-live-proof.gif b/docs/assets/archify-live-proof.gif index 9be575e..07935e0 100644 Binary files a/docs/assets/archify-live-proof.gif and b/docs/assets/archify-live-proof.gif differ diff --git a/docs/assets/archify-live-proof.json b/docs/assets/archify-live-proof.json index f21a599..d4237e9 100644 --- a/docs/assets/archify-live-proof.json +++ b/docs/assets/archify-live-proof.json @@ -7,13 +7,13 @@ "fps": 10, "frameCount": 54, "durationSeconds": 5.4, - "bytes": 1570846, - "sha256": "7cda809adfbc3ed34eb95652f5a6658d8dc2f0a1fa8837ad39b2a0cf7e88c068", + "bytes": 1626604, + "sha256": "c6a59a9397eb3324555413686255dbf91d2b50c876e684dd2d009f9dea62a1bb", "scenes": [ { "id": "signal-flow", "artifact": "docs/gallery/artifacts/agent-tool-call.workflow.html", - "artifactSha256": "a7a3b2836f7f5795173db40527a9d2df1c53c99ac3a08049617ccef3072629c0", + "artifactSha256": "c6e63dda2a337bd81bf5adedaebeb0bbee81cc999eeed06205c92e8b761b2890", "view": "happy-path", "eyebrow": "WORKFLOW · SIGNAL FLOW", "title": "Agent Tool Call", @@ -22,7 +22,7 @@ { "id": "blueprint", "artifact": "docs/gallery/artifacts/production-deployment.architecture.html", - "artifactSha256": "c41965605d4b7157c8a080aa52cf7d1075d96bdcae3cbbd2f82a6974bc5160cc", + "artifactSha256": "300f90aae568b4fd4bd8e3113065a64333edc413e2969747a3ff7e93b09caee9", "view": "request-boundary", "eyebrow": "ARCHITECTURE · BLUEPRINT", "title": "Production Deployment", @@ -31,7 +31,7 @@ { "id": "classic", "artifact": "docs/gallery/artifacts/cache-miss.sequence.html", - "artifactSha256": "738d651b8c3f3ff8b5b5298834cbd8b7acda5f46a456c5be031647cbc6eb2216", + "artifactSha256": "ac0937105ee8a8e1f2dd48350f8a5514b7ba887f1a63b172f997638f9998b72b", "view": "cache-fallback", "eyebrow": "SEQUENCE · CLASSIC", "title": "Cache Miss Request", diff --git a/docs/gallery.html b/docs/gallery.html index 36cebed..64691bf 100644 --- a/docs/gallery.html +++ b/docs/gallery.html @@ -339,7 +339,7 @@

Artifact9/9 pass
CompositionSHOWCASE · PASS
Graph12N · 11E
-
SHA-256a7a3b2836f7f
+
SHA-256c6e63dda2a33
Play named chapter ↗ @@ -369,7 +369,7 @@

Artifact9/9 pass

CompositionSHOWCASE · PASS
Graph12N · 12E
-
SHA-256c41965605d4b
+
SHA-256300f90aae568
Play named chapter ↗ @@ -398,7 +398,7 @@

Artifact9/9 pass

CompositionSHOWCASE · PASS
Graph7N · 12E
-
SHA-256738d651b8c3f
+
SHA-256ac0937105ee8
Play named chapter ↗ @@ -427,7 +427,7 @@

Artifact9/9 pass

CompositionSHOWCASE · PASS
Graph10N · 10E
-
SHA-2569380260f7e27
+
SHA-256b96182887977
Play named chapter ↗ @@ -456,7 +456,7 @@

Artifact9/9 pass

CompositionSHOWCASE · PASS
Graph11N · 10E
-
SHA-2568897a1e52ef5
+
SHA-25636ffca62fe43
Play named chapter ↗ @@ -485,7 +485,7 @@

Artifact9/9 pass

CompositionSHOWCASE · PASS
Graph10N · 10E
-
SHA-2563c30dd770f18
+
SHA-2563f3912ab833f
Play named chapter ↗ @@ -514,7 +514,7 @@

Artifact9/9 pass

CompositionSHOWCASE · PASS
Graph7N · 14E
-
SHA-25648d171db0c78
+
SHA-256ffcde32b6b51
Play named chapter ↗ @@ -543,7 +543,7 @@

Artifact9/9 pass

CompositionSHOWCASE · PASS
Graph12N · 12E
-
SHA-256f4f58688101d
+
SHA-2568285443e5198
Play named chapter ↗ @@ -572,7 +572,7 @@

Artifact9/9 pass

CompositionSHOWCASE · PASS
Graph10N · 6E
-
SHA-2569ef047c4a029
+
SHA-256c3146f9cdda8
Play named chapter ↗ @@ -601,7 +601,7 @@

Artifact9/9 pass

CompositionSHOWCASE · PASS
Graph11N · 6E
-
SHA-2563f13e00c2523
+
SHA-256b544137df372
Play named chapter ↗ @@ -630,7 +630,7 @@

Artifact9/9 pass
CompositionSHOWCASE · PASS
Graph10N · 9E
-
SHA-256b1753910784a
+
SHA-2564972ff260888

Play named chapter ↗ @@ -716,8 +716,8 @@

- + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +

@@ -6596,7 +6603,7 @@

Terminal + Recovery

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6701,6 +6708,698 @@

Terminal + Recovery

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6718,6 +7417,30 @@

Terminal + Recovery

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/docs/gallery/artifacts/agent-tool-call.workflow.html b/docs/gallery/artifacts/agent-tool-call.workflow.html index 4732bcd..a0c7379 100644 --- a/docs/gallery/artifacts/agent-tool-call.workflow.html +++ b/docs/gallery/artifacts/agent-tool-call.workflow.html @@ -1,5 +1,5 @@ - + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -6644,7 +6651,7 @@

Why It Matters

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6749,6 +6756,698 @@

Why It Matters

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6766,6 +7465,30 @@

Why It Matters

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/docs/gallery/artifacts/async-job-roundtrip.sequence.html b/docs/gallery/artifacts/async-job-roundtrip.sequence.html index 3132ab3..a51ecdf 100644 --- a/docs/gallery/artifacts/async-job-roundtrip.sequence.html +++ b/docs/gallery/artifacts/async-job-roundtrip.sequence.html @@ -1,5 +1,5 @@ - + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -4925,20 +4932,20 @@

Async Job Roundtrip

- - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -6674,7 +6681,7 @@

Two Observation Paths

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6779,6 +6786,698 @@

Two Observation Paths

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6796,6 +7495,30 @@

Two Observation Paths

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/docs/gallery/artifacts/cache-miss.sequence.html b/docs/gallery/artifacts/cache-miss.sequence.html index 2582199..94f34d1 100644 --- a/docs/gallery/artifacts/cache-miss.sequence.html +++ b/docs/gallery/artifacts/cache-miss.sequence.html @@ -1,5 +1,5 @@ - + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -4925,18 +4932,18 @@

Cache Miss Request Sequence

- - - - - - - - - - - - + + + + + + + + + + + + @@ -6662,7 +6669,7 @@

Async Trace

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6767,6 +6774,698 @@

Async Trace

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6784,6 +7483,30 @@

Async Trace

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/docs/gallery/artifacts/deployment-release.lifecycle.html b/docs/gallery/artifacts/deployment-release.lifecycle.html index 4fb1135..7581de1 100644 --- a/docs/gallery/artifacts/deployment-release.lifecycle.html +++ b/docs/gallery/artifacts/deployment-release.lifecycle.html @@ -1,5 +1,5 @@ - + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -6608,7 +6615,7 @@

Explicit Endings

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6713,6 +6720,698 @@

Explicit Endings

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6730,6 +7429,30 @@

Explicit Endings

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/docs/gallery/artifacts/event-stream.dataflow.html b/docs/gallery/artifacts/event-stream.dataflow.html index d546949..c92ce46 100644 --- a/docs/gallery/artifacts/event-stream.dataflow.html +++ b/docs/gallery/artifacts/event-stream.dataflow.html @@ -1,5 +1,5 @@ - + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -6686,7 +6693,7 @@

Failure Ownership

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6791,6 +6798,698 @@

Failure Ownership

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6808,6 +7507,30 @@

Failure Ownership

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/docs/gallery/artifacts/incident-response.workflow.html b/docs/gallery/artifacts/incident-response.workflow.html index 613aaee..f598544 100644 --- a/docs/gallery/artifacts/incident-response.workflow.html +++ b/docs/gallery/artifacts/incident-response.workflow.html @@ -1,5 +1,5 @@ - + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -6613,7 +6620,7 @@

Communication Contract

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6718,6 +6725,698 @@

Communication Contract

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6735,6 +7434,30 @@

Communication Contract

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/docs/gallery/artifacts/product-analytics.dataflow.html b/docs/gallery/artifacts/product-analytics.dataflow.html index 9904584..1810bd2 100644 --- a/docs/gallery/artifacts/product-analytics.dataflow.html +++ b/docs/gallery/artifacts/product-analytics.dataflow.html @@ -1,5 +1,5 @@ - + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -6647,7 +6654,7 @@

Derived Consumers

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6752,6 +6759,698 @@

Derived Consumers

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6769,6 +7468,30 @@

Derived Consumers

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/docs/gallery/artifacts/production-deployment.architecture.html b/docs/gallery/artifacts/production-deployment.architecture.html index 2a3dda2..b1501bd 100644 --- a/docs/gallery/artifacts/production-deployment.architecture.html +++ b/docs/gallery/artifacts/production-deployment.architecture.html @@ -1,5 +1,5 @@ - + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -6661,7 +6668,7 @@

Operational Evidence

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6766,6 +6773,698 @@

Operational Evidence

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6783,6 +7482,30 @@

Operational Evidence

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/docs/gallery/artifacts/release-delivery.workflow.html b/docs/gallery/artifacts/release-delivery.workflow.html index 01c4ccc..ea50a4d 100644 --- a/docs/gallery/artifacts/release-delivery.workflow.html +++ b/docs/gallery/artifacts/release-delivery.workflow.html @@ -1,5 +1,5 @@ - + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -6639,7 +6646,7 @@

Release Evidence

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6744,6 +6751,698 @@

Release Evidence

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6761,6 +7460,30 @@

Release Evidence

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/docs/gallery/artifacts/web-app.architecture.html b/docs/gallery/artifacts/web-app.architecture.html index b454244..d3d6579 100644 --- a/docs/gallery/artifacts/web-app.architecture.html +++ b/docs/gallery/artifacts/web-app.architecture.html @@ -1,5 +1,5 @@ - + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -6602,7 +6609,7 @@

Security

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6707,6 +6714,698 @@

Security

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6724,6 +7423,30 @@

Security

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/docs/gallery/manifest.json b/docs/gallery/manifest.json index 366b54f..1ca1915 100644 --- a/docs/gallery/manifest.json +++ b/docs/gallery/manifest.json @@ -27,8 +27,8 @@ "engineeringProfile": null, "nodeCount": 12, "edgeCount": 11, - "artifactBytes": 644009, - "artifactSha256": "a7a3b2836f7f5795173db40527a9d2df1c53c99ac3a08049617ccef3072629c0", + "artifactBytes": 689138, + "artifactSha256": "c6e63dda2a337bd81bf5adedaebeb0bbee81cc999eeed06205c92e8b761b2890", "sourceBytes": 6367, "sourceSha256": "9c94c8c0b2104478b77befb4cc5e343d381acba6e66645ab17616fde20b60e05", "checks": [ @@ -125,8 +125,8 @@ "engineeringProfile": "deployment-ownership", "nodeCount": 12, "edgeCount": 12, - "artifactBytes": 646602, - "artifactSha256": "c41965605d4b7157c8a080aa52cf7d1075d96bdcae3cbbd2f82a6974bc5160cc", + "artifactBytes": 691735, + "artifactSha256": "300f90aae568b4fd4bd8e3113065a64333edc413e2969747a3ff7e93b09caee9", "sourceBytes": 5454, "sourceSha256": "738e9c7f22e4e6e88f5aafc087dc58409105927daa9a79aaf240c8393654494f", "checks": [ @@ -223,8 +223,8 @@ "engineeringProfile": null, "nodeCount": 7, "edgeCount": 12, - "artifactBytes": 640506, - "artifactSha256": "738d651b8c3f3ff8b5b5298834cbd8b7acda5f46a456c5be031647cbc6eb2216", + "artifactBytes": 686399, + "artifactSha256": "ac0937105ee8a8e1f2dd48350f8a5514b7ba887f1a63b172f997638f9998b72b", "sourceBytes": 4345, "sourceSha256": "4bbaa6ca3cd4036002fd5d0f3a332eb922cec69d09bac3d7925f95beb4308389", "checks": [ @@ -321,8 +321,8 @@ "engineeringProfile": null, "nodeCount": 10, "edgeCount": 10, - "artifactBytes": 642833, - "artifactSha256": "9380260f7e27c0051d62c0fa31629f27c1f75b9470709e14226665bc97174436", + "artifactBytes": 687962, + "artifactSha256": "b961828879772f67e54ebf45544dae271f6283ae994e1097aaa0a07b24dfc387", "sourceBytes": 5164, "sourceSha256": "e5c880417eac1623923c3dd5461e0c78acd762e1621a28bd53fc14a84cad6a91", "checks": [ @@ -419,8 +419,8 @@ "engineeringProfile": null, "nodeCount": 11, "edgeCount": 10, - "artifactBytes": 640146, - "artifactSha256": "8897a1e52ef56907790854d52abd15230b58f29319aac3ab0004938396cfb74a", + "artifactBytes": 685275, + "artifactSha256": "36ffca62fe43910bf93e92ca709daae5bf9b92c7b7328c9a75db109d785565bd", "sourceBytes": 5375, "sourceSha256": "5c760c82eee545dfb7da38a4e628059d7891af4cfbfc13c8a7009ea64f69aa1b", "checks": [ @@ -517,8 +517,8 @@ "engineeringProfile": null, "nodeCount": 10, "edgeCount": 10, - "artifactBytes": 644091, - "artifactSha256": "3c30dd770f181415c11ec0b0526d86991af2177fb790182ecb7bf47d0c798cd7", + "artifactBytes": 689220, + "artifactSha256": "3f3912ab833f561a6cf93b9d65cdb4027fee6d5fa78fd9c7fd31ace5630c2ad5", "sourceBytes": 5627, "sourceSha256": "6fa38a7ccedf96da592d60b392783b846188eaa3ec9136c6dc06ca020e43a110", "checks": [ @@ -615,8 +615,8 @@ "engineeringProfile": null, "nodeCount": 7, "edgeCount": 14, - "artifactBytes": 640916, - "artifactSha256": "48d171db0c78bd06a4c404d9a8d4d461d458445002acc1bb50ae518f9f416fb5", + "artifactBytes": 686957, + "artifactSha256": "ffcde32b6b5199d82f9c251735dea6cab81c26572661e4dff7c0a74a3db3de30", "sourceBytes": 4370, "sourceSha256": "545f20920c89b69c2bb1f15508d5543fddbeb15cffba61e102206e04bd5a5cd9", "checks": [ @@ -713,8 +713,8 @@ "engineeringProfile": null, "nodeCount": 12, "edgeCount": 12, - "artifactBytes": 648010, - "artifactSha256": "f4f58688101dc5fbcc918f2208daf99d44755876c41dc31454655296b218da20", + "artifactBytes": 693139, + "artifactSha256": "8285443e5198b8d21e42051ef43c9ab7ea3fdce04af24a4d382c9f603a8efcff", "sourceBytes": 5433, "sourceSha256": "a92d6597ddbe164f395da32a7d4b33cfae08edd7d9600a6fb45a2058e5bbb43e", "checks": [ @@ -811,8 +811,8 @@ "engineeringProfile": null, "nodeCount": 10, "edgeCount": 6, - "artifactBytes": 637726, - "artifactSha256": "9ef047c4a029fa4649d84a0c299552a5d02f31ac0e3a51b7187b6696f33ed718", + "artifactBytes": 682856, + "artifactSha256": "c3146f9cdda8df3ce50ec3c45b4e2a3042445a13604e27cf5551336cb896a3ee", "sourceBytes": 4417, "sourceSha256": "af939579dcafc8b4c49f09594591577994398aee57a57369d4c791dfc0d85a42", "checks": [ @@ -909,8 +909,8 @@ "engineeringProfile": null, "nodeCount": 11, "edgeCount": 6, - "artifactBytes": 638696, - "artifactSha256": "3f13e00c25236bfdb214e8c10659ac2fbc0cb8d5cc6d727ef19207ee4abfdde3", + "artifactBytes": 683826, + "artifactSha256": "b544137df372a2ffa8f660939c0ee7c39e2fe89140362bd619ab205a156b7217", "sourceBytes": 4274, "sourceSha256": "735c2c5819266e6ea2a71775b6eefce98b53030ce90cb721dc8e1d022ce5be71", "checks": [ @@ -1007,8 +1007,8 @@ "engineeringProfile": null, "nodeCount": 10, "edgeCount": 9, - "artifactBytes": 637933, - "artifactSha256": "b1753910784abc7e37952868051861c2c90d953aeb80342ebb59febdeec87612", + "artifactBytes": 683066, + "artifactSha256": "4972ff260888f6acfc714b9068af0b35b74088655cb74a6ac86d4c9a9f235962", "sourceBytes": 3793, "sourceSha256": "483350f5297df682aba4e4a0fa491307ce3d3abd725ae4df07fab177490752cf", "checks": [ diff --git a/examples/archify-repo-grid-strict.drawio b/examples/archify-repo-grid-strict.drawio new file mode 100644 index 0000000..6c69692 --- /dev/null +++ b/examples/archify-repo-grid-strict.drawio @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/archify-repo-grid.drawio b/examples/archify-repo-grid.drawio new file mode 100644 index 0000000..9f68c06 --- /dev/null +++ b/examples/archify-repo-grid.drawio @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/archify-repo-grid.html b/examples/archify-repo-grid.html index fcacfe4..c96d0ff 100644 --- a/examples/archify-repo-grid.html +++ b/examples/archify-repo-grid.html @@ -1,5 +1,5 @@ - + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -6550,7 +6557,7 @@

Grid mode

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6655,6 +6662,698 @@

Grid mode

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6672,6 +7371,30 @@

Grid mode

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/examples/archify-repo-strict.drawio b/examples/archify-repo-strict.drawio new file mode 100644 index 0000000..6279d3a --- /dev/null +++ b/examples/archify-repo-strict.drawio @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/archify-repo.drawio b/examples/archify-repo.drawio new file mode 100644 index 0000000..79ebfb8 --- /dev/null +++ b/examples/archify-repo.drawio @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/archify-repo.html b/examples/archify-repo.html index e53437e..78b2199 100644 --- a/examples/archify-repo.html +++ b/examples/archify-repo.html @@ -1,5 +1,5 @@ - + @@ -3424,6 +3424,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4825,6 +4830,8 @@
+ +
@@ -6612,7 +6619,7 @@

Quality gates

// WebP encoding (older Safari), so detect explicitly. function supports(format) { if (format === 'share-card') return true; - if (format === 'svg' || format === 'png') return true; + if (format === 'svg' || format === 'png' || format === 'drawio' || format === 'drawio-strict') return true; if (format === 'webm') return canRecordMotion(); var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp'; try { @@ -6717,6 +6724,698 @@

Quality gates

} }); + // ---- draw.io export ------------------------------------------------- + // Converts the canonical SVG into a draw.io mxGraphModel XML so the + // diagram can be opened and further edited in draw.io. Every edge is + // bound to its source/target vertex cell (dragging a box keeps its + // connections), and interior route waypoints are preserved. Mirrors + // the Node-side converter in renderers/shared/svg-to-drawio.mjs. + var DRAWIO_KIND_STYLE = { + frontend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + backend: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + database: 'shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;', + cloud: 'ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + security: 'shape=shield;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + messagebus: 'shape=queue;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_STATE_STYLE = { + start: 'ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;', + active: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;', + waiting: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;', + decision: 'rhombus;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;', + success: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;strokeWidth=2;', + failure: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;', + neutral: 'rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;', + external: 'shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;' + }; + var DRAWIO_FRAME_STYLE = { + region: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#9673a6;dashed=0;verticalAlign=top;fontStyle=1;', + 'security-group': 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#d79b00;dashed=1;verticalAlign=top;fontStyle=1;', + lane: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;', + 'exception-lane': 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b85450;dashed=1;verticalAlign=top;align=left;spacingLeft=8;', + stage: 'rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=center;fontStyle=1;', + group: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;verticalAlign=top;', + segment: 'rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#b0b0b0;verticalAlign=top;align=left;spacingLeft=8;' + }; + function drawioEscapeAttr(v) { + return String(v == null ? '' : v).replace(/[&"<>']/g, function (ch) { + return ch === '&' ? '&' : ch === '"' ? '"' : ch === '<' ? '<' : ch === '>' ? '>' : '''; + }); + } + function drawioAttr(tag, name) { + var m = tag.match(new RegExp('\\s' + name + '="([^"]*)"')); + return m ? m[1] : null; + } + function drawioParsePoints(value) { + if (!value) return []; + return value.split(';').filter(Boolean).map(function (pair) { + var c = pair.split(',').map(Number); + return [c[0], c[1]]; + }); + } + function drawioFindGroupClose(svg, openIndex) { + var depth = 1, i = openIndex; + while (depth > 0 && i < svg.length) { + var no = svg.indexOf('', i + 1); + if (nc === -1) return -1; + if (no !== -1 && no < nc) { depth += 1; i = no; } + else { depth -= 1; i = nc; } + } + return i; + } + // Parse the self-describing SVG into {viewBox, nodes, edges, boundaries, lifelines}. + function drawioParseSvg(svg) { + var vb = svg.match(new RegExp('<' + 'svg[^>]*\\bviewBox="0 0 (\\d+\\.?\\d*) (\\d+\\.?\\d*)"')); + var viewBox = vb ? [parseFloat(vb[1]), parseFloat(vb[2])] : [1000, 700]; + // Nodes: with first inner . + var nodes = []; + var nodeMatches = Array.from(svg.matchAll(/]*\bdata-node-id="([^"]*)"[^>]*>/g)); + nodeMatches.forEach(function (m) { + var open = m[0]; + var ci = drawioFindGroupClose(svg, m.index); + var inner = svg.slice(m.index + open.length, ci); + var rm = inner.match(/]*>/); + if (!rm) return; + var box = rm[0]; + nodes.push({ + id: m[1], + kind: drawioAttr(open, 'data-node-kind') || 'backend', + label: drawioAttr(open, 'data-node-label') || '', + sublabel: drawioAttr(open, 'data-node-sublabel'), + tag: drawioAttr(open, 'data-node-tag'), + x: parseFloat(drawioAttr(box, 'x') || '0'), + y: parseFloat(drawioAttr(box, 'y') || '0'), + width: parseFloat(drawioAttr(box, 'width') || '0'), + height: parseFloat(drawioAttr(box, 'height') || '0') + }); + }); + // Edges: with data-composition-points. + var edges = []; + var edgeMatches = Array.from(svg.matchAll(/]*\bdata-composition-points="([^"]*)"[^>]*>/g)); + edgeMatches.forEach(function (m) { + var tag = m[0]; + var points = drawioParsePoints(m[1]); + if (points.length < 2) return; + var from = drawioAttr(tag, 'data-edge-from') || drawioAttr(tag, 'data-composition-edge-from'); + var to = drawioAttr(tag, 'data-edge-to') || drawioAttr(tag, 'data-composition-edge-to'); + if (!from || !to) return; + edges.push({ + from: from, to: to, + label: drawioAttr(tag, 'data-edge-label') || drawioAttr(tag, 'data-composition-edge-label'), + id: drawioAttr(tag, 'data-edge-id') || drawioAttr(tag, 'data-composition-edge-id'), + points: points + }); + }); + // Boundaries: . + var boundaries = []; + var bMatches = Array.from(svg.matchAll(/]*\bdata-graph-role="structural-frame"[^>]*>/g)); + bMatches.forEach(function (m) { + var tag = m[0]; + var w = parseFloat(drawioAttr(tag, 'width') || '0'); + var h = parseFloat(drawioAttr(tag, 'height') || '0'); + if (w <= 0 || h <= 0) return; + var after = svg.slice(m.index + tag.length, m.index + tag.length + 300); + var tm = after.match(/]*>([^<]*)<\/text>/); + boundaries.push({ + kind: drawioAttr(tag, 'data-composition-frame-kind') || 'boundary', + label: tm ? tm[1].trim() : '', + x: parseFloat(drawioAttr(tag, 'x') || '0'), + y: parseFloat(drawioAttr(tag, 'y') || '0'), + width: w, height: h + }); + }); + // Sequence lifelines: vertical dashed paths. + var lifelines = []; + var llMatches = Array.from(svg.matchAll(/]*\bd="M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)"[^>]*stroke-dasharray/g)); + llMatches.forEach(function (m) { + lifelines.push({ x: parseFloat(m[1]), y1: parseFloat(m[2]), y2: parseFloat(m[3]) }); + }); + return { viewBox: viewBox, nodes: nodes, edges: edges, boundaries: boundaries, lifelines: lifelines }; + } + function drawioNodeStyle(node) { + // Lifecycle states carry their own type enum. + var diagramType = document.documentElement.getAttribute('data-diagram-type') || ''; + if (diagramType === 'lifecycle' && DRAWIO_STATE_STYLE[node.kind]) return DRAWIO_STATE_STYLE[node.kind]; + return DRAWIO_KIND_STYLE[node.kind] || DRAWIO_KIND_STYLE.backend; + } + function drawioBuildXml(parsed) { + var vw = parsed.viewBox[0], vh = parsed.viewBox[1]; + // Resolve container parents: smallest boundary containing node center. + // Returns parentId + the boundary's origin (draw.io child coords are + // RELATIVE to their parent, so we must subtract the parent origin). + var parents = {}; + var sorted = parsed.boundaries.map(function (b, i) { + return Object.assign({}, b, { drawioId: 'boundary-' + i }); + }).sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + parsed.nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { + parents[n.id] = { drawioId: b.drawioId, ox: b.x, oy: b.y }; break; + } + } + }); + var vertexIds = {}; + parsed.nodes.forEach(function (n) { vertexIds[n.id] = true; }); + var lines = []; + lines.push(''); + lines.push('<' + 'mxGraphModel dx="' + Math.round(vw) + '" dy="' + Math.round(vh) + '" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + Math.round(vw) + '" pageHeight="' + Math.round(vh) + '" math="0" shadow="0">'); + lines.push(' '); + lines.push(' '); + lines.push(' '); + // Boundaries first (containers). + parsed.boundaries.forEach(function (b, i) { + var style = DRAWIO_FRAME_STYLE[b.kind] || DRAWIO_FRAME_STYLE.region; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Nodes. + parsed.nodes.forEach(function (n) { + var style = drawioNodeStyle(n); + var pinfo = parents[n.id]; + var parentId = pinfo ? pinfo.drawioId : '1'; + var relX = n.x - (pinfo ? pinfo.ox : 0); + var relY = n.y - (pinfo ? pinfo.oy : 0); + var parts = [n.label, n.sublabel, n.tag].filter(Boolean); + var htmlVal = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Sequence lifelines. + parsed.lifelines.forEach(function (ll) { + var id = 'lifeline-' + Math.round(ll.x) + '-' + Math.round(ll.y1); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + lines.push(' '); + }); + // Edges: real source/target binding + fixed interior waypoints. + parsed.edges.forEach(function (edge, i) { + if (!vertexIds[edge.from] || !vertexIds[edge.to]) return; + var id = edge.id ? 'edge-' + edge.id : 'edge-' + i; + var value = edge.label ? drawioEscapeAttr(edge.label) : ''; + lines.push(' '); + lines.push(' '); + // Interior waypoints only (draw.io computes endpoints from vertices). + if (edge.points.length > 2) { + lines.push(' '); + for (var p = 1; p < edge.points.length - 1; p++) { + lines.push(' '); + } + lines.push(' '); + } + lines.push(' '); + lines.push(' '); + }); + lines.push('
'); + lines.push(''); + return lines.join('\n'); + } + function archifySvgToDrawio(svgString) { + return drawioBuildXml(drawioParseSvg(svgString)); + } + + // ---- draw.io strict export ----------------------------------------- + // 1:1 fidelity variant: reads the LIVE SVG computed styles (exact + // composited fill/stroke colors, same corner radii — arcSize = 2 × + // radius on edges — same dash patterns) but always force-resolves the + // artifact's LIGHT theme, so the exported file never depends on the + // reader's current theme or the browser preference behind it. + function drawioParseColor(v) { + var s = String(v || '').trim(); + var m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] }; + // Theme variables come back as their authored tokens (hex), while + // computed fill/stroke values arrive as rgb()/rgba(). + var h = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (h) { + var hex = h[1].length === 3 ? h[1].split('').map(function (c) { return c + c; }).join('') : h[1]; + return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), a: 1 }; + } + return null; + } + function drawioHex(c) { + var h = function (n) { var s = Math.round(Math.max(0, Math.min(255, n))).toString(16); return s.length < 2 ? '0' + s : s; }; + return '#' + h(c.r) + h(c.g) + h(c.b); + } + function drawioBlend(fg, base) { + return { r: fg.r * fg.a + base.r * (1 - fg.a), g: fg.g * fg.a + base.g * (1 - fg.a), b: fg.b * fg.a + base.b * (1 - fg.a), a: 1 }; + } + function drawioCssColor(v, base) { + var c = drawioParseColor(v); + if (!c) return null; + return drawioHex(c.a >= 1 ? c : drawioBlend(c, base)); + } + function drawioRadiusFromD(d) { + var m = String(d || '').match(/L\s*(-?[\d.]+)\s+(-?[\d.]+)\s+Q\s*(-?[\d.]+)\s+(-?[\d.]+)/); + return m ? Math.round(Math.hypot(+m[3] - +m[1], +m[4] - +m[2])) : 0; + } + // Run `fn` with the artifact forced to its light theme and restore the + // reader's theme afterwards. The swap and restore happen inside one + // synchronous task, so the page never repaints in between; it only + // affects how the style reads below resolve. + function withLightTheme(fn) { + var root = document.documentElement; + var prev = root.getAttribute('data-theme'); + root.setAttribute('data-theme', 'light'); + try { + return fn(); + } finally { + if (prev === null) root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', prev); + } + } + function drawioStrictPalette(svgEl) { + var cs = getComputedStyle(svgEl); + // Fallbacks are the light-theme values: the export is always light. + var mask = drawioParseColor(cs.getPropertyValue('--mask').trim()) || { r: 255, g: 255, b: 255, a: 1 }; + var bg = drawioParseColor(cs.getPropertyValue('--bg').trim()) || { r: 248, g: 250, b: 252, a: 1 }; + var text = drawioHex(drawioParseColor(cs.getPropertyValue('--text').trim()) || { r: 15, g: 23, b: 42, a: 1 }); + var muted = drawioHex(drawioParseColor(cs.getPropertyValue('--text-muted').trim()) || { r: 100, g: 116, b: 139, a: 1 }); + return { + mask: drawioHex(mask), maskC: mask, bg: drawioHex(bg), bgC: bg, text: text, textMuted: muted, + of: function (el, base) { + var s = getComputedStyle(el); + var b = base === 'mask' ? mask : bg; + var out = {}; + var f = drawioCssColor(s.fill, b); + if (f) out.fill = f; + var st = drawioCssColor(s.stroke, b); + if (st) out.stroke = st; + var dash = s.strokeDasharray; + if (dash && dash !== 'none') { + var parts = dash.split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + if (parts.length) out.dash = parts.join(' '); + } + return out; + } + }; + } + function buildStrictXmlLight() { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- nodes ----------------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + else if (fillRect && cls !== 'c-mask' && +r.getAttribute('width') > +fillRect.getAttribute('width')) fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + tag: g.getAttribute('data-node-tag'), + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- boundaries ------------------------------------------------------ + var boundaries = []; + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + boundaries.push({ + kind: r.getAttribute('data-composition-frame-kind') || 'boundary', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r, textEl: null + }); + }); + // Boundary labels: the first sibling following each frame rect. + boundaries.forEach(function (b) { + var t = b.el.nextElementSibling; + if (t && t.tagName.toLowerCase() === 'text') b.textEl = t; + }); + // -- edges ----------------------------------------------------------- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + edges.push({ + from: from, to: to, + label: p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'), + el: p, pts: pts, + sw: +p.getAttribute('stroke-width') || 1.5, + radius: drawioRadiusFromD(p.getAttribute('d')) + }); + }); + // -- edge labels (positioned in data-detail groups) ----------- + var edgeLabels = []; + svgEl.querySelectorAll('g[data-detail="context"][data-edge-from]').forEach(function (g) { + var t = g.querySelector('text'); + if (!t) return; + edgeLabels.push({ + from: g.getAttribute('data-edge-from'), to: g.getAttribute('data-edge-to'), + x: +t.getAttribute('x'), y: +t.getAttribute('y') - 4, el: t + }); + }); + // -- lifelines ------------------------------------------------------- + var lifelines = []; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (!m) return; + if (getComputedStyle(p).strokeDasharray === 'none') return; + lifelines.push({ x: +m[1], y1: +m[2], y2: +m[3], el: p, sw: +p.getAttribute('stroke-width') || 0.8 }); + }); + // -- helpers --------------------------------------------------------- + function parents() { + var sorted = boundaries.map(function (b, i) { return Object.assign({}, b, { id: 'boundary-' + i }); }) + .sort(function (a, b) { return a.width * a.height - b.width * b.height; }); + var map = {}; + nodes.forEach(function (n) { + var cx = n.x + n.width / 2, cy = n.y + n.height / 2; + for (var i = 0; i < sorted.length; i++) { + var b = sorted[i]; + if (cx >= b.x && cx <= b.x + b.width && cy >= b.y && cy <= b.y + b.height) { map[n.id] = b; break; } + } + }); + return map; + } + var pm = parents(); + function fractionAlong(pts, target) { + var total = 0, segs = []; + for (var i = 0; i < pts.length - 1; i++) { + var len = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]) || 1; + segs.push({ ax: pts[i][0], ay: pts[i][1], dx: pts[i + 1][0] - pts[i][0], dy: pts[i + 1][1] - pts[i][1], len: len, start: total }); + total += len; + } + var best = 1e30, result = 0.5; + segs.forEach(function (s) { + var t = Math.max(0, Math.min(1, ((target[0] - s.ax) * s.dx + (target[1] - s.ay) * s.dy) / (s.len * s.len))); + var dist = Math.hypot(target[0] - (s.ax + t * s.dx), target[1] - (s.ay + t * s.dy)); + if (dist < best) { best = dist; result = (s.start + t * s.len) / (total || 1); } + }); + return Math.max(0, Math.min(1, result)); + } + function borderFrac(pt, node) { + if (!node) return null; + var f = function (v) { return v < 0.04 ? 0 : v > 0.96 ? 1 : Math.round(v * 100) / 100; }; + return [f((pt[0] - node.x) / node.width), f((pt[1] - node.y) / node.height)]; + } + // -- emit ------------------------------------------------------------ + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="0" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0" background="' + P.bg + '">'); + L.push(' '); + L.push(' '); + L.push(' '); + boundaries.forEach(function (b, i) { + var c = P.of(b.el, 'bg'); + var tc = b.textEl ? P.of(b.textEl, 'bg') : null; + var left = b.kind === 'lane' || b.kind === 'exception-lane' || b.kind === 'segment'; + var style = ['rounded=' + (b.rx > 0 ? 1 : 0), b.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(b.rx) : '', + 'fillColor=' + (c.fill || 'none'), 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + b.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (tc && tc.fill ? tc.fill : P.textMuted), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', left ? 'align=left;spacingLeft=8' : ''].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = P.of(n.fillRect, 'mask'); + var p = pm[n.id]; + var relX = n.x - (p ? p.x : 0), relY = n.y - (p ? p.y : 0); + var html = n.label; + if (n.sublabel || n.tag) { + html = '' + n.label + '
' + + (n.sublabel ? '' + n.sublabel + '' : '') + + (n.sublabel && n.tag ? '
' : '') + + (n.tag ? '' + n.tag + '' : ''); + } + var style = ['rounded=' + (n.rx > 0 ? 1 : 0), n.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(n.rx) : '', + 'fillColor=' + (c.fill || '#d5e8d4'), 'strokeColor=' + (c.stroke || '#82b366'), 'strokeWidth=' + n.sw, + 'fontColor=' + P.text, 'fontSize=11', 'fontFamily=' + FONT, 'whiteSpace=wrap', 'html=1', 'verticalAlign=middle'] + .filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + lifelines.forEach(function (ll, i) { + var c = P.of(ll.el, 'bg'); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = P.of(e.el, 'bg'); + var lbl = edgeLabels.find(function (l) { return l.from === e.from && l.to === e.to; }); + var lc = lbl ? P.of(lbl.el, 'bg') : null; + var style = ['edgeStyle=none', e.radius > 0 ? 'rounded=1;arcSize=' + e.radius * 2 : 'rounded=0', + 'strokeColor=' + (c.stroke || '#666666'), 'strokeWidth=' + e.sw, + c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (lc && lc.fill ? lc.fill : P.textMuted), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + P.mask, 'html=1'].filter(Boolean).join(';'); + var exit = borderFrac(e.pts[0], src), entry = borderFrac(e.pts[e.pts.length - 1], tgt); + if (exit) style += ';exitX=' + exit[0] + ';exitY=' + exit[1] + ';exitDx=0;exitDy=0'; + if (entry) style += ';entryX=' + entry[0] + ';entryY=' + entry[1] + ';entryDx=0;entryDy=0'; + L.push(' '); + L.push(' '); + if (lbl && e.label) { + var t = fractionAlong(e.pts, [lbl.x, lbl.y]); + L.push(' '); + } + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + + // ---- draw.io sequence export ---------------------------------------- + // Sequence geometry is a timeline, not a node graph: participants are + // exported as draw.io UML lifelines spanning the full timeline so each + // message's authored y survives as an exit/entry fraction, activation + // bars become lifeline children, and segment bands keep their labels. + // Mirrors buildDrawioXmlSequence in renderers/shared/svg-to-drawio.mjs. + function drawioKindPair(kind) { + var style = DRAWIO_KIND_STYLE[kind] || DRAWIO_KIND_STYLE.backend; + var fill = style.match(/fillColor=(#[0-9a-f]{6})/i); + var stroke = style.match(/strokeColor=(#[0-9a-f]{6})/i); + return { fill: fill ? fill[1] : null, stroke: stroke ? stroke[1] : null }; + } + function drawioBuildSequenceXml(strict) { + var svgEl = document.querySelector('.diagram-container svg'); + if (!svgEl) throw new Error('No diagram SVG found.'); + var P = drawioStrictPalette(svgEl); + var FONT = 'JetBrains Mono,ui-monospace,Menlo,monospace'; + var vb = (svgEl.getAttribute('viewBox') || '0 0 1000 700').split(/\s+/); + var vw = Math.round(+vb[2]), vh = Math.round(+vb[3]); + // -- participants --------------------------------------------------- + var nodes = [], nodeById = {}; + svgEl.querySelectorAll('g[id^="node-"]').forEach(function (g) { + var fillRect = null; + g.querySelectorAll('rect').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!fillRect && cls.indexOf('c-') === 0 && cls !== 'c-mask') fillRect = r; + }); + var geom = fillRect || g.querySelector('rect'); + if (!geom) return; + var n = { + id: g.getAttribute('data-node-id'), + label: g.getAttribute('data-node-label') || '', + sublabel: g.getAttribute('data-node-sublabel'), + kind: g.getAttribute('data-node-kind') || 'backend', + x: +geom.x.baseVal.value, y: +geom.y.baseVal.value, + width: +geom.width.baseVal.value, height: +geom.height.baseVal.value, + rx: +(fillRect ? fillRect.getAttribute('rx') : 0) || 0, + sw: +(fillRect ? fillRect.getAttribute('stroke-width') : 1.5) || 1.5, + fillRect: fillRect || geom + }; + nodes.push(n); nodeById[n.id] = n; + }); + // -- timeline span: deepest dashed lifeline foot --------------------- + var footY = 0; + svgEl.querySelectorAll('path').forEach(function (p) { + if (p.hasAttribute('data-composition-points')) return; + var d = p.getAttribute('d') || ''; + var m = d.match(/^M\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+L\s*\1\s+(\d+\.?\d*)$/); + if (m && getComputedStyle(p).strokeDasharray !== 'none') footY = Math.max(footY, +m[3]); + }); + if (!footY) { + nodes.forEach(function (n) { footY = Math.max(footY, n.y + n.height + 60); }); + if (!footY) footY = 400; + } + // -- segment bands + labels (labels dodge messages, live elsewhere) -- + var segments = []; + var segLabels = {}; + svgEl.querySelectorAll('g[data-graph-role="segment-label"]').forEach(function (g) { + segLabels[g.getAttribute('data-segment-id')] = (g.textContent || '').trim(); + }); + svgEl.querySelectorAll('rect[data-graph-role="structural-frame"]').forEach(function (r) { + var sid = r.getAttribute('data-composition-frame-id'); + segments.push({ + label: sid != null && segLabels[sid] ? segLabels[sid] : '', + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- activation bars ------------------------------------------------- + var activations = []; + svgEl.querySelectorAll('rect[data-graph-role="activation"]').forEach(function (r) { + var cls = r.getAttribute('class') || ''; + if (!cls || cls === 'c-mask') return; + activations.push({ + participant: r.getAttribute('data-activation-participant'), + x: +r.x.baseVal.value, y: +r.y.baseVal.value, + width: +r.width.baseVal.value, height: +r.height.baseVal.value, + rx: +r.getAttribute('rx') || 0, sw: +r.getAttribute('stroke-width') || 1, + el: r + }); + }); + // -- messages: label on the focus group, dash via computed style ---- + var edges = []; + svgEl.querySelectorAll('path[data-composition-points]').forEach(function (p) { + var from = p.getAttribute('data-edge-from') || p.getAttribute('data-composition-edge-from'); + var to = p.getAttribute('data-edge-to') || p.getAttribute('data-composition-edge-to'); + if (!from || !to) return; + var pts = (p.getAttribute('data-composition-points') || '').split(';').filter(Boolean).map(function (pair) { + var c = pair.split(','); + return [+c[0], +c[1]]; + }); + if (pts.length < 2) return; + var label = p.getAttribute('data-edge-label') || p.getAttribute('data-composition-edge-label'); + var owner = p.parentElement; + if (!label && owner && owner.tagName.toLowerCase() === 'g') label = owner.getAttribute('data-edge-label'); + var dashParts = (getComputedStyle(p).strokeDasharray || '').split(/[\s,]+/).map(Number).filter(function (n) { return n > 0; }); + edges.push({ + from: from, to: to, id: p.getAttribute('data-edge-id') || p.getAttribute('data-composition-edge-id'), + label: label || '', pts: pts, el: p, + sw: +p.getAttribute('stroke-width') || 1.5, + dash: dashParts.length ? dashParts.join(' ') : '' + }); + }); + // -- emit ------------------------------------------------------------ + var colorsOf = function (el, base) { return strict ? P.of(el, base) : null; }; + var L = []; + L.push(''); + L.push('<' + 'mxGraphModel dx="' + vw + '" dy="' + vh + '" grid="' + (strict ? 0 : 1) + '" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="' + vw + '" pageHeight="' + vh + '" math="0" shadow="0"' + (strict ? ' background="' + P.bg + '"' : '') + '>'); + L.push(' '); + L.push(' '); + L.push(' '); + segments.forEach(function (s, i) { + var c = colorsOf(s.el, 'bg'); + var style = ['rounded=' + (s.rx > 0 ? 1 : 0), s.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(s.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : 'none'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#b0b0b0'), 'strokeWidth=' + s.sw, + c && c.dash ? 'dashed=1;dashPattern=' + c.dash : '', + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=9', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1', 'verticalAlign=top', 'align=left', 'spacingLeft=8'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + nodes.forEach(function (n) { + var c = colorsOf(n.fillRect, 'mask') || (strict ? null : drawioKindPair(n.kind)); + var parts = [n.label, n.sublabel].filter(Boolean); + var html = parts.length > 1 + ? '' + parts[0] + '
' + parts.slice(1).join(' · ') + '' + : n.label; + var style = ['shape=umlLifeline', 'perimeter=lifelinePerimeter', 'size=' + Math.round(n.height), + 'container=1', 'collapsible=0', 'recursiveResize=0', 'outlineConnect=0', + c && c.fill ? 'fillColor=' + c.fill : '', + c && c.stroke ? 'strokeColor=' + c.stroke : '', + 'strokeWidth=' + n.sw, + 'fontColor=' + (strict ? P.text : '#000000'), 'fontSize=11', 'fontFamily=' + FONT, + 'whiteSpace=wrap', 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + activations.forEach(function (a, i) { + var participant = nodeById[a.participant]; + if (!participant) return; + var c = colorsOf(a.el, 'mask') || (strict ? null : drawioKindPair(participant.kind)); + var style = ['rounded=' + (a.rx > 0 ? 1 : 0), a.rx > 0 ? 'absoluteArcSize=1;arcSize=' + Math.round(a.rx) : '', + 'fillColor=' + (c && c.fill ? c.fill : '#ffffff'), 'strokeColor=' + (c && c.stroke ? c.stroke : '#999999'), + 'strokeWidth=' + a.sw, 'html=1'].filter(Boolean).join(';') + ';'; + L.push(' '); + L.push(' '); + L.push(' '); + }); + edges.forEach(function (e, i) { + var src = nodeById[e.from], tgt = nodeById[e.to]; + if (!src || !tgt) return; + var c = colorsOf(e.el, 'bg'); + var frac = function (y, cell) { return Math.max(0, Math.min(1, (y - cell.y) / (footY - cell.y))).toFixed(3); }; + var exitY = frac(e.pts[0][1], src), entryY = frac(e.pts[e.pts.length - 1][1], tgt); + var style = ['edgeStyle=none', 'rounded=0', + 'strokeColor=' + (c && c.stroke ? c.stroke : '#666666'), 'strokeWidth=' + e.sw, + e.dash ? 'dashed=1;dashPattern=' + e.dash : '', + 'endArrow=block', 'endFill=1', 'endSize=' + Math.max(4, Math.round(10 * e.sw)), + 'fontColor=' + (strict ? P.textMuted : '#666666'), 'fontSize=8', 'fontFamily=' + FONT, + 'labelBackgroundColor=' + (strict ? P.mask : '#ffffff'), 'html=1'] + .filter(Boolean).join(';') + + ';exitX=0.5;exitY=' + exitY + ';exitDx=0;exitDy=0' + + ';entryX=0.5;entryY=' + entryY + ';entryDx=0;entryDy=0;'; + L.push(' '); + // Label geometry y=-1 lifts the label one label-height above the + // line, matching the Archify artifact. + L.push(' '); + if (e.pts.length > 2) { + L.push(' '); + for (var p = 1; p < e.pts.length - 1; p++) L.push(' '); + L.push(' '); + } + L.push(' '); + L.push(' '); + }); + L.push('
'); + L.push(''); + return L.join('\n'); + } + // Public entry: always a light-themed .drawio file, independent of the + // viewer's current theme (and the browser preference behind it). + // Sequence artifacts carry timeline geometry and route through the + // dedicated sequence builder instead. + function drawioBuildStrictXml() { + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + return withLightTheme(function () { + return isSequence ? drawioBuildSequenceXml(true) : buildStrictXmlLight(); + }); + } + function runExport(format) { var base = diagramFilename(); close(true); @@ -6734,6 +7433,30 @@

Quality gates

recordExportReceipt('svg', blob, d.canonicalStateClean); download(blob, base + '.svg'); }) + : format === 'drawio' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Sequence artifacts need live computed styles (timeline + // geometry), so they read the live SVG instead of the + // serialized string. + var isSequence = document.documentElement.getAttribute('data-diagram-type') === 'sequence'; + var xml = isSequence ? drawioBuildSequenceXml(false) : archifySvgToDrawio(d.svgString); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio', blob, d.canonicalStateClean); + download(blob, base + '.drawio'); + toast('Downloaded draw.io'); + }) + : format === 'drawio-strict' + ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) { + // Strict reads the live SVG but force-resolves the light + // theme, so the file never follows the reader's current + // theme; serializeSvg only supplies the canonical-state + // receipt. + var xml = drawioBuildStrictXml(); + var blob = new Blob([xml], { type: 'application/xml;charset=utf-8' }); + recordExportReceipt('drawio-strict', blob, d.canonicalStateClean); + download(blob, base + '-strict.drawio'); + toast('Downloaded draw.io (strict)'); + }) : format === 'webm' ? recordWebm().then(function (blob) { document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size)); diff --git a/examples/architecture-strict.drawio b/examples/architecture-strict.drawio new file mode 100644 index 0000000..10b9c6a --- /dev/null +++ b/examples/architecture-strict.drawio @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/architecture.drawio b/examples/architecture.drawio new file mode 100644 index 0000000..2daac24 --- /dev/null +++ b/examples/architecture.drawio @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/checkout-platform-delta.html b/examples/checkout-platform-delta.html index 7c9820a..1e0e75e 100644 --- a/examples/checkout-platform-delta.html +++ b/examples/checkout-platform-delta.html @@ -3387,6 +3387,11 @@ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); } + .toolbar .export-menu button[data-format="drawio"]::before, + .toolbar .export-menu button[data-format="drawio-strict"]::before { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='2.5' y='2.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Crect x='11.5' y='12.5' width='6' height='5' rx='1' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M8.5 5h6v7.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E"); + } .toolbar .export-menu button[data-format="share-card"], .toolbar .export-menu button[data-action="route-share-card"], .toolbar .export-menu button[data-action="reach-share-card"] { @@ -4709,7 +4714,7 @@
+ ADD− DEL~ MOD↔ MOVE