// md.jsx — рендер markdown материалов базы в React-элементы (без innerHTML). // Вырос из мини-рендера wiki.jsx ЦБ LMS: там хватало заголовков, списков и ссылок, // материалы базы сообщества — это ещё таблицы, код, цитаты и разделители. // // Блоки: ## / ### / ####, абзацы, списки - и 1. (с вложенностью по отступу), // таблицы |…|, ```код```, > цитаты, ---. // Строчные: **жирный**, *курсив*, `код`, [текст](url), [[slug]] / [[slug|текст]], // [текст](slug) — ссылка на другой материал базы по slug. // Ссылки наружу — только http(s)/mailto, всё прочее выводится текстом. const MD_SLUG = /^[a-z0-9][a-z0-9-]*$/; function mdInline(text, ctx, keyBase) { const parts = []; const re = /`([^`]+)`|\[\[([a-z0-9-]+)(?:\|([^\]]+))?\]\]|\[([^\]]+)\]\(([^)\s]+)\)|\*\*([^*]+)\*\*|\*([^*\s][^*]*?)\*/g; let last = 0, m, i = 0; const k = () => `${keyBase}-${i++}`; const internal = (slug, label) => { const known = ctx.slugs && ctx.slugs.has(slug); if (!known) return label; // материал ещё не написан — просто текст return { e.preventDefault(); ctx.open(slug); }}>{label}; }; while ((m = re.exec(text))) { if (m.index > last) parts.push(text.slice(last, m.index)); if (m[1] !== undefined) parts.push({m[1]}); else if (m[2]) parts.push(internal(m[2], m[3] || m[2])); else if (m[4]) { const href = m[5]; if (/^(https?:|mailto:)/i.test(href)) { parts.push({mdInline(m[4], ctx, k())}); } else if (MD_SLUG.test(href.replace(/\.md$/, '').split('/').pop())) { parts.push(internal(href.replace(/\.md$/, '').split('/').pop(), m[4])); } else parts.push(m[4]); } else if (m[6]) parts.push({mdInline(m[6], ctx, k())}); else if (m[7]) parts.push({m[7]}); last = m.index + m[0].length; } if (last < text.length) parts.push(text.slice(last)); return parts; } const splitRow = (line) => line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(c => c.trim()); function mdRender(md, ctx = {}) { const lines = String(md || '').replace(/\r/g, '').split('\n'); const out = []; let k = 0, i = 0; const key = () => 'b' + (k++); while (i < lines.length) { const raw = lines[i]; const line = raw.trim(); if (!line) { i++; continue; } // ```код``` if (line.startsWith('```')) { const buf = []; i++; while (i < lines.length && !lines[i].trim().startsWith('```')) { buf.push(lines[i]); i++; } i++; out.push(
{buf.join('\n')}
); continue; } // заголовки (# → h2: h1 страницы — заголовок материала) const h = line.match(/^(#{1,4})\s+(.*)$/); if (h) { const lvl = Math.min(4, Math.max(2, h[1].length)); out.push(React.createElement('h' + lvl, { key: key() }, mdInline(h[2], ctx, 'h' + k))); i++; continue; } if (/^(-{3,}|\*{3,}|_{3,})$/.test(line)) { out.push(
); i++; continue; } // таблица: строка заголовков + |---| if (line.startsWith('|') && i + 1 < lines.length && /^\|?\s*:?-{2,}/.test(lines[i + 1].trim())) { const head = splitRow(line); i += 2; const rows = []; while (i < lines.length && lines[i].trim().startsWith('|')) { rows.push(splitRow(lines[i])); i++; } const kb = 't' + k; out.push(
{head.map((c, j) => )}{rows.map((r, ri) => {head.map((_, j) => )})}
{mdInline(c, ctx, kb + 'h' + j)}
{mdInline(r[j] || '', ctx, kb + ri + '-' + j)}
); continue; } // > цитата if (line.startsWith('>')) { const buf = []; while (i < lines.length && lines[i].trim().startsWith('>')) { buf.push(lines[i].trim().replace(/^>\s?/, '')); i++; } out.push(
{mdRender(buf.join('\n'), ctx)}
); continue; } // списки (вложенность по отступу — сдвигом пункта) if (/^([-*+]|\d+[.)])\s+/.test(line)) { const ordered = /^\d/.test(line); const items = []; while (i < lines.length) { const l = lines[i]; const mm = l.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/); if (mm) { items.push({ depth: Math.min(3, Math.floor(mm[1].replace(/\t/g, ' ').length / 2)), text: mm[3] }); i++; continue; } // продолжение пункта на следующей строке с отступом if (l.trim() && /^\s{2,}/.test(l) && items.length) { items[items.length - 1].text += ' ' + l.trim(); i++; continue; } break; } const kb = 'l' + k; out.push(React.createElement(ordered ? 'ol' : 'ul', { key: key() }, items.map((it, j) =>
  • {mdInline(it.text, ctx, kb + j)}
  • ))); continue; } // абзац const buf = []; while (i < lines.length && lines[i].trim() && !/^(#{1,4}\s|```|>|\||([-*+]|\d+[.)])\s)/.test(lines[i].trim()) && !/^(-{3,}|\*{3,})$/.test(lines[i].trim())) { buf.push(lines[i].trim()); i++; } if (!buf.length) { buf.push(line); i++; } out.push(

    {mdInline(buf.join(' '), ctx, 'p' + k)}

    ); } return out; } window.mdRender = mdRender;