// preview-markdown.js — markdown plugin for the browse preview pane. // Click a .md / .markdown file in the tree → instantiate Toast UI // editor inside the right pane. Save (Ctrl+S) writes back via PUT // when the file came from a server URL; FS-API and zip-virtual files // are read-only for now (toolbar shows a hint). // // Toast UI Editor is loaded from mdedit's bundled vendor file in the // browse build (see browse/build.sh). window.toastui is available // synchronously when this module runs. (function () { 'use strict'; if (!window.app || !window.app.modules) return; function escapeHtml(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } var currentInstance = null; // { editor, container, dirty, node, hash } // Compute SHA-256 hex of a string for a quick "is this content // different from what was loaded?" check. Used to decide whether // the save button should be active. Not used for integrity. async function hashContent(text) { if (!window.crypto || !window.crypto.subtle) return null; var enc = new TextEncoder().encode(text); var buf = await window.crypto.subtle.digest('SHA-256', enc); var bytes = new Uint8Array(buf); var hex = ''; for (var i = 0; i < bytes.length; i++) { hex += bytes[i].toString(16).padStart(2, '0'); } return hex; } function dispose() { if (currentInstance && currentInstance.editor) { try { currentInstance.editor.destroy(); } catch (_) { /* ignore */ } } currentInstance = null; } async function render(node, container, ctx) { if (typeof window.toastui === 'undefined') { container.innerHTML = '
' + 'Toast UI Editor isn\'t bundled in this build.
'; return; } // Tear down any previous markdown instance (single-file model). dispose(); // Read the file content. var text; try { var buf = await ctx.getArrayBuffer(node); text = new TextDecoder('utf-8', { fatal: false }).decode(buf); } catch (e) { container.innerHTML = '
' + 'Could not read ' + escapeHtml(node.name) + ': ' + escapeHtml(e.message || String(e)) + '
'; return; } // Build the markdown plugin's DOM: // ┌──────────────────────────────────┐ // │ toolbar (Save, dirty marker) │ // ├──────────────────────────────────┤ // │ Toast UI editor │ // └──────────────────────────────────┘ // // TOC pane is deferred — a near-term iteration can split this // into editor | toc once the simpler form is exercised. container.innerHTML = ''; container.style.display = 'flex'; container.style.flexDirection = 'column'; var toolbar = document.createElement('div'); toolbar.className = 'md-toolbar'; toolbar.style.cssText = 'display:flex;align-items:center;gap:0.5rem;' + 'padding:0.35rem 0.75rem;background:var(--bg-secondary);' + 'border-bottom:1px solid var(--border);flex-shrink:0;'; var saveBtn = document.createElement('button'); saveBtn.className = 'btn btn-sm btn-primary'; saveBtn.type = 'button'; saveBtn.textContent = 'Save'; saveBtn.disabled = true; // enabled when content changes var dirty = document.createElement('span'); dirty.style.cssText = 'color:var(--text-muted);font-size:0.85rem;'; dirty.textContent = ''; var status = document.createElement('span'); status.style.cssText = 'flex:1;text-align:right;color:var(--text-muted);font-size:0.85rem;'; toolbar.appendChild(saveBtn); toolbar.appendChild(dirty); toolbar.appendChild(status); container.appendChild(toolbar); var editorHost = document.createElement('div'); editorHost.style.cssText = 'flex:1;min-height:0;overflow:hidden;'; container.appendChild(editorHost); var initialHash = await hashContent(text); var editor = new window.toastui.Editor({ el: editorHost, height: '100%', initialEditType: 'markdown', previewStyle: 'vertical', initialValue: text, usageStatistics: false, toolbarItems: [ ['heading', 'bold', 'italic', 'strike'], ['hr', 'quote'], ['ul', 'ol', 'task', 'indent', 'outdent'], ['table', 'image', 'link'], ['code', 'codeblock'] ] }); currentInstance = { editor: editor, container: container, dirty: false, node: node, hash: initialHash }; function markDirty(isDirty) { currentInstance.dirty = isDirty; saveBtn.disabled = !isDirty; dirty.textContent = isDirty ? '● modified' : ''; } editor.on('change', async function () { var current = editor.getMarkdown(); var h = await hashContent(current); markDirty(h !== currentInstance.hash); }); async function save() { if (!currentInstance.dirty) return; var content = editor.getMarkdown(); // Read-only sources: zip-virtual, FS-API without write // permission. For now we only attempt PUT against server URLs; // FS-API saves can be wired in a later iteration via the // existing zddc-source polyfill. if (!node.url || window.app.state.source !== 'server') { status.textContent = 'Save not yet supported for this source.'; return; } try { status.textContent = 'Saving…'; var resp = await fetch(node.url, { method: 'PUT', headers: { 'Content-Type': 'text/markdown; charset=utf-8' }, body: content, credentials: 'same-origin' }); if (!resp.ok) { throw new Error('HTTP ' + resp.status); } currentInstance.hash = await hashContent(content); markDirty(false); status.textContent = 'Saved ' + new Date().toLocaleTimeString(); } catch (e) { status.textContent = 'Save failed: ' + (e.message || e); } } saveBtn.addEventListener('click', save); // Ctrl+S / Cmd+S inside the editor → save. container.addEventListener('keydown', function (e) { if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); save(); } }); } window.app.modules.markdown = { render: render, dispose: dispose }; })();