pub const VIEWER_SCRIPT: &str = "// The diagram viewer: each inlined view sits in a frame at a scale a\n// reader can read, with zoom, pan and Fit, 1:1, Wide and Fullscreen\n// controls. Without this script a view is scaled to the text column (the\n// stylesheet\'s rule) and the controls stay hidden, so a page with no\n// JavaScript shows a diagram and no dead buttons.\n//\n// Scale: a view is shown at the scale that fits the frame\'s width, held\n// between MIN and 1, so a wide view scrolls inside its frame instead of\n// shrinking its text to nothing, and a small one is not blown up. Fit\n// shows the whole view, width and height, at whatever scale that takes;\n// 1:1 shows it at the size LikeC4 laid it out. Zooming (the wheel with\n// Ctrl or Cmd held, which is what a trackpad pinch sends in Chrome and\n// Firefox, and Safari\'s own gesture events for a pinch; the + and -\n// buttons; + - 0 with the frame focused) sets the scale freely, kept\n// about the pointer; a plain wheel scrolls the page as usual. The scale\n// is the view\'s CSS width, and panning is the frame\'s own scroll, by\n// its scrollbars or by dragging, so text stays crisp and the browser\n// keeps the position. Wide lets every figure on the site take the\n// window\'s width, remembered like the color scheme; Legend shows or hides\n// every figure\'s legend, remembered the same way; Fullscreen takes one\n// figure to the screen.\n(function () {\n var WIDE_KEY = \"asbuilt-docs-wide\";\n var LEGEND_KEY = \"asbuilt-docs-legend\";\n var MIN = 0.7;\n var MIN_ZOOM = 0.1;\n var MAX_ZOOM = 4;\n var STEP = 1.25;\n var WHEEL = 0.002; // scale change per pixel of wheel delta, exponential\n var DRAG = 4; // pixels of movement before a press becomes a drag\n var GUTTER = 16;\n var FRAME_SHARE = 0.75; // of the window\'s height; the stylesheet\'s 75vh\n var root = document.documentElement;\n // The zoom modifier\'s name for the hint: Cmd on Apple platforms, Ctrl\n // elsewhere. The script accepts either key everywhere.\n var MAC = /Mac|iPhone|iPad|iPod/.test(navigator.platform || \"\");\n var wide = false;\n // Each diagram\'s legend shows until a visitor hides it; the choice is\n // site-wide, like Wide.\n var legendShown = true;\n try {\n wide = window.localStorage.getItem(WIDE_KEY) === \"1\";\n legendShown = window.localStorage.getItem(LEGEND_KEY) !== \"0\";\n } catch (e) {}\n var figures = [];\n\n function viewBoxSize(svg) {\n var parts = (svg.getAttribute(\"viewBox\") || \"\").trim().split(/[\\s,]+/);\n var width = parseFloat(parts[2]);\n var height = parseFloat(parts[3]);\n return width > 0 && height > 0 ? { width: width, height: height } : null;\n }\n\n function layoutAll() {\n figures.forEach(function (f) {\n f.layout();\n });\n }\n\n function setup(figure) {\n var svg = figure.querySelector(\"svg.c4\");\n var frame = figure.querySelector(\".viewer-frame\");\n var bar = figure.querySelector(\".viewer-bar\");\n if (!svg || !frame || !bar) return;\n var natural = viewBoxSize(svg);\n if (!natural) return;\n var legend = figure.querySelector(\".legend\");\n var legendButton = bar.querySelector(\"[data-viewer-legend]\");\n if (legendButton && !legend) {\n legendButton.hidden = true;\n legendButton = null;\n }\n var mode = \"auto\"; // auto | fit | one | zoom\n var zoom = 1; // the scale in zoom mode\n var current = 1; // the scale last applied\n var pannable = false; // whether the view overflows its frame\n var readout = bar.querySelector(\".viewer-scale\");\n var full = bar.querySelector(\"[data-viewer-full]\");\n var hint = bar.querySelector(\".viewer-hint\");\n // No wheel to hint at on a touch-only device.\n if (hint && window.matchMedia && window.matchMedia(\"(hover: none)\").matches) hint = null;\n\n function fullscreen() {\n return document.fullscreenElement === figure;\n }\n function widthFit() {\n return frame.clientWidth / natural.width;\n }\n // The height the frame may take: the stylesheet\'s share of the\n // window, or in fullscreen the screen below the controls.\n function heightBudget() {\n if (fullscreen()) {\n return window.innerHeight - frame.getBoundingClientRect().top - GUTTER;\n }\n return FRAME_SHARE * window.innerHeight;\n }\n function scale() {\n if (mode === \"zoom\") return zoom;\n if (mode === \"one\") return 1;\n if (mode === \"fit\") return Math.min(widthFit(), heightBudget() / natural.height);\n return Math.min(1, Math.max(MIN, widthFit()));\n }\n function layout() {\n if (wide && !fullscreen()) {\n // The window\'s width, measured from the document (which leaves\n // out the scrollbar), from wherever the text column put us; the\n // root\'s own left accounts for any horizontal scroll.\n figure.style.marginLeft = \"0\";\n var left = figure.getBoundingClientRect().left - root.getBoundingClientRect().left;\n figure.style.marginLeft = GUTTER - left + \"px\";\n figure.style.width = root.clientWidth - 2 * GUTTER + \"px\";\n } else {\n figure.style.marginLeft = \"\";\n figure.style.width = \"\";\n }\n current = scale();\n svg.style.width = Math.round(natural.width * current) + \"px\";\n // A view that fits has nowhere to pan to: no grab cursor, no drag.\n pannable =\n frame.scrollWidth > frame.clientWidth + 1 || frame.scrollHeight > frame.clientHeight + 1;\n frame.classList.toggle(\"pannable\", pannable);\n if (readout) readout.textContent = Math.round(current * 100) + \"%\";\n if (hint) {\n hint.textContent =\n (MAC ? \"\\u2318\" : \"Ctrl\") + \" scroll to zoom\" + (pannable ? \" \\u00b7 drag to pan\" : \"\");\n }\n bar.querySelectorAll(\"[data-viewer-mode]\").forEach(function (button) {\n var pressed = button.getAttribute(\"data-viewer-mode\") === mode;\n button.setAttribute(\"aria-pressed\", String(pressed));\n });\n var wideButton = bar.querySelector(\"[data-viewer-wide]\");\n if (wideButton) wideButton.setAttribute(\"aria-pressed\", String(wide));\n if (legend) legend.hidden = !legendShown;\n if (legendButton) legendButton.setAttribute(\"aria-pressed\", String(legendShown));\n }\n // Scale to `next`, keeping the view point under the frame point\n // (`fx`, `fy`, from the frame\'s top left; its center when absent)\n // where it is.\n function zoomTo(next, fx, fy) {\n next = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, next));\n if (fx === undefined) {\n fx = frame.clientWidth / 2;\n fy = frame.clientHeight / 2;\n }\n var before = current;\n var x = frame.scrollLeft + fx;\n var y = frame.scrollTop + fy;\n mode = \"zoom\";\n zoom = next;\n layout();\n var ratio = current / before;\n frame.scrollLeft = x * ratio - fx;\n frame.scrollTop = y * ratio - fy;\n }\n\n bar.querySelectorAll(\"[data-viewer-mode]\").forEach(function (button) {\n button.addEventListener(\"click\", function () {\n var next = button.getAttribute(\"data-viewer-mode\");\n mode = mode === next ? \"auto\" : next;\n layout();\n });\n });\n bar.querySelectorAll(\"[data-viewer-zoom]\").forEach(function (button) {\n button.addEventListener(\"click\", function () {\n var out = button.getAttribute(\"data-viewer-zoom\") === \"out\";\n zoomTo(out ? current / STEP : current * STEP);\n });\n });\n if (legendButton) {\n legendButton.addEventListener(\"click\", function () {\n legendShown = !legendShown;\n try {\n window.localStorage.setItem(LEGEND_KEY, legendShown ? \"1\" : \"0\");\n } catch (e) {}\n layoutAll();\n });\n }\n var wideButton = bar.querySelector(\"[data-viewer-wide]\");\n if (wideButton) {\n wideButton.addEventListener(\"click\", function () {\n wide = !wide;\n try {\n window.localStorage.setItem(WIDE_KEY, wide ? \"1\" : \"0\");\n } catch (e) {}\n layoutAll();\n });\n }\n if (full) {\n if (figure.requestFullscreen) {\n full.addEventListener(\"click\", function () {\n if (fullscreen()) document.exitFullscreen();\n else figure.requestFullscreen();\n });\n } else {\n full.hidden = true;\n }\n }\n\n frame.addEventListener(\n \"wheel\",\n function (event) {\n if (!(event.ctrlKey || event.metaKey)) return;\n event.preventDefault();\n // Lines and pages to pixels: a line is about 16px, a page the frame.\n var unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? frame.clientHeight : 1;\n var delta = event.deltaY * unit;\n var rect = frame.getBoundingClientRect();\n zoomTo(current * Math.exp(-delta * WHEEL), event.clientX - rect.left, event.clientY - rect.top);\n },\n { passive: false }\n );\n // Safari reports a trackpad pinch as gesture events (its scale is\n // relative to the gesture\'s start), not as the wheel with Ctrl.\n var pinchBase = null;\n frame.addEventListener(\"gesturestart\", function (event) {\n event.preventDefault();\n pinchBase = current;\n });\n frame.addEventListener(\"gesturechange\", function (event) {\n event.preventDefault();\n if (pinchBase === null) return;\n var rect = frame.getBoundingClientRect();\n zoomTo(pinchBase * event.scale, event.clientX - rect.left, event.clientY - rect.top);\n });\n frame.addEventListener(\"gestureend\", function () {\n pinchBase = null;\n });\n frame.addEventListener(\"keydown\", function (event) {\n if (event.target !== frame || event.ctrlKey || event.metaKey || event.altKey) return;\n if (event.key === \"+\" || event.key === \"=\") zoomTo(current * STEP);\n else if (event.key === \"-\") zoomTo(current / STEP);\n else if (event.key === \"0\") {\n mode = \"auto\";\n layout();\n } else return;\n event.preventDefault();\n });\n\n // Drag to pan, with the mouse: touch keeps the browser\'s own\n // scrolling. A press that moves becomes a drag and is not a click,\n // so a node\'s link under the pointer is not followed on release.\n var press = null;\n var suppressClick = false;\n // A node is a link, which the browser would start dragging as one.\n frame.addEventListener(\"dragstart\", function (event) {\n event.preventDefault();\n });\n frame.addEventListener(\n \"click\",\n function (event) {\n if (!suppressClick) return;\n suppressClick = false;\n event.stopPropagation();\n event.preventDefault();\n },\n true\n );\n frame.addEventListener(\"pointerdown\", function (event) {\n suppressClick = false;\n if (!pannable || event.pointerType !== \"mouse\" || event.button !== 0) return;\n if (event.target.closest(\"button\")) return;\n press = {\n id: event.pointerId,\n x: event.clientX,\n y: event.clientY,\n left: frame.scrollLeft,\n top: frame.scrollTop,\n dragged: false,\n };\n });\n frame.addEventListener(\"pointermove\", function (event) {\n if (!press || event.pointerId !== press.id) return;\n // A release the frame never saw (outside it, or behind another\n // window) leaves the press armed: the button being up says so.\n if (!(event.buttons & 1)) {\n press = null;\n frame.classList.remove(\"dragging\");\n return;\n }\n var dx = event.clientX - press.x;\n var dy = event.clientY - press.y;\n if (!press.dragged && Math.abs(dx) < DRAG && Math.abs(dy) < DRAG) return;\n if (!press.dragged) {\n press.dragged = true;\n frame.classList.add(\"dragging\");\n frame.setPointerCapture(press.id);\n }\n frame.scrollLeft = press.left - dx;\n frame.scrollTop = press.top - dy;\n });\n function release(event) {\n if (!press || event.pointerId !== press.id) return;\n // The click that follows a drag\'s release is swallowed above; the\n // flag is cleared by the next press, so a cancelled drag with no\n // click leaves nothing armed.\n suppressClick = press.dragged;\n press = null;\n frame.classList.remove(\"dragging\");\n }\n frame.addEventListener(\"pointerup\", release);\n frame.addEventListener(\"pointercancel\", release);\n\n // An edge stands for the relations between what it joins, more than\n // its label can say; the page wrote them beside the view. A click on\n // such an edge opens them in a popover, each endpoint a link.\n var edges = null;\n var edgeData = figure.querySelector(\"script.viewer-edges\");\n if (edgeData) {\n try {\n edges = JSON.parse(edgeData.textContent);\n } catch (e) {}\n }\n var popover = null;\n function closePopover(refocus) {\n if (!popover || popover.hidden) return;\n popover.hidden = true;\n if (refocus) frame.focus();\n }\n // A link only for a web or file URL. asbuilt docs writes relative\n // paths in the edge data, but a host that copies this script may feed\n // it others; the browser\'s URL parser reads the scheme however it is\n // spelled, so a `javascript:` or `data:` link shows as text. The link\n // gets the resolved URL: the value checked is the value written.\n var LINKABLE = [\"http:\", \"https:\", \"file:\"];\n function linkable(href) {\n if (!href) return null;\n try {\n var url = new URL(href, document.baseURI);\n return LINKABLE.indexOf(url.protocol) >= 0 ? url.href : null;\n } catch (e) {\n return null;\n }\n }\n function endpoint(text, href) {\n var url = linkable(href);\n var el = document.createElement(url ? \"a\" : \"span\");\n if (url) el.href = url;\n el.textContent = text;\n return el;\n }\n function openPopover(key, relations, event) {\n if (!popover) {\n popover = document.createElement(\"div\");\n popover.className = \"viewer-popover\";\n popover.setAttribute(\"role\", \"dialog\");\n popover.setAttribute(\"aria-label\", \"Relations\");\n popover.hidden = true;\n var close = document.createElement(\"button\");\n close.type = \"button\";\n close.className = \"viewer-popover-close\";\n close.setAttribute(\"aria-label\", \"Close\");\n close.textContent = \"\\u00d7\";\n close.addEventListener(\"click\", function () {\n closePopover(true);\n });\n popover.appendChild(close);\n popover.appendChild(document.createElement(\"h3\"));\n popover.appendChild(document.createElement(\"ul\"));\n popover.addEventListener(\"keydown\", function (e) {\n if (e.key === \"Escape\") {\n e.preventDefault();\n closePopover(true);\n }\n });\n figure.appendChild(popover);\n }\n popover.querySelector(\"h3\").textContent = key.replace(\"->\", \" \\u2192 \");\n var list = popover.querySelector(\"ul\");\n list.textContent = \"\";\n relations.forEach(function (r) {\n var li = document.createElement(\"li\");\n li.appendChild(endpoint(r.source, r.source_href));\n li.appendChild(document.createTextNode(\" \\u2192 \"));\n li.appendChild(endpoint(r.target, r.target_href));\n var detail = document.createElement(\"span\");\n detail.className = \"rel-detail\";\n detail.textContent =\n \" \" +\n r.kind +\n (r.items.length ? \" \" + r.items.join(\", \") : \"\") +\n (r.technology ? \" (\" + r.technology + \")\" : \"\");\n li.appendChild(detail);\n list.appendChild(li);\n });\n popover.hidden = false;\n // Beside the click, inside the figure (which scrolls in fullscreen).\n var rect = figure.getBoundingClientRect();\n var left = event.clientX - rect.left + figure.scrollLeft + 8;\n var top = event.clientY - rect.top + figure.scrollTop + 8;\n left = Math.max(0, Math.min(left, figure.scrollLeft + figure.clientWidth - popover.offsetWidth - 8));\n top = Math.max(0, Math.min(top, figure.scrollTop + figure.clientHeight - popover.offsetHeight - 8));\n popover.style.left = left + \"px\";\n popover.style.top = top + \"px\";\n popover.querySelector(\".viewer-popover-close\").focus();\n }\n if (edges) {\n frame.querySelectorAll(\".edge[data-from]\").forEach(function (edge) {\n var key = edge.getAttribute(\"data-from\") + \"->\" + edge.getAttribute(\"data-to\");\n if (edges[key]) edge.classList.add(\"c4-explained\");\n });\n frame.addEventListener(\"click\", function (event) {\n var edge = event.target.closest(\".edge.c4-explained\");\n if (!edge) return;\n var key = edge.getAttribute(\"data-from\") + \"->\" + edge.getAttribute(\"data-to\");\n event.preventDefault();\n openPopover(key, edges[key], event);\n });\n document.addEventListener(\"click\", function (event) {\n if (!popover || popover.hidden || popover.contains(event.target)) return;\n // Another edge of this figure is re-opening it; any other click,\n // including an edge of another figure, closes it.\n if (frame.contains(event.target) && event.target.closest(\".edge.c4-explained\")) return;\n closePopover(false);\n });\n frame.addEventListener(\"keydown\", function (event) {\n if (event.key === \"Escape\") closePopover(true);\n });\n }\n\n document.addEventListener(\"fullscreenchange\", layout);\n // The frame\'s width changes with the column; the window\'s with Wide\n // (which sizes the figure itself, so the frame alone would not tell).\n if (window.ResizeObserver) new ResizeObserver(layout).observe(frame);\n\n figure.setAttribute(\"data-viewer-active\", \"\");\n frame.setAttribute(\"tabindex\", \"0\");\n bar.hidden = false;\n layout();\n figures.push({ layout: layout });\n }\n\n function setupAll(root) {\n if (root.matches && root.matches(\"figure[data-viewer]:not([data-viewer-active])\")) setup(root);\n if (root.querySelectorAll) {\n root.querySelectorAll(\"figure[data-viewer]:not([data-viewer-active])\").forEach(setup);\n }\n }\n\n document.addEventListener(\"DOMContentLoaded\", function () {\n setupAll(document);\n window.addEventListener(\"resize\", layoutAll);\n // A host that builds its page after load (a single-page app embedding\n // a figure) gets its figures set up as they arrive.\n if (window.MutationObserver) {\n new MutationObserver(function (records) {\n records.forEach(function (record) {\n record.addedNodes.forEach(function (node) {\n if (node.nodeType === 1) setupAll(node);\n });\n });\n }).observe(document.body, { childList: true, subtree: true });\n }\n });\n})();\n";Expand description
viewer.js: the diagram viewer, when viewer is on.