/
deka
/
buffer
Обзор
Документация
Войти
/
deka
/
buffer
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
dev
src/converters/board-text.js
178 строк
7 KB
Maksim Ratnikov
feat: доска возвращается из доски — обратный маппер в board-layout-v1
05 авг 2026, 12:05
05 авг 2026, 12:05
a462c74
Код
Авторство
О чём код?
const ENTITIES = { amp: "&", lt: "<", gt: ">", quot: "\"", "#39": "'", nbsp: " " }; const TAG_RE = /<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>/g; function decodeEntities(value) { return value.replace(/&(#?\w+);/g, (whole, name) => (name in ENTITIES ? ENTITIES[name] : whole)); } function escapeText(value) { return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); } export function parseNativeText(raw) { if (typeof raw !== "string") return { blocks: [] }; const source = raw.replace(//g, ""); const blocks = []; const marks = { bold: 0, italic: 0, strike: 0 }; let listType = null; let current = null; const flush = () => { if (current && current.spans.length) { const first = current.spans[0]; const last = current.spans[current.spans.length - 1]; first.text = first.text.replace(/^ +/, ""); last.text = last.text.replace(/ +$/, ""); const merged = []; for (const span of current.spans) { if (!span.text) continue; const previous = merged[merged.length - 1]; const sameMarks = previous && !!previous.bold === !!span.bold && !!previous.italic === !!span.italic && !!previous.strike === !!span.strike; if (sameMarks) previous.text += span.text; else merged.push(span); } if (merged.length) blocks.push({ ...current, spans: merged }); } current = null; }; const addText = (chunk) => { const text = decodeEntities(chunk).replace(/\s+/g, " "); if (!text) return; if (!current && !text.trim()) return; const span = { text }; if (marks.bold > 0) span.bold = true; if (marks.italic > 0) span.italic = true; if (marks.strike > 0) span.strike = true; if (!current) current = { type: listType || "p", spans: [] }; current.spans.push(span); }; const mark = (name, closing) => { marks[name] = Math.max(0, marks[name] + (closing ? -1 : 1)); }; TAG_RE.lastIndex = 0; let cursor = 0; let match; while ((match = TAG_RE.exec(source))) { addText(source.slice(cursor, match.index)); cursor = TAG_RE.lastIndex; const closing = match[0][1] === "/"; const tag = match[1].toLowerCase(); if (tag === "ul" || tag === "ol") { flush(); listType = closing ? null : tag; } else if (tag === "li" || tag === "p" || tag === "div" || tag === "br") { flush(); } else if (tag === "b" || tag === "strong") { mark("bold", closing); } else if (tag === "i" || tag === "em") { mark("italic", closing); } else if (tag === "s" || tag === "strike" || tag === "del") { mark("strike", closing); } } addText(source.slice(cursor)); flush(); return { blocks }; } export function renderNativeText(text) { const blocks = Array.isArray(text?.blocks) ? text.blocks : []; if (!blocks.length) return "<p></p>"; const out = []; let openList = null; for (const block of blocks) { const inner = (block.spans || []).map(spanToHtml).join(""); if (block.type === "ul" || block.type === "ol") { if (openList !== block.type) { if (openList) out.push(`</${openList}>`); out.push(`<${block.type}>`); openList = block.type; } out.push(`<li>${inner}</li>`); } else { if (openList) { out.push(`</${openList}>`); openList = null; } out.push(`<p>${inner}</p>`); } } if (openList) out.push(`</${openList}>`); return out.join(""); } function spanToHtml(span) { let html = escapeText(span.text ?? ""); if (span.strike) html = `<s>${html}</s>`; if (span.italic) html = `<i>${html}</i>`; if (span.bold) html = `<b>${html}</b>`; return html; } // Markdown-подмножество из board-layout-v1 в те же блоки, что даёт parseNativeText: // **жирный**, *курсив*, ~~зачёркнутый~~, списки "- " и "1. ", перенос строки. export function markdownToBlocks(text) { if (typeof text !== "string" || !text.trim()) return { blocks: [] }; const blocks = []; for (const rawLine of text.split(/\r?\n/)) { const line = rawLine.trim(); if (!line) continue; let type = "p"; let content = line; const bullet = line.match(/^[-*•]\s+(.*)$/); const ordered = line.match(/^\d+[.)]\s+(.*)$/); if (bullet) { type = "ul"; content = bullet[1]; } else if (ordered) { type = "ol"; content = ordered[1]; } const spans = markdownSpans(content); if (spans.length) blocks.push({ type, spans }); } return { blocks }; } const MD_TOKEN_RE = /(\*\*|__|~~|\*|_)/; function markdownSpans(text) { const marks = { bold: false, italic: false, strike: false }; const spans = []; let rest = text; const push = (chunk) => { if (!chunk) return; const span = { text: chunk }; if (marks.bold) span.bold = true; if (marks.italic) span.italic = true; if (marks.strike) span.strike = true; const previous = spans[spans.length - 1]; const sameMarks = previous && !!previous.bold === !!span.bold && !!previous.italic === !!span.italic && !!previous.strike === !!span.strike; if (sameMarks) previous.text += span.text; else spans.push(span); }; while (rest) { const match = rest.match(MD_TOKEN_RE); if (!match) { push(rest); break; } push(rest.slice(0, match.index)); const token = match[1]; if (token === "**" || token === "__") marks.bold = !marks.bold; else if (token === "~~") marks.strike = !marks.strike; else marks.italic = !marks.italic; rest = rest.slice(match.index + token.length); } if (spans.length) { spans[0].text = spans[0].text.replace(/^ +/, ""); spans[spans.length - 1].text = spans[spans.length - 1].text.replace(/ +$/, ""); } return spans.filter((span) => span.text); } // Сколько символов текста в блоке — нужно движку раскладки для оценки переносов. export function blocksTextLength(blocks) { let total = 0; for (const block of blocks?.blocks ?? blocks ?? []) { for (const span of block.spans ?? []) total += span.text.length; } return total; } // Обратно в markdown — нужно обратному мапперу, чтобы отдать LLM текст, // а не блоки. Пара к markdownToBlocks. export function blocksToMarkdown(text) { const blocks = text?.blocks ?? []; const lines = []; let ordinal = 0; for (const block of blocks) { const body = (block.spans ?? []).map(spanToMarkdown).join(""); if (block.type === "ul") { lines.push(`- ${body}`); ordinal = 0; } else if (block.type === "ol") { lines.push(`${++ordinal}. ${body}`); } else { lines.push(body); ordinal = 0; } } return lines.join("\n"); } function spanToMarkdown(span) { let out = span.text ?? ""; if (!out) return ""; if (span.strike) out = `~~${out}~~`; if (span.italic) out = `*${out}*`; if (span.bold) out = `**${out}**`; return out; }