// preview-markdown.js — markdown plugin for the browse preview pane. // // Layout (CSS Grid): // ┌─────────────────────────────────────────────────────────────────┐ // │ toolbar: Save | ● modified | status | source │ // ├────────────────────────────────────────┬────────────────────────┤ // │ │ Outline │ // │ │ • Heading 1 │ // │ Toast UI Editor │ • Subheading │ // │ (md / wysiwyg / preview) │ • Heading 2 │ // │ ├────────────────────────┤ // │ │ Front matter │ // │ │ title: Foo │ // │ │ revision: A │ // └────────────────────────────────────────┴────────────────────────┘ // Grid keeps every cell's size definite, which is what Toast UI needs // to compute its inner scroll regions correctly. The previous nested- // flexbox layout produced indeterminate heights and a fragile TOC // pane width — grid fixes both. // // Save (Ctrl+S) writes back via PUT (server mode) or // FileSystemWritableFileStream (FS-API). Zip-virtual files are // read-only — Save stays disabled. Toast UI is vendored // (shared/vendor/toastui-editor-all.min.js); window.toastui is // available synchronously before this module runs. (function () { 'use strict'; if (!window.app || !window.app.modules) return; var SIDEBAR_MIN_WIDTH = 180; var SIDEBAR_MAX_WIDTH = 480; var SIDEBAR_DEFAULT_WIDTH = 280; var FM_DEFAULT_HEIGHT = 180; // px — front-matter pane height inside sidebar function escapeHtml(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } var currentInstance = null; // { editor, container, dirty, node, hash, tocEl, fmEl } var lastSidebarWidth = SIDEBAR_DEFAULT_WIDTH; // remember across mounts var lastFmHeight = FM_DEFAULT_HEIGHT; 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; } // ── Front matter ──────────────────────────────────────────────────────── // Lightweight YAML front-matter parser. Same envelope as mdedit's: // `---\n…\n---\n`, key:value lines, simple `[a, b, c]` arrays. function parseFrontMatter(content) { if (!content || content.indexOf('---\n') !== 0) { return { data: {}, body: content || '' }; } var endIdx = content.indexOf('\n---\n', 4); if (endIdx === -1) return { data: {}, body: content }; var fmText = content.substring(4, endIdx); var body = content.substring(endIdx + 5); var data = {}; var lines = fmText.split('\n'); for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); if (!line || line.charAt(0) === '#') continue; var colon = line.indexOf(':'); if (colon <= 0) continue; var key = line.substring(0, colon).trim(); var val = line.substring(colon + 1).trim(); val = val.replace(/^["']|["']$/g, ''); if (val.startsWith('[') && val.endsWith(']')) { val = val.slice(1, -1).split(',').map(function (s) { return s.trim().replace(/^["']|["']$/g, ''); }); } data[key] = val; } return { data: data, body: body }; } function renderFrontMatter(fmEl, content) { if (!fmEl) return; var parsed = parseFrontMatter(content); var keys = Object.keys(parsed.data); if (keys.length === 0) { fmEl.innerHTML = '
No front matter.
'; return; } var html = 'Empty file.
'; return; } var headings = parseHeadings(content); if (headings.length === 0) { tocEl.innerHTML = 'No headings yet.
'; return; } // Build a flat list; CSS handles indentation. Using a flat list // (rather than nested