A page, including a journal day, can be moved into the unlocked vault with its id kept. Links to it elsewhere become [[locked:id]], shown as Locked Page until the vault is unlocked, so its title is kept only in the vault. Move out reverses it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
94 lines
5.1 KiB
JavaScript
94 lines
5.1 KiB
JavaScript
'use strict';
|
|
|
|
// ---------- markdown ----------
|
|
// A small, safe renderer. It builds elements (never HTML strings), and only http(s) links are links.
|
|
// [[Page title]] and [[Page title|text]] link to other pages; pageByTitle (app.js) says which exist.
|
|
const INLINE_SRC = '`([^`\\n]+)`|\\*\\*([^*\\n]+)\\*\\*|(?<![\\w*])\\*([^*\\s][^*\\n]*?)\\*(?![\\w*])|\\[\\[([^\\[\\]|\\n]+?)(?:\\|([^\\[\\]\\n]*))?\\]\\]|\\[([^\\]\\n]+)\\]\\((https?:\\/\\/[^\\s)]+)\\)|(https?:\\/\\/[^\\s<]*[^\\s<.,;:!?)\\]\'"])|(?<=^|[\\s(])#([\\p{L}\\p{N}_][\\p{L}\\p{N}_/-]*)';
|
|
const tagChip = t => h('a', { class: 'chip', href: '#/tag/' + encodeURIComponent(t) }, '#' + t);
|
|
const extLink = (url, label) => h('a', { href: url, target: '_blank', rel: 'noopener noreferrer' }, label);
|
|
|
|
// A link to a page that was moved to the vault reads [[locked:id]]. It says "Locked Page" until the vault is
|
|
// unlocked, and then shows the page's own name and opens it.
|
|
function lockedLink(id) {
|
|
const name = S.vaultNames?.get(id) ?? (S.vaultNames && id.startsWith('daily-') ? dayLabel(id.slice(6)) : null);
|
|
if (name != null) return h('a', { class: 'wikilink in-vault', href: '#/vault/' + id }, icon('lock', 16), name);
|
|
return h('a', { class: 'wikilink locked', href: '#/vault', title: 'This page is in your vault. Unlock the vault to open it.' }, icon('lock', 16), 'Locked Page');
|
|
}
|
|
|
|
function wikiLink(target, text) {
|
|
const title = target.trim();
|
|
if (/^locked:[a-z0-9-]+$/.test(title)) return lockedLink(title.slice(7));
|
|
const page = pageByTitle(title);
|
|
if (page) return h('a', { class: 'wikilink', href: '#/p/' + page.id }, text || title);
|
|
return h('a', { class: 'wikilink missing', href: '#/link/' + encodeURIComponent(title), title: 'No page called "' + title + '" yet. Click to create it.' }, text || title);
|
|
}
|
|
|
|
// With plain set (the vault) [[links]] and #tags stay as typed, so a page there never points into the other notebook.
|
|
// Only a link to another page in the vault, which is [[locked:id]], is shown as a link.
|
|
function inline(text, plain) {
|
|
const re = new RegExp(INLINE_SRC, 'gu'), out = [];
|
|
let last = 0, m;
|
|
while ((m = re.exec(text))) {
|
|
if (m.index > last) out.push(text.slice(last, m.index));
|
|
last = re.lastIndex;
|
|
if (m[1] != null) out.push(h('code', {}, m[1]));
|
|
else if (m[2] != null) out.push(h('strong', {}, inline(m[2], plain)));
|
|
else if (m[3] != null) out.push(h('em', {}, inline(m[3], plain)));
|
|
else if (m[4] != null) out.push(plain && !m[4].trim().startsWith('locked:') ? m[0] : wikiLink(m[4], m[5]));
|
|
else if (m[6] != null) out.push(extLink(m[7], m[6]));
|
|
else if (m[8] != null) out.push(extLink(m[8], m[8]));
|
|
else {
|
|
const t = m[9].replace(/[/-]+$/, '');
|
|
if (plain || !t || /^\d+$/.test(t)) out.push('#' + m[9]);
|
|
else out.push(tagChip(t.toLowerCase()), m[9].slice(t.length));
|
|
}
|
|
}
|
|
if (last < text.length) out.push(text.slice(last));
|
|
return out;
|
|
}
|
|
|
|
// md renders a page. onTask(lineIndex, checked) makes the checkboxes work; without it they are read-only.
|
|
function md(src, onTask, big, plain) {
|
|
const root = h('div', { class: 'md' + (big ? ' md-big' : '') }), lines = src.split('\n');
|
|
let para = [], i = 0, m;
|
|
const flush = () => {
|
|
if (!para.length) return;
|
|
const p = h('p');
|
|
para.forEach((t, k) => { if (k) p.append(h('br')); p.append(...inline(t, plain)); });
|
|
root.append(p); para = [];
|
|
};
|
|
while (i < lines.length) {
|
|
const line = lines[i];
|
|
if (/^\s*```/.test(line)) {
|
|
flush(); const code = []; i++;
|
|
while (i < lines.length && !/^\s*```/.test(lines[i])) code.push(lines[i++]);
|
|
i++; root.append(h('pre', {}, h('code', {}, code.join('\n')))); continue;
|
|
}
|
|
if (!line.trim()) { flush(); i++; continue; }
|
|
if ((m = /^(#{1,3})\s+(.+)$/.exec(line))) { flush(); root.append(h('h' + (m[1].length + 2), { class: 'md-h' }, inline(m[2], plain))); i++; continue; }
|
|
if (/^\s*(-{3,}|\*{3,})\s*$/.test(line)) { flush(); root.append(h('hr')); i++; continue; }
|
|
if (/^>/.test(line)) {
|
|
flush(); const q = [];
|
|
while (i < lines.length && /^>/.test(lines[i])) q.push(lines[i++].replace(/^>\s?/, ''));
|
|
const bq = h('blockquote'); q.forEach((t, k) => { if (k) bq.append(h('br')); bq.append(...inline(t, plain)); }); root.append(bq); continue;
|
|
}
|
|
if (/^(\s*)([-*+]|\d+[.)])\s+/.test(line)) {
|
|
flush(); const ordered = /^\s*\d/.test(line), list = h(ordered ? 'ol' : 'ul');
|
|
while (i < lines.length && (m = /^(\s*)([-*+]|\d+[.)])\s+(?:\[([ xX])\]\s+)?(.*)$/.exec(lines[i]))) {
|
|
const lvl = Math.min(3, Math.floor(m[1].replace(/\t/g, ' ').length / 2)), at = i;
|
|
const li = h('li', { class: 'ind-' + lvl + (m[3] != null ? ' task' : '') });
|
|
if (m[3] != null) {
|
|
const box = h('input', { type: 'checkbox', checked: m[3] !== ' ', disabled: !onTask, 'aria-label': m[4] || 'Task' });
|
|
if (onTask) box.addEventListener('change', () => onTask(at, box.checked));
|
|
li.append(box, h('span', { class: m[3] !== ' ' ? 'done' : '' }, inline(m[4], plain)));
|
|
} else li.append(...inline(m[4], plain));
|
|
list.append(li); i++;
|
|
}
|
|
root.append(list); continue;
|
|
}
|
|
para.push(line); i++;
|
|
}
|
|
flush();
|
|
return root;
|
|
}
|