UNPKG

bpmn-js-markdown-documentation-panel

Version:

A comprehensive documentation management plugin for Camunda Modeler with markdown support, element linking, and coverage tracking

1,484 lines (1,407 loc) 83.2 kB
import { marked } from "marked"; //#region ../../node_modules/.pnpm/bpmn-js@13.2.2/node_modules/bpmn-js/lib/util/ModelUtil.js /** * @typedef { import('../model/Types').Element } Element * @typedef { import('../model/Types').ModdleElement } ModdleElement */ /** * Is an element of the given BPMN type? * * @param {Element|ModdleElement} element * @param {string} type * * @return {boolean} */ function is(element, type) { var bo = getBusinessObject(element); return bo && typeof bo.$instanceOf === "function" && bo.$instanceOf(type); } /** * Return the business object for a given element. * * @param {Element|ModdleElement} element * * @return {ModdleElement} */ function getBusinessObject(element) { return element && element.businessObject || element; } //#endregion //#region src/extension/managers/AutocompleteManager.ts var AutocompleteManager = class { _callbacks; _selectedIndex = -1; constructor(options) { this._callbacks = options.callbacks; } setupAutocompleteEventListeners() { setTimeout(() => { const textarea = document.getElementById("doc-textarea"); if (!textarea) return; textarea.addEventListener("input", () => { this.handleAutocomplete(); }); textarea.addEventListener("keydown", (event) => { this._handleAutocompleteKeydown(event); }); document.addEventListener("click", (event) => { const dropdown = document.getElementById("autocomplete-dropdown"); const textarea$1 = document.getElementById("doc-textarea"); if (dropdown && !(event.target instanceof Node && dropdown.contains(event.target)) && event.target !== textarea$1) this.hideAutocomplete(); }); }, 100); } handleAutocomplete() { const textarea = document.getElementById("doc-textarea"); if (!textarea) return; const cursorPos = textarea.selectionStart; const text = textarea.value; let hashPos = -1; for (let i = cursorPos - 1; i >= 0; i--) { if (text[i] === "#") { hashPos = i; break; } if (text[i] === " " || text[i] === "\n" || text[i] === " ") break; } if (hashPos >= 0) { if (hashPos > 0 && text[hashPos - 1] === "(") { let foundClosingBracket = false; for (let i = hashPos - 2; i >= 0; i--) { if (text[i] === "]") { foundClosingBracket = true; break; } if (text[i] === "\n" || text[i] === "\r") break; } if (foundClosingBracket) { const searchText = text.substring(hashPos + 1, cursorPos); if (!searchText.includes(" ") && !searchText.includes("\n")) { this._showAutocomplete(searchText, hashPos); return; } } } } this.hideAutocomplete(); } hideAutocomplete() { const dropdown = document.getElementById("autocomplete-dropdown"); if (!dropdown) return; dropdown.classList.remove("visible"); this._selectedIndex = -1; } destroy() { this.hideAutocomplete(); } _showAutocomplete(searchText, hashPos) { const dropdown = document.getElementById("autocomplete-dropdown"); const autocompleteList = document.getElementById("autocomplete-list"); const textarea = document.getElementById("doc-textarea"); if (!dropdown || !autocompleteList || !textarea) return; const allElements = this._getAllElements(); const filteredElements = allElements.filter((element) => element.id.toLowerCase().includes(searchText.toLowerCase()) || element.name.toLowerCase().includes(searchText.toLowerCase())); if (filteredElements.length === 0) { this.hideAutocomplete(); return; } autocompleteList.innerHTML = ""; filteredElements.slice(0, 10).forEach((element) => { const item = document.createElement("div"); item.className = "autocomplete-item"; item.innerHTML = ` <div class="autocomplete-item-id">${element.id}</div> <div class="autocomplete-item-name">${element.name}</div> <div class="autocomplete-item-type">${element.type}</div> `; item.addEventListener("click", () => { this._selectAutocompleteItem(element.id, hashPos); }); autocompleteList.appendChild(item); }); this._positionAutocomplete(textarea, hashPos); dropdown.classList.add("visible"); this._selectedIndex = 0; this._updateAutocompleteSelection(Array.from(autocompleteList.children)); } _positionAutocomplete(textarea, hashPos) { const dropdown = document.getElementById("autocomplete-dropdown"); if (!dropdown) return; const textareaRect = textarea.getBoundingClientRect(); const style = window.getComputedStyle(textarea); const tempSpan = document.createElement("span"); tempSpan.style.visibility = "hidden"; tempSpan.style.position = "absolute"; tempSpan.style.top = "-9999px"; tempSpan.style.fontFamily = style.fontFamily; tempSpan.style.fontSize = style.fontSize; tempSpan.style.fontWeight = style.fontWeight; tempSpan.style.letterSpacing = style.letterSpacing; tempSpan.style.whiteSpace = "pre"; const textUpToHash = textarea.value.substring(0, hashPos); const linesUpToHash = textUpToHash.split("\n"); const currentLine = linesUpToHash[linesUpToHash.length - 1]; tempSpan.textContent = currentLine; this._callbacks.getCanvasContainer().appendChild(tempSpan); this._callbacks.getCanvasContainer().removeChild(tempSpan); const left = textareaRect.left + 10; const top = textareaRect.top + 100; dropdown.style.setProperty("left", `${left}px`, "important"); dropdown.style.setProperty("top", `${top}px`, "important"); dropdown.style.setProperty("position", "fixed", "important"); dropdown.style.setProperty("z-index", "10001", "important"); } _getAllElements() { const elements = []; const seenIds = /* @__PURE__ */ new Set(); const allElements = this._callbacks.getAllElements(); allElements.forEach((element) => { if (element.businessObject?.id) { const bo = element.businessObject; const elementId = bo.id; if (seenIds.has(elementId)) return; seenIds.add(elementId); elements.push({ id: elementId, name: bo.name || "Unnamed", type: this._callbacks.getElementTypeName(element) }); } }); return elements.sort((a, b) => a.id.localeCompare(b.id)); } _handleAutocompleteKeydown(event) { const dropdown = document.getElementById("autocomplete-dropdown"); if (!dropdown || !dropdown.classList.contains("visible")) return; const items = Array.from(dropdown.querySelectorAll(".autocomplete-item")); if (event.key === "ArrowDown") { event.preventDefault(); this._selectedIndex = Math.min(this._selectedIndex + 1, items.length - 1); this._updateAutocompleteSelection(items); } else if (event.key === "ArrowUp") { event.preventDefault(); this._selectedIndex = Math.max(this._selectedIndex - 1, 0); this._updateAutocompleteSelection(items); } else if (event.key === "Enter") { event.preventDefault(); if (this._selectedIndex >= 0 && items[this._selectedIndex]) { const selectedId = items[this._selectedIndex].querySelector(".autocomplete-item-id")?.textContent; const textarea = document.getElementById("doc-textarea"); if (!textarea) return; const cursorPos = textarea.selectionStart; const text = textarea.value; let hashPos = -1; for (let i = cursorPos - 1; i >= 0; i--) if (text[i] === "#") { hashPos = i; break; } if (hashPos >= 0 && selectedId) this._selectAutocompleteItem(selectedId, hashPos); } } else if (event.key === "Escape") { event.preventDefault(); this.hideAutocomplete(); } } _updateAutocompleteSelection(items) { const dropdown = document.getElementById("autocomplete-dropdown"); const autocompleteList = document.getElementById("autocomplete-list"); if (!dropdown || !autocompleteList) return; Array.from(items).forEach((item, index) => { if (index === this._selectedIndex) { item.classList.add("selected"); const itemTop = item.offsetTop; const itemBottom = itemTop + item.offsetHeight; const dropdownTop = dropdown.scrollTop; const dropdownBottom = dropdownTop + dropdown.clientHeight; if (itemTop < dropdownTop) dropdown.scrollTop = itemTop; else if (itemBottom > dropdownBottom) dropdown.scrollTop = itemBottom - dropdown.clientHeight; } else item.classList.remove("selected"); }); } _selectAutocompleteItem(elementId, hashPos) { const textarea = document.getElementById("doc-textarea"); if (!textarea) return; const text = textarea.value; const cursorPos = textarea.selectionStart; const beforeHash = text.substring(0, hashPos + 1); const afterCursor = text.substring(cursorPos); const newText = beforeHash + elementId + afterCursor; textarea.value = newText; const newCursorPos = hashPos + 1 + elementId.length; textarea.setSelectionRange(newCursorPos, newCursorPos); this.hideAutocomplete(); this._callbacks.updatePreview(); this._callbacks.saveDocumentationLive(); textarea.focus(); } }; //#endregion //#region src/extension/utils/MarkdownRenderer.ts /** * Enhanced markdown renderer with GitHub-style alerts and improved code block rendering */ var MarkdownRenderer = class MarkdownRenderer { renderer; constructor() { this.renderer = new marked.Renderer(); this.setupRenderer(); } setupRenderer() { this.renderer.blockquote = (quote) => { let quoteStr = ""; if (typeof quote === "string") quoteStr = quote; else if (quote && typeof quote === "object") if (quote.text) quoteStr = quote.text; else if (quote.tokens && Array.isArray(quote.tokens)) quoteStr = quote.tokens.map((token) => { if (typeof token === "string") return token; if (token?.text) return token.text; if (token?.raw) return token.raw; return ""; }).join(""); else quoteStr = quote ? `Unsupported quote format: ${JSON.stringify(quote)}` : ""; else quoteStr = quote ? String(quote) : ""; let alertMatch = quoteStr.match(/<p>\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]<\/p>\s*<p>([\s\S]*?)<\/p>/); if (!alertMatch) alertMatch = quoteStr.match(/<p>\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s+([\s\S]*?)<\/p>/); if (!alertMatch) alertMatch = quoteStr.match(/\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s+([\s\S]*?)$/); if (alertMatch) { const [, type, content] = alertMatch; const alertType = type.toLowerCase(); const icon = this.getAlertIcon(alertType); return ` <div class="markdown-alert markdown-alert-${alertType}"> <p class="markdown-alert-title"> ${icon} ${type} </p> <div class="markdown-alert-content">${content}</div> </div> `; } const alertMarkerMatch = quoteStr.match(/GITHUB_ALERT_(NOTE|TIP|IMPORTANT|WARNING|CAUTION):([\s\S]*)$/); if (alertMarkerMatch) { const [, type, content] = alertMarkerMatch; const alertType = type.toLowerCase(); const icon = this.getAlertIcon(alertType); const cleanContent = content.trim(); return ` <div class="markdown-alert markdown-alert-${alertType}"> <p class="markdown-alert-title"> ${icon} ${type} </p> ${cleanContent && cleanContent !== "No content" ? `<div class="markdown-alert-content">${this.escapeHtml(cleanContent)}</div>` : ""} </div> `; } return `<blockquote>${quoteStr}</blockquote>`; }; this.renderer.code = (code, language, _escaped, _meta) => { let codeStr = ""; if (typeof code === "string") codeStr = code; else if (code && typeof code === "object") if (code.text) codeStr = code.text; else if (code.raw) codeStr = code.raw; else codeStr = code ? `Unsupported code format: ${JSON.stringify(code)}` : ""; else codeStr = code ? String(code) : ""; const validLanguage = language || ""; let title = ""; let actualLanguage = validLanguage; const blockIdMatch = codeStr.match(/<!-- BLOCK_ID:([^>]+) -->/); if (blockIdMatch) { const blockId = blockIdMatch[1]; const blockData = MarkdownRenderer.codeBlockData.get(blockId); if (blockData) { title = blockData.title; actualLanguage = blockData.language; } codeStr = codeStr.replace(/<!-- BLOCK_ID:[^>]+ -->\s*/, ""); } const displayLanguage = actualLanguage || validLanguage || "text"; const escapedCode = this.escapeHtml(codeStr); const codeId = `code_${Math.random().toString(36).substr(2, 9)}`; return `<div class="markdown-code-block"> <div class="markdown-code-header"> <div class="markdown-code-info"> ${title ? `<span class="markdown-code-title">${this.escapeHtml(title)}</span>` : ""} <span class="markdown-code-language">${displayLanguage}</span> </div> <button class="markdown-code-copy" onclick="copyCodeBlock('${codeId}')" title="Copy code"> <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path> <path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path> </svg> </button> </div> <pre><code id="${codeId}" class="language-${displayLanguage}">${escapedCode}</code></pre> </div>`; }; this.renderer.codespan = (code) => { return `<code class="markdown-inline-code">${this.escapeHtml(code)}</code>`; }; } getAlertIcon(type) { const icons = { note: `<svg class="markdown-alert-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/> </svg>`, tip: `<svg class="markdown-alert-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"/> </svg>`, important: `<svg class="markdown-alert-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/> </svg>`, warning: `<svg class="markdown-alert-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/> </svg>`, caution: `<svg class="markdown-alert-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/> </svg>` }; return icons[type] || icons.note; } escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } /** * Store for code block titles and languages (static storage during rendering) */ static codeBlockData = /* @__PURE__ */ new Map(); /** * Renders markdown content with GitHub-style alerts and enhanced code blocks */ async render(markdown) { console.log("🚀 MARKDOWN RENDERER CALLED WITH:", JSON.stringify(markdown.substring(0, 100))); if (!markdown?.trim()) return "<em>No documentation.</em>"; MarkdownRenderer.codeBlockData.clear(); let processedMarkdown = markdown; processedMarkdown = this.preprocessIncompleteCodeBlocks(processedMarkdown); processedMarkdown = processedMarkdown.replace(/```([a-zA-Z0-9_+-]+):([^`\n]+)\n([\s\S]*?)```/g, (_match, language, title, code) => { const blockId = `cb_${Math.random().toString(36).substr(2, 9)}`; MarkdownRenderer.codeBlockData.set(blockId, { title: title.trim(), language }); return `\`\`\`${language}\n<!-- BLOCK_ID:${blockId} -->\n${code}\n\`\`\``; }); processedMarkdown = this.preprocessAlerts(processedMarkdown); try { const rendered = await Promise.resolve(marked(processedMarkdown, { renderer: this.renderer, gfm: true, breaks: true })); return typeof rendered === "string" ? rendered : ""; } catch (error) { console.error("Markdown rendering error:", error); return `<div class="markdown-error">Error rendering markdown: ${error instanceof Error ? error.message : "Unknown error"}</div>`; } } /** * Pre-processes incomplete code blocks - only renders if properly closed */ preprocessIncompleteCodeBlocks(markdown) { return markdown.replace(/```([a-zA-Z0-9_+-]*)\n([\s\S]+)$/m, (match, _language, content) => { if (!content.includes("```")) { console.log("FOUND INCOMPLETE CODE BLOCK - treating as regular text"); return content; } return match; }); } /** * Pre-processes alerts (alerts are handled in the blockquote renderer) */ preprocessAlerts(markdown) { return markdown; } }; //#endregion //#region src/extension/managers/ExportManager.ts var ExportManager = class { _elementRegistry; _markdownRenderer; _moddle; _canvas; constructor(elementRegistry, moddle, canvas) { this._elementRegistry = elementRegistry; this._moddle = moddle; this._canvas = canvas; this._markdownRenderer = new MarkdownRenderer(); } setupExportEventListeners() { setTimeout(() => { const exportBtn = document.getElementById("export-btn"); if (exportBtn) { exportBtn.replaceWith(exportBtn.cloneNode(true)); const newBtn = document.getElementById("export-btn"); newBtn?.addEventListener("click", () => { this.handleExport(); }); } }, 100); } handleExport() { this.exportDocumentation().catch((error) => { console.error("Export failed:", error); this._showNotification("Export failed", "error"); }); } /** * Export documentation in HTML format */ async exportDocumentation() { try { const processInfo = this._getProcessInfo(); const elements = this._getAllElementsWithDocumentation(); const documentedElements = elements.filter((el) => el.hasDocumentation); if (documentedElements.length === 0) { this._showNotification("No documented elements found to export", "warning"); return; } const htmlContent = await this._generateHTMLExport(elements, processInfo); const processName = processInfo.name || processInfo.id || "Process"; const filename = `${processName}_Documentation.html`; this._downloadFile(htmlContent, filename, "text/html"); this._showNotification(`Documentation exported successfully (${documentedElements.length} elements)`, "success"); } catch (error) { console.error("Export failed:", error); const errorMessage = error instanceof Error ? error.message : "Unknown error"; this._showNotification(`Export failed: ${errorMessage}`, "error"); } } /** * Get BPMN diagram as SVG */ async _getDiagramSVG() { try { const canvasContainer = this._canvas.getContainer(); const svgElement = canvasContainer.querySelector("svg"); if (svgElement) { const svgCopy = svgElement.cloneNode(true); const allElements = this._elementRegistry.getAll(); let minX = Number.POSITIVE_INFINITY; let minY = Number.POSITIVE_INFINITY; let maxX = Number.NEGATIVE_INFINITY; let maxY = Number.NEGATIVE_INFINITY; allElements.forEach((element) => { if (element.x !== void 0 && element.y !== void 0 && element.width !== void 0 && element.height !== void 0) { minX = Math.min(minX, element.x); minY = Math.min(minY, element.y); maxX = Math.max(maxX, element.x + element.width); maxY = Math.max(maxY, element.y + element.height); } }); const padding = 50; const diagramWidth = maxX - minX; const diagramHeight = maxY - minY; const viewBoxX = minX - padding; const viewBoxY = minY - padding; const viewBoxWidth = diagramWidth + padding * 2; const viewBoxHeight = diagramHeight + padding * 2; const viewBox = `${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}`; svgCopy.setAttribute("viewBox", viewBox); const aspectRatio = viewBoxWidth / viewBoxHeight; let svgWidth = 800; let svgHeight = 600; if (aspectRatio > svgWidth / svgHeight) svgHeight = svgWidth / aspectRatio; else svgWidth = svgHeight * aspectRatio; svgCopy.setAttribute("width", Math.round(svgWidth).toString()); svgCopy.setAttribute("height", Math.round(svgHeight).toString()); const background = document.createElementNS("http://www.w3.org/2000/svg", "rect"); background.setAttribute("x", viewBoxX.toString()); background.setAttribute("y", viewBoxY.toString()); background.setAttribute("width", viewBoxWidth.toString()); background.setAttribute("height", viewBoxHeight.toString()); background.setAttribute("fill", "#ffffff"); svgCopy.insertBefore(background, svgCopy.firstChild); return svgCopy.outerHTML; } return ""; } catch (error) { console.error("Error getting diagram SVG:", error); return ""; } } /** * Get all elements with their documentation status */ _getAllElementsWithDocumentation() { const elements = []; const seenIds = /* @__PURE__ */ new Set(); const allElements = this._elementRegistry.getAll(); allElements.forEach((element) => { if (element.businessObject?.id) { const bo = element.businessObject; const elementId = bo.id; if (seenIds.has(elementId)) return; seenIds.add(elementId); const documentation = this._getElementDocumentation(element); elements.push({ id: elementId, name: bo.name || "Unnamed", type: this._getElementTypeName(element), hasDocumentation: !!documentation?.trim(), documentation: documentation || "", element }); } }); return elements.sort((a, b) => a.id.localeCompare(b.id)); } /** * Get documentation for a specific element */ _getElementDocumentation(element) { if (!element || !element.businessObject) return ""; const bo = element.businessObject; if (bo.documentation && bo.documentation.length > 0) return bo.documentation[0].text || ""; return ""; } /** * Get element type name for display */ _getElementTypeName(element) { if (!element || !element.businessObject) return "Unknown"; const bo = element.businessObject; const type = bo.$type || ""; if (type.includes(":")) { const typeName = type.split(":")[1]; return typeName.replace(/([A-Z])/g, " $1").trim(); } return type || "Unknown"; } /** * Generate HTML export content */ async _generateHTMLExport(elements, processInfo) { const totalElements = elements.length; const documentedCount = elements.filter((el) => el.hasDocumentation).length; const undocumentedCount = totalElements - documentedCount; const coveragePercentage = totalElements > 0 ? Math.round(documentedCount / totalElements * 100) : 0; const processTitle = processInfo.name || processInfo.id || "BPMN Process"; const processDocumentation = processInfo.element ? this._getElementDocumentation(processInfo.element) : ""; const diagramSVG = await this._getDiagramSVG(); const tocItems = elements.map((el) => `<li><a href="#element-${el.id}" class="toc-link">${el.name} (${el.id})</a></li>`).join(""); const elementSections = await Promise.all(elements.map(async (el) => { const markdownContent = el.documentation || ""; let htmlContent = markdownContent ? await this._markdownRenderer.render(markdownContent) : "<p class='no-documentation'><em>No documentation available</em></p>"; htmlContent = this._fixElementLinksForExport(htmlContent, elements); return ` <div class="element-section" id="element-${el.id}"> <div class="element-header"> <div class="element-title-info"> <h2 class="element-title">${el.name}</h2> <div class="element-meta"> <span class="element-id">${el.id}</span> </div> </div> </div> <div class="element-content"> ${htmlContent} </div> </div> `; })).then((sections) => sections.join("")); return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${this._escapeHtml(processTitle)} - Documentation</title> <style> ${this._generateStyles()} </style> </head> <body> <div class="container"> <header class="header"> <h1 class="main-title">${this._escapeHtml(processTitle)}</h1> ${processDocumentation ? `<p class="subtitle">${this._escapeHtml(processDocumentation)}</p>` : ""} <div class="export-info"> <span class="export-date">Generated on ${(/* @__PURE__ */ new Date()).toLocaleDateString()}</span> </div> </header> <div class="stats-section"> <div class="stats-grid"> <div class="stat-card"> <div class="stat-number">${totalElements}</div> <div class="stat-label">Total Elements</div> </div> <div class="stat-card documented"> <div class="stat-number">${documentedCount}</div> <div class="stat-label">Documented</div> </div> <div class="stat-card undocumented"> <div class="stat-number">${undocumentedCount}</div> <div class="stat-label">Undocumented</div> </div> <div class="stat-card coverage"> <div class="stat-number">${coveragePercentage}%</div> <div class="stat-label">Coverage</div> </div> </div> </div> ${diagramSVG ? ` <div class="diagram-section"> <h2 class="section-title">Process Diagram</h2> <div class="diagram-container"> ${diagramSVG} </div> </div> ` : ""} <div class="toc-section"> <h2 class="section-title">Table of Contents</h2> <div class="toc-container"> <ul class="toc-list"> ${tocItems} </ul> </div> </div> <div class="documentation-section"> <h2 class="section-title">Element Documentation</h2> ${elementSections} </div> <div class="back-to-top"> <button onclick="window.scrollTo({top: 0, behavior: 'smooth'})" class="back-to-top-btn"> ↑ Back to Top </button> </div> </div> <script> // Add smooth scrolling for table of contents links document.querySelectorAll('.toc-link').forEach(link => { link.addEventListener('click', function(e) { e.preventDefault(); const target = document.querySelector(this.getAttribute('href')); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); }); // Show/hide back to top button window.addEventListener('scroll', function() { const backToTop = document.querySelector('.back-to-top'); if (window.scrollY > 300) { backToTop.style.display = 'block'; } else { backToTop.style.display = 'none'; } }); <\/script> </body> </html>`; } /** * Fix element links in HTML content for export by adding element- prefix */ _fixElementLinksForExport(htmlContent, elements) { const elementIds = new Set(elements.map((el) => el.id)); return htmlContent.replace(/href="#([^"]+)"/g, (match, elementId) => { if (elementIds.has(elementId)) return `href="#element-${elementId}"`; return match; }); } /** * Generate CSS styles for the HTML export */ _generateStyles() { return ` * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #262c33; background: #f8f9fa; min-height: 100vh; } .container { max-width: 1200px; margin: 20px auto; padding: 0; background: white; border: 1px solid #dae2ec; border-radius: 8px; overflow: hidden; } .header { background: white; color: #262c33; padding: 60px 40px; text-align: center; border-bottom: 1px solid #dae2ec; } .main-title { font-size: 2.5em; font-weight: 600; margin-bottom: 10px; } .subtitle { font-size: 1.1em; opacity: 0.9; margin-bottom: 20px; } .export-date { font-size: 0.9em; opacity: 0.8; background: #fafafa; padding: 4px 12px; border-radius: 4px; display: inline-block; border: 1px solid #dae2ec; } .stats-section { padding: 40px; background: #fafafa; border-bottom: 1px solid #dae2ec; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; } .stat-card { background: white; padding: 25px 20px; border-radius: 4px; text-align: center; border: 1px solid #dae2ec; } .stat-number { font-size: 2.2em; font-weight: 600; color: #262c33; display: block; margin-bottom: 5px; } .stat-label { color: #666; font-size: 0.9em; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; } .diagram-section { padding: 40px; background: white; border-bottom: 1px solid #dae2ec; } .section-title { font-size: 1.8em; color: #262c33; margin-bottom: 25px; display: inline-block; } .diagram-container { background: #fafafa; border-radius: 4px; padding: 20px; text-align: center; border: 1px solid #dae2ec; } .diagram-container svg { max-width: 100%; height: auto; border-radius: 4px; } .toc-section { padding: 40px; background: #fafafa; border-bottom: 1px solid #dae2ec; } .toc-container { background: white; border-radius: 4px; padding: 30px; border: 1px solid #dae2ec; } .toc-list { list-style: none; columns: 2; column-gap: 30px; column-fill: balance; } .toc-list li { break-inside: avoid; margin-bottom: 8px; position: relative; } .toc-link { color: #262c33; text-decoration: none; font-weight: 500; transition: color 0.3s ease; } .toc-link:hover { color: #666; } .documentation-section { padding: 40px; background: white; } .element-section { margin-bottom: 30px; background: white; border-radius: 4px; border: 1px solid #dae2ec; overflow: hidden; } .element-header { background: white; color: #262c33; padding: 20px 25px; display: flex; align-items: center; gap: 15px; border-bottom: 1px solid #dae2ec; } .element-title-info { flex: 1; } .element-title { font-size: 1.3em; font-weight: 600; margin-bottom: 8px; } .element-meta { display: flex; gap: 15px; align-items: center; } .element-id { font-size: 0.9em; opacity: 0.8; font-family: 'Monaco', 'Consolas', monospace; } .element-content { padding: 25px; background: white; } .element-content h1, .element-content h2, .element-content h3, .element-content h4, .element-content h5, .element-content h6 { color: #262c33; margin-top: 20px; margin-bottom: 12px; font-weight: 600; } .element-content p { margin-bottom: 15px; line-height: 1.7; } .element-content ul, .element-content ol { margin-bottom: 15px; padding-left: 25px; } .element-content li { margin-bottom: 8px; line-height: 1.6; } .element-content code { background: #fafafa; padding: 2px 6px; border-radius: 4px; font-family: 'Monaco', 'Consolas', monospace; font-size: 0.9em; color: #262c33; } .element-content pre { background: #fafafa; padding: 15px; border-radius: 4px; overflow-x: auto; margin-bottom: 15px; border-left: 3px solid #bfcbd9; } .element-content blockquote { border-left: 3px solid #bfcbd9; padding-left: 15px; margin: 15px 0; color: #666; font-style: italic; background: #fafafa; padding: 12px 15px; border-radius: 0 4px 4px 0; } .element-content table { width: 100%; border-collapse: collapse; margin-bottom: 15px; border: 1px solid #dae2ec; border-radius: 4px; overflow: hidden; } .element-content th, .element-content td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #dae2ec; } .element-content th { background: #fafafa; color: #262c33; font-weight: 600; } .element-content tr:hover { background: #fafafa; } .element-content a { color: #262c33; text-decoration: none; font-weight: 500; } .element-content a:hover { color: #666; text-decoration: underline; } .no-documentation { color: #666; font-style: italic; text-align: center; padding: 15px; background: #fafafa; border-radius: 4px; } .back-to-top { position: fixed; bottom: 30px; right: 30px; display: none; z-index: 1000; } .back-to-top-btn { background: white; color: #262c33; border: 1px solid #dae2ec; padding: 10px 12px; border-radius: 4px; cursor: pointer; font-size: 1em; transition: background-color 0.3s ease; } .back-to-top-btn:hover { background: #fafafa; } @media (max-width: 768px) { .container { margin: 0; box-shadow: none; } .header { padding: 40px 20px; } .main-title { font-size: 2.2em; } .stats-section, .diagram-section, .toc-section, .documentation-section { padding: 20px; } .stats-grid { grid-template-columns: 1fr; } .toc-list { columns: 1; } .element-header { padding: 20px; flex-direction: column; text-align: center; gap: 10px; } .element-meta { justify-content: center; } .element-content { padding: 20px; } } @media print { body { background: white; } .container { box-shadow: none; } .back-to-top { display: none; } .element-section { page-break-inside: avoid; break-inside: avoid; } } `; } /** * Escape HTML special characters */ _escapeHtml(unsafe) { return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;"); } /** * Get process information from BPMN diagram */ _getProcessInfo() { try { const rootElement = this._canvas.getRootElement(); if (rootElement?.businessObject) { const bo = rootElement.businessObject; return { name: bo.name || null, id: bo.id || null, filename: "process.bpmn", element: rootElement }; } return { name: null, id: null, filename: "process.bpmn", element: null }; } catch (error) { console.error("Error getting process info:", error); return { name: null, id: null, filename: "process.bpmn", element: null }; } } /** * Download file to user's system */ _downloadFile(content, filename, mimeType) { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } /** * Show notification to user */ _showNotification(message, type) { const notification = document.createElement("div"); notification.className = `notification notification-${type}`; notification.textContent = message; Object.assign(notification.style, { position: "fixed", top: "20px", right: "20px", padding: "12px 16px", borderRadius: "4px", color: "white", fontSize: "14px", fontWeight: "500", zIndex: "10000", maxWidth: "300px", wordWrap: "break-word", backgroundColor: type === "success" ? "#28a745" : type === "warning" ? "#ffc107" : "#dc3545", boxShadow: "0 2px 8px rgba(0,0,0,0.2)" }); document.body.appendChild(notification); setTimeout(() => { if (notification.parentNode) notification.parentNode.removeChild(notification); }, 3e3); } destroy() {} }; //#endregion //#region src/extension/managers/OverviewManager.ts var OverviewManager = class { _callbacks; _currentFilter = "all"; _currentSearchTerm = ""; constructor(options) { this._callbacks = options.callbacks; } setupOverviewEventListeners() { setTimeout(() => { document.getElementById("overview-search")?.addEventListener("input", (event) => { this.filterOverviewList(event.target.value); }); document.getElementById("show-all")?.addEventListener("click", () => { this.setOverviewFilter("all"); }); document.getElementById("show-documented")?.addEventListener("click", () => { this.setOverviewFilter("documented"); }); document.getElementById("show-undocumented")?.addEventListener("click", () => { this.setOverviewFilter("undocumented"); }); }, 100); } refreshOverview() { this._updateFilterButtonStates(); this._updateCoverageStats(); this._updateOverviewList(); } filterOverviewList(searchTerm) { this._currentSearchTerm = searchTerm; this._updateOverviewList(); } setOverviewFilter(filter) { this._currentFilter = filter; const sidebar = this._callbacks.getSidebar(); if (!sidebar) return; sidebar.querySelectorAll(".btn-small").forEach((btn) => { btn.classList.remove("active"); }); const activeButton = sidebar.querySelector(`#show-${filter}`); if (activeButton) activeButton.classList.add("active"); this._updateOverviewList(); } destroy() {} _updateFilterButtonStates() { const sidebar = this._callbacks.getSidebar(); if (!sidebar) return; sidebar.querySelectorAll(".btn-small").forEach((btn) => { btn.classList.remove("active"); }); const activeButton = sidebar.querySelector(`#show-${this._currentFilter}`); if (activeButton) activeButton.classList.add("active"); } _updateCoverageStats() { const sidebar = this._callbacks.getSidebar(); if (!sidebar) return; const elements = this._getAllElementsWithDocumentation(); const documentedCount = elements.filter((el) => el.hasDocumentation).length; const totalCount = elements.length; const percentage = totalCount > 0 ? Math.round(documentedCount / totalCount * 100) : 0; const documentedCountEl = sidebar.querySelector("#documented-count"); if (documentedCountEl) documentedCountEl.textContent = documentedCount.toString(); const totalCountEl = sidebar.querySelector("#total-count"); if (totalCountEl) totalCountEl.textContent = totalCount.toString(); const coveragePercentageEl = sidebar.querySelector("#coverage-percentage"); if (coveragePercentageEl) coveragePercentageEl.textContent = `${percentage}%`; const progressBar = sidebar.querySelector("#coverage-progress"); if (progressBar) progressBar.style.width = `${percentage}%`; } _getAllElementsWithDocumentation() { const elements = []; const seenIds = /* @__PURE__ */ new Set(); const allElements = this._callbacks.getAllElements(); allElements.forEach((element) => { if (element.businessObject?.id) { const bo = element.businessObject; const elementId = bo.id; if (seenIds.has(elementId)) return; seenIds.add(elementId); const documentation = this._callbacks.getElementDocumentation(element); elements.push({ id: elementId, name: bo.name || "Unnamed", type: this._callbacks.getElementTypeName(element), hasDocumentation: !!documentation?.trim(), documentation: documentation || "", element }); } }); return elements.sort((a, b) => a.id.localeCompare(b.id)); } _updateOverviewList() { const sidebar = this._callbacks.getSidebar(); if (!sidebar) return; const overviewList = sidebar.querySelector("#overview-list"); if (!overviewList) return; const elements = this._getAllElementsWithDocumentation(); let filteredElements = elements; if (this._currentFilter === "documented") filteredElements = elements.filter((el) => el.hasDocumentation); else if (this._currentFilter === "undocumented") filteredElements = elements.filter((el) => !el.hasDocumentation); if (this._currentSearchTerm) { const searchTerm = this._currentSearchTerm.toLowerCase(); filteredElements = filteredElements.filter((el) => el.id.toLowerCase().includes(searchTerm) || el.name.toLowerCase().includes(searchTerm) || el.documentation.toLowerCase().includes(searchTerm)); } if (filteredElements.length === 0) { overviewList.innerHTML = "<div class=\"overview-loading\">No elements found</div>"; return; } overviewList.innerHTML = filteredElements.map((element) => { const statusClass = element.hasDocumentation ? "documented" : "undocumented"; const statusText = element.hasDocumentation ? "documented" : "undocumented"; return ` <div class="element-item ${statusClass}" data-element-id="${element.id}"> <div class="element-header"> <span class="element-id">${element.id}</span> <span class="element-status ${statusClass}">${statusText}</span> </div> <div class="element-info"> <span>${element.name}</span> <span>•</span> <span>${element.type}</span> </div> </div> `; }).join(""); overviewList.querySelectorAll(".element-item").forEach((card) => { card.addEventListener("click", () => { const elementId = card.getAttribute("data-element-id"); if (elementId) { this._callbacks.selectElementById(elementId); this._callbacks.switchToElementTab(); } }); }); } }; //#endregion //#region src/extension/managers/SidebarManager.ts var SidebarManager = class { _canvas; _htmlGenerator; _onSidebarReady; _sidebar = null; _resizeObserver = null; _cleanupRaf = null; _isResizing = false; _resizeStartX = 0; _resizeStartWidth = 0; _isVerticalResizing = false; _resizeStartY = 0; _resizeStartHeight = 0; _customWidth = null; _wasVisible = false; _isMinimized = false; _minimizedIcon = null; constructor(options) { this._canvas = options.canvas; this._htmlGenerator = options.htmlGenerator; this._onSidebarReady = options.onSidebarReady; this._isMinimized = this._getMinimizedPreference(); } initializeSidebar() { const existingSidebar = document.getElementById("documentation-sidebar"); if (existingSidebar) existingSidebar.remove(); const existingHandle = document.getElementById("horizontal-resize-handle"); if (existingHandle) existingHandle.remove(); const existingHelpPopover = document.getElementById("help-popover"); if (existingHelpPopover) existingHelpPopover.remove(); const sidebar = document.createElement("div"); sidebar.id = "documentation-sidebar"; sidebar.className = "documentation-sidebar"; sidebar.style.display = "none"; sidebar.innerHTML = this._htmlGenerator.generateSidebarHTML(); const canvasContainer = this._getCanvasContainer(); const currentPosition = window.getComputedStyle(canvasContainer).position; if (currentPosition === "static") canvasContainer.style.position = "relative"; canvasContainer.appendChild(sidebar); this._sidebar = sidebar; const helpPopoverDiv = document.createElement("div"); helpPopoverDiv.innerHTML = this._htmlGenerator.generateHelpPopoverHTML(); const helpPopover = helpPopoverDiv.firstElementChild; canvasContainer.appendChild(helpPopover); const horizontalResizeHandle = document.createElement("div"); horizontalResizeHandle.id = "horizontal-resize-handle"; horizontalResizeHandle.className = "horizontal-resize-handle"; canvasContainer.appendChild(horizontalResizeHandle); if (this._onSidebarReady) setTimeout(() => { this._onSidebarReady?.(sidebar); }, 10); setTimeout(() => { this.updateSidebarPosition(); this.setupResizeObserver(); this.setupResizeHandles(); }, 100); } showSidebar() { if (this._sidebar && !this._sidebar.parentElement) { this.initializeSidebar(); setTimeout(() => { this.showSidebar(); }, 10); return; } if (this._isMinimized) { this._showMinimizedIcon(); return; } this._hideMinimizedIcon(); this.updateSidebarPosition(); if (this._sidebar) { this._sidebar.style.display = "flex"; this._sidebar.classList.add("visible"); } const horizontalHandle = document.getElementById("horizontal-resize-handle"); if (horizontalHandle) horizontalHandle.style.display = "block"; this._wasVisible = true; } hideSidebar() { if (this._sidebar) { this._wasVisible = this._sidebar.classList.contains("visible"); this._sidebar.classList.remove("visible"); this._sidebar.style.display = "none"; } this._hideMinimizedIcon(); const horizontalHandle = document.getElementById("horizontal-resize-handle"); if (horizontalHandle) horizontalHandle.style.display = "none"; } updateSidebarPosition() { const canvasContainer = this._getCanvasContainer(); const containerRect = canvasContainer.getBoundingClientRect(); const propertiesPanel = document.querySelector(".bio-properties-panel-container") || document.querySelector(".djs-properties-panel") || document.querySelector("[data-tab=\"properties\"]") || document.querySelector(".properties-panel"); if (propertiesPanel) { const panelRect = propertiesPanel.getBoundingClientRect(); const topOffset = Math.max(panelRect.top - containerRect.top, 0); const bottomOffset = Math.max(containerRect.bottom - panelRect.bottom, 0); const availableHeight = containerRect.height - topOffset - bottomOffset; if (this._sidebar) { this._sidebar.style.top = `${topOffset}px`; this._sidebar.style.height = `${Math.max(availableHeight, 300)}px`; if (!this._isResizing) { const width = this._customWidth ? `${this._customWidth}px` : "350px"; this._sidebar.style.setProperty("width", width, "important"); const horizontalHandle = document.getElementById("horizontal-resize-handle"); if (horizontalHandle) { const sidebarWidth = this._customWidth || 350; horizontalHandle.style.right = `${sidebarWidth}px`; horizontalHandle.style.top = `${topOffset}px`; horizontalHandle.style.height = `${Math.max(availableHeight, 900)}px`; } } } } else if (this._sidebar) { this._sidebar.style.right = "0px"; this._sidebar.style.top = "0px"; this._sidebar.style.height = "100%"; if (!this._isResizing) { const width = this._customWidth ? `${this._customWidth}px` : "350px"; this._sidebar.style.setProperty("width", width, "important"); const horizontalHandle = document.getElementById("horizontal-resize-handle"); if (horizontalHandle) { const sidebarWidth = this._customWidth || 350; horizontalHandle.style.right = `${sidebarWidth}px`; horizontalHandle.style.top = "0px"; horizontalHandle.style.height = "100%"; } } } } setupResizeObserver() { if (this._resizeObserver) this._resizeObserver.disconnect(); const propertiesPanel = document.querySelector(".bio-properties-panel-container") || document.querySelector(".djs-properties-panel") || document.querySelector("[data-tab=\"properties\"]") || document.querySelector(".properties-panel"); if (propertiesPanel && window.ResizeObserver) { this._resizeObserver = new ResizeObserver(() => { requestAnimationFrame(() => { this.updateSidebarPosition(); }); }); this._resizeObserver.observe(propertiesPanel); const parentContainer = propertiesPanel.parentElement; if (parentContainer) this._resizeObserver.observe(parentContainer);