]> BookStack Code Mirror - hacks/commitdiff
refactor(mermaid-viewer): Construct viewer DOM programmatically
authorAlexander Wilms <redacted>
Thu, 26 Jun 2025 22:06:42 +0000 (00:06 +0200)
committerAlexander Wilms <redacted>
Thu, 26 Jun 2025 22:06:42 +0000 (00:06 +0200)
content/mermaid-viewer/head.html

index 3037a8a0897fa6e38e76dc435a1a485e506648ee..aeec2627792f39a09e6dee7556227eed542958dd 100644 (file)
             this.setupEventListeners();
         }
 
+        /**
+         * Creates the DOM structure for the viewer programmatically.
+         * This is safer and more maintainable than using innerHTML with a large template string.
+         */
         setupViewer() {
-            this.container.innerHTML = `
-                <div class="${CSS_CLASSES.CONTROLS}">
-                    <div class="${CSS_CLASSES.BUTTON_BASE} mermaid-btn toggle-interaction" title="Toggle interaction">
-                        <i class="${CSS_CLASSES.LOCK_ICON}" aria-hidden="true"></i>
-                    </div>
-                    <div class="${CSS_CLASSES.BUTTON_BASE} mermaid-btn" title="Copy code">
-                        <i class="fa fa-copy" aria-hidden="true"></i>
-                    </div>
-                </div>
-                <div class="${CSS_CLASSES.ZOOM_CONTROLS}">
-                    <div class="${CSS_CLASSES.BUTTON_BASE} mermaid-zoom-btn zoom-in" title="Zoom in"><i class="fa fa-search-plus" aria-hidden="true"></i></div>
-                    <div class="${CSS_CLASSES.BUTTON_BASE} mermaid-zoom-btn zoom-out" title="Zoom out"><i class="fa fa-search-minus" aria-hidden="true"></i></div>
-                    <div class="${CSS_CLASSES.BUTTON_BASE} mermaid-zoom-btn zoom-reset" title="Reset"><i class="fa fa-refresh" aria-hidden="true"></i></div>
-                </div>
-                <div class="${CSS_CLASSES.VIEWPORT}">
-                    <div class="${CSS_CLASSES.CONTENT}">
-                        <div class="${CSS_CLASSES.DIAGRAM}">${this.mermaidCode}</div>
-                    </div>
-                </div>
-            `;
-            this.viewport = this.container.querySelector(`.${CSS_CLASSES.VIEWPORT}`);
-            this.content = this.container.querySelector(`.${CSS_CLASSES.CONTENT}`);
-            this.diagram = this.container.querySelector(`.${CSS_CLASSES.DIAGRAM}`);
-
-            // Cache control elements
-            this.toggleInteractionBtn = this.container.querySelector('.toggle-interaction');
-            this.copyCodeBtn = this.container.querySelector('.mermaid-btn:not(.toggle-interaction)');
-            this.zoomInBtn = this.container.querySelector('.zoom-in');
-            this.zoomOutBtn = this.container.querySelector('.zoom-out');
-            this.zoomResetBtn = this.container.querySelector('.zoom-reset');
+            const createButton = (title, iconClass, ...extraClasses) => {
+                const button = document.createElement('div');
+                button.className = `${CSS_CLASSES.BUTTON_BASE} ${extraClasses.join(' ')}`;
+                button.title = title;
+                const icon = document.createElement('i');
+                icon.className = iconClass;
+                icon.setAttribute('aria-hidden', 'true');
+                button.append(icon);
+                return button;
+            };
+
+            const controls = document.createElement('div');
+            controls.className = CSS_CLASSES.CONTROLS;
+            this.toggleInteractionBtn = createButton('Toggle interaction', CSS_CLASSES.LOCK_ICON, 'mermaid-btn', 'toggle-interaction');
+            this.copyCodeBtn = createButton('Copy code', 'fa fa-copy', 'mermaid-btn');
+            controls.append(this.toggleInteractionBtn, this.copyCodeBtn);
+
+            const zoomControls = document.createElement('div');
+            zoomControls.className = CSS_CLASSES.ZOOM_CONTROLS;
+            this.zoomInBtn = createButton('Zoom in', 'fa fa-search-plus', 'mermaid-zoom-btn', 'zoom-in');
+            this.zoomOutBtn = createButton('Zoom out', 'fa fa-search-minus', 'mermaid-zoom-btn', 'zoom-out');
+            this.zoomResetBtn = createButton('Reset', 'fa fa-refresh', 'mermaid-zoom-btn', 'zoom-reset');
+            zoomControls.append(this.zoomInBtn, this.zoomOutBtn, this.zoomResetBtn);
+
+            this.diagram = document.createElement('div');
+            this.diagram.className = CSS_CLASSES.DIAGRAM;
+            // Use textContent for security, preventing any potential HTML injection.
+            // Mermaid will parse the text content safely.
+            this.diagram.textContent = this.mermaidCode;
+
+            this.content = document.createElement('div');
+            this.content.className = CSS_CLASSES.CONTENT;
+            this.content.append(this.diagram);
+
+            this.viewport = document.createElement('div');
+            this.viewport.className = CSS_CLASSES.VIEWPORT;
+            this.viewport.append(this.content);
+
+            // Clear the container and append the new structure
+            this.container.innerHTML = '';
+            this.container.append(controls, zoomControls, this.viewport);
 
             // Function to render the diagram and perform post-render setup
             const renderAndSetup = () => {