c[0]);
for (const a of list) if (!catOrder.includes(a.cat)) catOrder.push(a.cat);
const byCat = new Map(catOrder.map(c => [c, []]));
for (const a of list) byCat.get(a.cat).push(a);
const back = new Map();
for (const a of list) {
a.text = a.lines.join('\n');
a.outs = new Set([...a.text.matchAll(/\[\[([^\]|]+)(?:\|[^\]]*)?\]\]/g)].map(m => m[1].trim()));
a.outs.delete(a.title);
for (const t of a.outs) { if (!back.has(t)) back.set(t, new Set()); back.get(t).add(a.title); }
}
const bySlug = new Map(list.map(a => [slug(a.title), a.title]));
// ---- inline markup ----
function wlink(t, label) {
const a = byT.get(t);
return `${label}`;
}
function pullNotes(s, ctx, K) {
let out = '', i = 0;
while (i < s.length) {
const j = s.indexOf('[*', i);
if (j < 0) { out += s.slice(i); break; }
out += s.slice(i, j);
let d = 0, k = j;
for (; k < s.length; k++) { if (s[k] === '[') d++; else if (s[k] === ']') { d--; if (d === 0) break; } }
if (k >= s.length) { out += s.slice(j); break; }
const body = s.slice(j + 2, k).trim();
ctx.notes.push(body);
const n = ctx.notes.length;
out += K(`[${n}]`);
i = k + 1;
}
return out;
}
function inline(s, ctx) {
const keep = [];
const K = h => { keep.push(h); return '\u0000' + (keep.length - 1) + '\u0000'; };
if (ctx) s = pullNotes(s, ctx, K);
s = s.replace(/`([^`]+)`/g, (_, c) => K('' + esc(c) + ''));
s = esc(s);
s = s.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_, t, l) => K(wlink(unesc(t.trim()), l || t.trim())));
s = s.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_, l, u) => K(`${l}`));
s = s.replace(/https?:\/\/[A-Za-z0-9\-._~:\/?#@!$&;'()*+,=%]+/g, u => {
let tail = '';
while (/[.,;:'!?]$/.test(u) || (u.endsWith(')') && (u.match(/\(/g) || []).length < (u.match(/\)/g) || []).length)) { tail = u.slice(-1) + tail; u = u.slice(0, -1); }
const shown = unesc(u).replace(/^https?:\/\/(www\.)?/, '');
return K(`${esc(shown.length > 56 ? shown.slice(0, 55) + '…' : shown)}`) + tail;
});
s = s.replace(/\*\*(.+?)\*\*/g, '$1');
while (/\u0000\d+\u0000/.test(s)) s = s.replace(/\u0000(\d+)\u0000/g, (_, i) => keep[+i]);
return s;
}
// ---- block markup ----
const isUl = l => /^\s*[-*]\s+/.test(l), isOl = l => /^\s*\d+[.)]\s+/.test(l);
const isStart = l => /^```/.test(l) || /^#{2,3}\s/.test(l) || /^\|/.test(l) || /^>\s?/.test(l) || isUl(l) || isOl(l) || /^\{\{.+\}\}$/.test(l.trim());
function cells(row) {
const s = row.trim().replace(/^\|/, '').replace(/\|$/, '');
const out = []; let cur = '', dep = 0, code = false;
for (let i = 0; i < s.length; i++) {
const c = s[i];
if (c === '`') code = !code;
if (!code && c === '[' && s[i + 1] === '[') dep++;
if (!code && c === ']' && s[i + 1] === ']' && dep > 0) dep--;
if (c === '\\' && s[i + 1] === '|') { cur += '|'; i++; continue; }
if (c === '|' && !code && dep === 0) { out.push(cur.trim()); cur = ''; continue; }
cur += c;
}
out.push(cur.trim());
return out;
}
function table(rows, ctx) {
const r = rows.map(cells);
let head = null, body = r;
if (r.length > 1 && r[1].every(c => /^:?-{2,}:?$/.test(c))) { head = r[0]; body = r.slice(2); }
return `
${head ? `
${head.map(c => `
${inline(c, ctx)}
`).join('')}
` : ''}${body.map(row => `
${row.map(c => `
${inline(c, ctx)}
`).join('')}
`).join('')}
`;
}
function render(a) {
const ctx = { notes: [], heads: [] }, L = a.lines;
let h = '', i = 0, n2 = 0, n3 = 0, box = 0;
while (i < L.length) {
const l = L[i];
if (/^```/.test(l)) {
const lang = l.slice(3).trim(), buf = []; i++;
while (i < L.length && !/^```/.test(L[i])) buf.push(L[i++]);
i++;
h += `
${esc(buf.join('\n'))}
`;
continue;
}
const m = l.match(/^(#{2,3})\s+(.+)$/);
if (m) {
const lv = m[1].length;
let num;
if (lv === 2) { n2++; n3 = 0; num = n2 + '.'; } else { n3++; num = n2 + '.' + n3 + '.'; }
const id = 'h-' + ctx.heads.length;
ctx.heads.push({ lv, num, text: m[2], id });
h += `${num} ${inline(m[2], ctx)}`;
i++; continue;
}
if (/^\|/.test(l)) { const rows = []; while (i < L.length && /^\|/.test(L[i])) rows.push(L[i++]); h += table(rows, ctx); continue; }
if (/^>\s?/.test(l)) { const b = []; while (i < L.length && /^>\s?/.test(L[i])) b.push(L[i++].replace(/^>\s?/, '')); h += ``; continue; }
if (isUl(l) || isOl(l)) {
const ol = isOl(l), same = ol ? isOl : isUl, items = [];
while (i < L.length) {
const x = L[i];
if (same(x)) { items.push(x.replace(ol ? /^\s*\d+[.)]\s+/ : /^\s*[-*]\s+/, '')); i++; }
else if (x.trim() !== '' && /^\s{2,}/.test(x) && items.length) { items[items.length - 1] += ' ' + x.trim(); i++; }
else if (x.trim() === '') { let k = i + 1; while (k < L.length && L[k].trim() === '') k++; if (k < L.length && same(L[k])) i = k; else break; }
else break;
}
h += `<${ol ? 'ol' : 'ul'}>` + items.map(t => {
const c = t.match(/^\[( |x|X)\]\s+(.*)$/);
if (!c) return `
${inline(t, ctx)}
`;
const k = a.title + '#' + (box++);
return ``;
}).join('') + `${ol ? 'ol' : 'ul'}>`;
continue;
}
const mac = l.trim().match(/^\{\{(.+)\}\}$/);
if (mac) { h += macro(mac[1].trim()); i++; continue; }
if (l.trim() === '') { i++; continue; }
const b = [L[i++]];
while (i < L.length && L[i].trim() !== '' && !isStart(L[i])) b.push(L[i++]);
h += `
${inline(b.join(' '), ctx)}
`;
}
return { html: h, ctx };
}
// ---- self-diagnosis: check questions, glossary scan, and the list they produce ----
const MARKS = [['ok', '알았다'], ['meh', '애매'], ['no', '몰랐다']];
function checkBox(a, flash) {
const m = diag[a.title];
return `
${flash ? `${esc(a.cat)}` : '스스로 확인 읽기 전에 먼저 답해 보세요'}