/
deka
/
buffer
Обзор
Документация
Войти
/
deka
/
buffer
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
dev
src/converters/markdown-to-wiki.js
318 строк
10 KB
Maksim Ratnikov
Extract markdown-to-wiki converter into src/
08 май 2026, 16:24
08 май 2026, 16:24
feea842
Код
Авторство
О чём код?
import { escapeHtml, layoutId, layoutWidths } from "../util/format.js"; export function mdInlineToWiki(text, depth = 0) { if (text === "" || text == null) return ""; if (depth > 64) return escapeHtml(String(text)); const out = []; let i = 0; const src = String(text); function emitText(value) { out.push(escapeHtml(value)); } function readDelim(delim) { let close = src.indexOf(delim, i + delim.length); if (close < 0) return null; const ch = delim[0]; while (close + delim.length < src.length && src[close + delim.length] === ch) { close += 1; } return { inner: src.slice(i + delim.length, close), end: close + delim.length }; } while (i < src.length) { const ch = src[i]; if (ch === "\\" && i + 1 < src.length) { const next = src[i + 1]; if ("\\*_`[]~".includes(next)) { emitText(next); i += 2; continue; } } if (ch === "`") { const close = src.indexOf("`", i + 1); if (close > i) { out.push(`<code data-set="inline">${escapeHtml(src.slice(i + 1, close))}</code>`); i = close + 1; continue; } } if (ch === "[") { const closeBracket = src.indexOf("]", i + 1); if (closeBracket > i && src[closeBracket + 1] === "(") { const closeParen = src.indexOf(")", closeBracket + 2); if (closeParen > closeBracket + 1) { const label = src.slice(i + 1, closeBracket); const rawUrl = src.slice(closeBracket + 2, closeParen); const safeUrl = /^\s*(?:javascript|data|vbscript):/i.test(rawUrl) ? "#" : rawUrl; out.push(`<a href="${escapeHtml(safeUrl)}">${mdInlineToWiki(label, depth + 1)}</a>`); i = closeParen + 1; continue; } } } if (ch === "*" || ch === "_") { const doubled = src.startsWith(ch + ch, i); if (doubled) { const result = readDelim(ch + ch); if (result) { out.push(`<strong>${mdInlineToWiki(result.inner, depth + 1)}</strong>`); i = result.end; continue; } } else { const result = readDelim(ch); if (result) { out.push(`<em>${mdInlineToWiki(result.inner, depth + 1)}</em>`); i = result.end; continue; } } } if (ch === "~" && src.startsWith("~~", i)) { const result = readDelim("~~"); if (result) { out.push(`<s>${mdInlineToWiki(result.inner, depth + 1)}</s>`); i = result.end; continue; } } emitText(ch); i += 1; } return out.join(""); } export function mdParaTag(content) { return `<p data-id="${layoutId()}" style="text-align: justify; margin-left: 0px !important;">${content}</p>`; } export function mdListItemMatch(line) { const bullet = line.match(/^(\s*)([-+*])\s+(.*)$/); if (bullet) { const indent = bullet[1].length; const taskMatch = bullet[3].match(/^\[( |x|X)\]\s+(.*)$/); return { kind: "ul", indent, marker: bullet[2], isTask: Boolean(taskMatch), checked: taskMatch ? taskMatch[1].toLowerCase() === "x" : false, text: taskMatch ? taskMatch[2] : bullet[3] }; } const ordered = line.match(/^(\s*)(\d+)\.\s+(.*)$/); if (ordered) { return { kind: "ol", indent: ordered[1].length, marker: ordered[2], isTask: false, checked: false, text: ordered[3] }; } return null; } export function mdGroupListItems(items) { const root = []; const stack = [{ indent: -1, items: root }]; for (const item of items) { while (stack.length > 1 && item.indent <= stack[stack.length - 1].indent) { stack.pop(); } const parent = stack[stack.length - 1]; const node = { ...item, children: [] }; parent.items.push(node); stack.push({ indent: item.indent, items: node.children }); } return root; } export function mdEmitTaskList(items) { const inner = items.map((item) => { const checkedAttr = item.checked ? ' checked=""' : ""; const dataChecked = item.checked ? "true" : "false"; const childrenHtml = item.children.length ? mdEmitListNodes(item.children) : ""; return `<li data-id="${layoutId()}" data-checked="${dataChecked}" data-type="taskItem" style="display: flex; align-items: flex-start; gap: 0.5rem;"><label style="display: flex; align-items: center; margin-top: 0.25rem; pointer-events: none;"><input type="checkbox" disabled=""${checkedAttr}><span></span></label><div>${mdParaTag(mdInlineToWiki(item.text))}${childrenHtml}</div></li>`; }).join(""); return `<ul data-id="${layoutId()}" data-type="taskList" style="text-align: justify; margin-left: 0px !important;">${inner}</ul>`; } export function mdEmitListNodes(items) { if (!items.length) return ""; const kind = items[0].kind; const allTask = kind === "ul" && items.every((item) => item.isTask); if (allTask) { return mdEmitTaskList(items); } const inner = items.map((item) => { const visibleText = item.isTask ? `[${item.checked ? "x" : " "}] ${item.text}` : item.text; const childrenHtml = item.children.length ? mdEmitListNodes(item.children) : ""; if (kind === "ol") { return `<li class="listItem_14jqc" data-id="${layoutId()}">${mdParaTag(mdInlineToWiki(visibleText))}${childrenHtml}</li>`; } return `<li data-id="${layoutId()}">${mdParaTag(mdInlineToWiki(visibleText))}${childrenHtml}</li>`; }).join(""); if (kind === "ol") { return `<ol class="orderedList_14jqc" data-id="${layoutId()}">${inner}</ol>`; } return `<ul data-id="${layoutId()}">${inner}</ul>`; } export function mdEmitTable(headerCells, bodyRows) { const colCount = Math.max(headerCells.length, ...bodyRows.map((row) => row.length)); const widths = layoutWidths(undefined, colCount); const tableId = layoutId(); const headerCellHtml = Array.from({ length: colCount }, (_, idx) => { const text = headerCells[idx] ?? ""; return `<th data-id="${layoutId()}" colspan="1" rowspan="1" backgroundcolor="var(--editor-highlight-bgc)"><span>${mdParaTag(mdInlineToWiki(text))}</span></th>`; }).join(""); const bodyHtml = bodyRows.map((row, rowIdx) => { const cellsHtml = Array.from({ length: colCount }, (_, idx) => { const text = row[idx] ?? ""; return `<td data-id="${layoutId()}" colspan="1" rowspan="1" data-bgc="#ffffff" style="background-color: rgb(255, 255, 255);">${mdParaTag(mdInlineToWiki(text))}</td>`; }).join(""); return `<tr data-id="${layoutId()}" data-original-row-index="${rowIdx}">${cellsHtml}</tr>`; }).join(""); return `<section data-id="${layoutId()}" data-table-id="${tableId}" columnwidths="${widths}" numberedactive="false" data-type="table"><table><tbody><tr data-id="${layoutId()}">${headerCellHtml}</tr>${bodyHtml}</tbody></table></section>`; } export function mdSplitTableRow(line) { const trimmed = line.replace(/^\s*\|/, "").replace(/\|\s*$/, ""); const cells = []; let current = ""; let escaped = false; for (const ch of trimmed) { if (escaped) { current += ch; escaped = false; continue; } if (ch === "\\") { escaped = true; continue; } if (ch === "|") { cells.push(current.trim()); current = ""; continue; } current += ch; } cells.push(current.trim()); return cells; } export function mdParseBlocks(lines) { const out = []; let i = 0; while (i < lines.length) { const raw = lines[i]; if (!raw.trim()) { i += 1; continue; } const fence = raw.match(/^(\s*)(`{3,}|~{3,})(.*)$/); if (fence) { const closer = fence[2]; const buf = []; let j = i + 1; while (j < lines.length && !lines[j].trim().startsWith(closer)) { buf.push(lines[j]); j += 1; } out.push(`<pre data-set="highlighted-code" data-id="${layoutId()}"><code>${escapeHtml(buf.join("\n"))}</code></pre>`); i = j < lines.length ? j + 1 : j; continue; } const heading = raw.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/); if (heading) { const level = heading[1].length; out.push(`<h${level} data-id="${layoutId()}">${mdInlineToWiki(heading[2])}</h${level}>`); i += 1; continue; } if (/^\s{0,3}(-{3,}|\*{3,}|_{3,})\s*$/.test(raw)) { out.push("<hr>"); i += 1; continue; } if (/^\s*>/.test(raw)) { const buf = []; while (i < lines.length && /^\s*>/.test(lines[i])) { buf.push(lines[i].replace(/^\s*>\s?/, "")); i += 1; } out.push(`<blockquote data-id="${layoutId()}">${mdParseBlocks(buf)}</blockquote>`); continue; } if (raw.includes("|") && i + 1 < lines.length && /^\s*\|?\s*:?-{1,}/.test(lines[i + 1])) { const headerCells = mdSplitTableRow(raw); const delim = mdSplitTableRow(lines[i + 1]); if (delim.every((cell) => /^:?-{1,}:?$/.test(cell.trim()))) { const bodyRows = []; let j = i + 2; while (j < lines.length && lines[j].trim() && lines[j].includes("|")) { bodyRows.push(mdSplitTableRow(lines[j])); j += 1; } out.push(mdEmitTable(headerCells, bodyRows)); i = j; continue; } } const firstListItem = mdListItemMatch(raw); if (firstListItem) { const items = []; while (i < lines.length) { const item = mdListItemMatch(lines[i]); if (!item) break; items.push(item); i += 1; } const grouped = mdGroupListItems(items); out.push(mdEmitListNodes(grouped)); continue; } const paraBuf = []; while (i < lines.length && lines[i].trim() && !/^(`{3,}|~{3,}|#{1,6}\s|\s{0,3}(-{3,}|\*{3,}|_{3,})\s*$|\s*>|\s*[-+*]\s+|\s*\d+\.\s+)/.test(lines[i])) { paraBuf.push(lines[i].trim()); i += 1; } if (paraBuf.length) { out.push(mdParaTag(mdInlineToWiki(paraBuf.join(" ")))); } } return out.join("\n"); } export function markdownToWikiHtml(source) { if (source == null) return ""; const text = String(source); if (!text.trim()) return ""; const lines = text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").split("\n"); return mdParseBlocks(lines); }