Skip to content

Instantly share code, notes, and snippets.

@bsorrentino
Last active August 23, 2024 22:17
Show Gist options
  • Save bsorrentino/ac30aace026483d9688117f596e9d265 to your computer and use it in GitHub Desktop.
Save bsorrentino/ac30aace026483d9688117f596e9d265 to your computer and use it in GitHub Desktop.
Mermaid Preview web component
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mermaid preview</title>
<style>
.h-screen {
height: 100vh;
}
</style>
<script type="module" src="./mermaid-preview.js"></script>
</head>
<body>
<mermaid-preview class="h-screen" id="preview1" theme="base">
---
title: ADAPTIVE RAG EXECUTOR
---
flowchart TD
start((start))
stop((stop))
web_search("web_search")
retrieve("retrieve")
grade_documents("grade_documents")
generate("generate")
transform_query("transform_query")
%% condition1{"check state"}
%% condition2{"check state"}
%% startcondition{"check state"}
%% start:::start --> startcondition:::startcondition
%% startcondition:::startcondition -->|web_search| web_search:::web_search
start:::start -->|web_search| web_search:::web_search
%% startcondition:::startcondition -->|vectorstore| retrieve:::retrieve
start:::start -->|vectorstore| retrieve:::retrieve
web_search:::web_search --> generate:::generate
retrieve:::retrieve --> grade_documents:::grade_documents
%% grade_documents:::grade_documents --> condition1:::condition1
%% condition1:::condition1 -->|transform_query| transform_query:::transform_query
grade_documents:::grade_documents -->|transform_query| transform_query:::transform_query
%% condition1:::condition1 -->|generate| generate:::generate
grade_documents:::grade_documents -->|generate| generate:::generate
transform_query:::transform_query --> retrieve:::retrieve
%% generate:::generate --> condition2:::condition2
%% condition2:::condition2 -->|not supported| generate:::generate
generate:::generate -->|not supported| generate:::generate
%% condition2:::condition2 -->|not useful| transform_query:::transform_query
generate:::generate -->|not useful| transform_query:::transform_query
%% condition2:::condition2 -->|useful| stop:::stop
generate:::generate -->|useful| stop:::stop
</mermaid-preview>
</body>
</html>
const d3moduleUrl = 'https://cdn.jsdelivr.net/npm/[email protected]/+esm'
const mermaidModuleUrl = 'https://cdn.jsdelivr.net/npm/[email protected]/+esm'
Promise.all([ import(mermaidModuleUrl), import(d3moduleUrl)]).then( ([mermaidModule, d3]) => {
const mermaid = mermaidModule.default
class MermaidPreview extends HTMLElement {
static get observedAttributes() {
return ['theme'];
}
attributeChangedCallback(name, oldValue, newValue) {
console.debug( 'attributeChangedCallback', name, oldValue, newValue )
if( name === 'theme' && oldValue !== newValue ) {
this.#init()
this.#renderDiagram()
}
}
get useMaxWidth() {
return this.getAttribute('useMaxWidth') === 'true'
}
constructor() {
super();
this._content = null
this._activeClass = null
this._lastTransform = null
const shadowRoot = this.attachShadow({ mode: "open" });
const style = document.createElement("style");
style.textContent = `
:host {
display: block;
width: 100%;
height: 100%;
}
.h-full {
height: 100%;
}
.w-full {
width: 100%;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.bg-neutral {
--tw-bg-opacity: 1;
background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
}
`
shadowRoot.appendChild(style);
const container = document.createElement('div')
container.classList.add("h-full");
container.classList.add("w-full");
container.classList.add("flex");
container.classList.add("items-center");
container.classList.add("justify-center");
container.classList.add("bg-neutral");
container.classList.add("mermaid");
const errorSlot = document.createElement('slot')
errorSlot.name = 'error'
container.appendChild(errorSlot)
shadowRoot.appendChild(container);
this.#renderDiagram()
}
/**
* @returns {ChildNode[]}
* @private
*/
get #textNodesContent() {
return Array.from(this.childNodes)
.filter(node => node.nodeType === this.TEXT_NODE)
.map(node => node.textContent?.trim())
.join('');
}
/**
* @returns {string}
* @private
*/
get #textContent() {
if (this._content) {
if (this._activeClass) {
return `
${this._content}
classDef ${this._activeClass} fill:#f96
`
}
return this._content
}
return this.#textNodesContent;
}
#updateSVGSize( svg, { right: width, bottom: height } ) {
if( this.useMaxWidth ) { // no update required
return svg
}
console.debug( `new svg size: width:${width} height:${height}`);
const maxWitdhRegex = /style="max-width:\s*[\d\.]+px;"/
if( maxWitdhRegex.test(svg) ) { // fix useMaxWidth === false doesn't work (to be investigated)
console.warn( 'useMaxWitdh is false, but found max-width in svg!' )
return svg.replace( maxWitdhRegex, `height="${height}" width="${width}" `)
}
return svg
.replace(/height="[\d\.]+"/, `height="${height}"`)
.replace(/width="[\d\.]+"/, `width="${width}"`)
;
}
async #renderDiagram() {
if( !this.#textContent ) return
// console.debug( this.#textContent )
const svgContainer = this.shadowRoot.querySelector('.mermaid')
return mermaid.render(`graph`, this.#textContent)
.then(res => svgContainer.innerHTML = this.#updateSVGSize(res.svg, svgContainer.getBoundingClientRect()) )
.then(() => this.#svgPanZoom())
.catch(e => {
console.error("RENDER ERROR", e)
const errorSlot = this.shadowRoot.querySelector('slot[name="error"]')
if( errorSlot ) {
const slotElements = errorSlot.assignedElements();
slotElements[0].textContent = e.message
}
})
}
#svgPanZoom() {
// console.debug( '_lastTransform', this._lastTransform )
const svgs = d3.select(this.shadowRoot).select(".mermaid svg");
// console.debug( 'svgs', svgs )
const self = this;
svgs.each(function () {
// 'this' refers to the current DOM element
const svg = d3.select(this);
// console.debug( 'svg', svg );
svg.html("<g>" + svg.html() + "</g>");
const inner = svg.select("g");
// console.debug( 'inner', inner )
const zoom = d3.zoom().on("zoom", event => {
inner.attr("transform", event.transform);
self._lastTransform = event.transform;
});
const selection = svg.call(zoom);
if (self._lastTransform !== null) {
inner.attr("transform", self._lastTransform)
// [D3.js Set initial zoom level](https://stackoverflow.com/a/46437252/521197)
selection.call(zoom.transform, self._lastTransform);
}
});
}
#onContent(e) {
const { detail: newContent } = e
this._content = newContent
this.#renderDiagram()
}
#onActive(e) {
const { detail: activeClass } = e
this._activeClass = activeClass;
this.#renderDiagram()
}
#init() {
console.debug( '#init', this.attributes.theme )
mermaid.initialize({
logLevel: 'none',
startOnLoad: false,
theme: this.getAttribute('theme') ?? 'dark',
useMaxWidth: this.useMaxWidth,
flowchart: { useMaxWidth: this.useMaxWidth },
sequence: { useMaxWidth: this.useMaxWidth },
gantt: { useMaxWidth: this.useMaxWidth },
journey: { useMaxWidth: this.useMaxWidth },
timeline: { useMaxWidth: this.useMaxWidth },
class: { useMaxWidth: this.useMaxWidth },
state: { useMaxWidth: this.useMaxWidth },
er: { useMaxWidth: this.useMaxWidth },
pie: { useMaxWidth: this.useMaxWidth },
quadrantChart: { useMaxWidth: this.useMaxWidth },
xyChart: { useMaxWidth: this.useMaxWidth },
requirement: { useMaxWidth: this.useMaxWidth },
mindmap: { useMaxWidth: this.useMaxWidth },
gitGraph: { useMaxWidth: this.useMaxWidth },
c4: { useMaxWidth: this.useMaxWidth },
sankey: { useMaxWidth: this.useMaxWidth },
block: { useMaxWidth: this.useMaxWidth },
});
}
#resizeHandler = () => this.#renderDiagram()
connectedCallback() {
this.addEventListener('graph', this.#onContent)
this.addEventListener('graph-active', this.#onActive)
window.addEventListener('resize', this.#resizeHandler)
}
disconnectedCallback() {
this.removeEventListener('graph', this.#onContent)
this.removeEventListener('graph-active', this.#onActive)
window.removeEventListener('resize', this.#resizeHandler)
}
}
if (!window.customElements.get('mermaid-preview')) {
window.customElements.define('mermaid-preview', MermaidPreview);
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment