A second notebook whose pages are encrypted with a key made from a passphrase plus the server key. It is unlocked into memory only (15 minutes idle, Lock, or sign-out), is not in search, lists or downloads of the ordinary notebook, and limits wrong guesses. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
83 lines
4.3 KiB
JavaScript
83 lines
4.3 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);
|
|
|
|
function wikiLink(target, text) {
|
|
const title = target.trim(), 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.
|
|
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[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;
|
|
}
|