';
}
@@ -5233,32 +5494,47 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
})();
// 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, alongside a TOC pane on the right.
-// Save (Ctrl+S) writes back via:
-// - PUT to the file's server URL when in server mode, or
-// - FileSystemWritableFileStream when in FS-API mode (local folder
-// picker). Both paths set dirty=false + a status timestamp on
-// success.
-// zip-virtual files are read-only — the save button stays disabled.
//
-// Toast UI Editor is bundled (shared/vendor/toastui-editor-all.min.js)
-// and is available synchronously as window.toastui by the time this
-// module runs.
+// 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 TOC_MIN_WIDTH = 180;
+ var TOC_MAX_WIDTH = 480;
+ var TOC_DEFAULT_WIDTH = 260;
+
function escapeHtml(s) {
return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}
- var currentInstance = null; // { editor, container, dirty, node, hash, tocEl }
+ var currentInstance = null; // { editor, container, dirty, node, hash, tocEl, fmEl }
+ var lastTocWidth = TOC_DEFAULT_WIDTH; // remember across mounts
- // Compute SHA-256 hex of a string for a "is this content different
- // from what we loaded?" check. Used to enable/disable Save.
async function hashContent(text) {
if (!window.crypto || !window.crypto.subtle) return null;
var enc = new TextEncoder().encode(text);
@@ -5278,20 +5554,80 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
currentInstance = null;
}
- // ── TOC (table of contents) ─────────────────────────────────────────────
- //
- // Ported from mdedit/js/toc.js, condensed: parse markdown for ATX-style
- // headings, build a flat hierarchical list, click jumps the editor to
- // the heading's line. We track WYSIWYG vs markdown mode and route the
- // scroll behaviour to whichever pane is visible.
+ // ── 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 = '
';
+ for (var i = 0; i < keys.length; i++) {
+ var k = keys[i];
+ var v = parsed.data[k];
+ var displayV = Array.isArray(v)
+ ? v.map(escapeHtml).join(', ')
+ : escapeHtml(String(v));
+ html += '
' + escapeHtml(k) + '
' + displayV + '
';
+ }
+ html += '
';
+ fmEl.innerHTML = html;
+ }
+
+ // ── TOC (table of contents) ────────────────────────────────────────────
+ // ATX headings only; the body markdown drives the outline. Clicking
+ // a heading routes to whichever Toast UI pane is currently active
+ // (WYSIWYG or markdown preview).
function parseHeadings(content) {
var headings = [];
- var lines = content.split('\n');
+ // Strip front matter so headings inside the envelope (e.g. comments)
+ // don't appear in the outline.
+ var parsed = parseFrontMatter(content);
+ var body = parsed.body;
+ var lines = body.split('\n');
+ var inFence = false;
for (var i = 0; i < lines.length; i++) {
- var m = lines[i].match(/^(#{1,6})\s+(.+)$/);
+ var line = lines[i];
+ // Skip fenced code blocks — headings inside them aren't real.
+ if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
+ if (inFence) continue;
+ var m = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
if (!m) continue;
- var text = m[2].trim()
+ var text = m[2]
.replace(/\\(.)/g, '$1')
.replace(/\*\*(.*?)\*\*/g, '$1')
.replace(/\*(.*?)\*/g, '$1')
@@ -5313,8 +5649,11 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
var hs = ww.querySelectorAll('h1, h2, h3, h4, h5, h6');
for (var i = 0; i < hs.length; i++) {
if (hs[i].textContent.trim() === heading.text) {
- var top = hs[i].getBoundingClientRect().top - ww.getBoundingClientRect().top;
- ww.scrollTop = top - 10;
+ var scroller = findScrollParent(hs[i]) || ww;
+ scroller.scrollTo({
+ top: hs[i].offsetTop - 12,
+ behavior: 'smooth'
+ });
flashHeading(hs[i]);
return;
}
@@ -5322,56 +5661,72 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr
} else {
var line = heading.lineIndex + 1;
try { editor.setSelection([line, 1], [line, 1]); } catch (_) { /* ignore */ }
+ // Find the matching heading in the live markdown preview
+ // (right column of split view). If preview is collapsed
+ // (markdown-only) this is a no-op.
var preview = els.mdPreview;
- if (!preview) return;
- var phs = preview.querySelectorAll('h1, h2, h3, h4, h5, h6');
- for (var j = 0; j < phs.length; j++) {
- if (phs[j].textContent.trim() === heading.text) {
- var ptop = phs[j].getBoundingClientRect().top - preview.getBoundingClientRect().top;
- preview.scrollTop = ptop - 10;
- flashHeading(phs[j]);
- return;
+ if (preview) {
+ var phs = preview.querySelectorAll('h1, h2, h3, h4, h5, h6');
+ for (var j = 0; j < phs.length; j++) {
+ if (phs[j].textContent.trim() === heading.text) {
+ var pscroller = findScrollParent(phs[j]) || preview;
+ pscroller.scrollTo({
+ top: phs[j].offsetTop - 12,
+ behavior: 'smooth'
+ });
+ flashHeading(phs[j]);
+ return;
+ }
}
}
}
- } catch (e) { /* swallow; click was best-effort */ }
+ } catch (_e) { /* swallow; click was best-effort */ }
+ }
+
+ function findScrollParent(el) {
+ var cur = el.parentElement;
+ while (cur) {
+ var s = getComputedStyle(cur);
+ if (/(auto|scroll)/.test(s.overflowY)) return cur;
+ cur = cur.parentElement;
+ }
+ return null;
}
function flashHeading(el) {
if (!el) return;
- el.style.transition = 'background-color 0.3s ease';
- el.style.backgroundColor = 'var(--primary-light)';
- setTimeout(function () {
- el.style.backgroundColor = '';
- setTimeout(function () { el.style.transition = ''; }, 300);
- }, 1200);
+ el.classList.add('md-toc__flash');
+ setTimeout(function () { el.classList.remove('md-toc__flash'); }, 900);
}
function renderToc(tocEl, content, editor) {
if (!tocEl) return;
+ if (!content || !content.trim()) {
+ tocEl.innerHTML = '
Empty file.
';
+ return;
+ }
var headings = parseHeadings(content);
- if (!content.trim()) {
- tocEl.innerHTML = '