// 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
};
})();