import { LitElement, html, css } from 'lit'
import { deflateRaw } from 'pako'
import { DarkMode } from '../shared/theme.js'
/**
* The draw.io that answers when the block names no server.
*
* Two hosts, because draw.io publishes two deployments of the same application: `viewer` is the one
* that opens a diagram read-only from a link, `embed` the one the editor talks to over postMessage
* (see `BlockContentDrawio.vue`). A self-hosted draw.io is one deployment doing both, which is why the
* `server` prop replaces both and defaults to neither.
*/
const PUBLIC_VIEWER = 'https://viewer.diagrams.net'
/** How many bytes are turned into characters at a time, below. */
const CHUNK_SIZE = 0x8000
/**
* A drawing as draw.io writes it into a link fragment.
*
* This is `Graph.compress` from draw.io itself, and every step of it matters to the other end:
* percent-encode, raw deflate (no zlib header, unlike Kroki's), then base64. The percent-encoding
* comes FIRST and is not a transport detail — draw.io decompresses and then `decodeURIComponent`s, so
* a diagram deflated without it comes back mangled at every non-ASCII character.
*
* `btoa` takes a string, and spreading a whole drawing into `String.fromCharCode` at once overflows
* the stack somewhere in the tens of thousands of bytes — hence a chunk at a time, as `block-kroki`
* does for the same reason.
*/
function compressForUrl(xml) {
const bytes = deflateRaw(encodeURIComponent(xml))
let binary = ''
for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK_SIZE))
}
return btoa(binary)
}
/**
* Block Draw.io
*/
export class BlockDrawioElement extends LitElement {
/**
* Metadata for the admin area and the editor's block picker. Collected at build time into
* `compiled/blocks.manifest.json`, which the server reads to register the block. Values must be
* plain literals. See `props` in `block-index` for what the picker does with that list.
*/
static definition = {
block: 'drawio',
name: 'Draw.io',
description: 'A diagram drawn in draw.io, stored as its own XML and edited on a canvas.',
icon: 'web-design',
/*
Fenced, and `xml` because that is what a draw.io document is — so an author reading the page
source gets it highlighted, and markdown keeps its hands off it. Without the fence a drawing is
a document full of `<` and `_` and lines beginning with spaces, every one of which means
something to markdown.
*/
template: `\`\`\`xml
` is what undoes the escaping the fence went through — the same three
* lines every source block in this wiki uses.
*/
_readSource() {
const fence = this.querySelector('pre')
this._source = ((fence ?? this).textContent ?? '').trim()
}
/**
* The link the frame opens.
*
* `lightbox=1` is draw.io's read-only viewer: no editing, no menus, just the drawing with zoom and
* a layers control. `edit=_blank` is deliberately absent — the pencil it adds opens the diagram in
* a copy of draw.io that has nowhere to save to, and the way to change a drawing here is the page.
*/
_url() {
const server = (this.server || '').trim().replace(/\/+$/, '') || PUBLIC_VIEWER
const params = new URLSearchParams({
lightbox: '1',
nav: '1',
ui: this._darkMode.isDark ? 'dark' : 'kennedy'
})
return `${server}/?${params.toString()}#R${encodeURIComponent(compressForUrl(this._source))}`
}
render() {
if (!this._source) {
return html`
This diagram is empty. Draw one with the Edit Content link above the block in the editor.
`
}
const height = Number(this.height) > 0 ? Number(this.height) : 420
return html`
${this.caption ? html`${this.caption}` : ''}
`
}
}
window.customElements.define('block-drawio', BlockDrawioElement)