/
deka
/
buffer
Обзор
Документация
Войти
/
deka
/
buffer
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
dev
src/converters/layout-renderer.js
237 строк
12 KB
Maksim Ratnikov
fix: emit the wiki's own block serialization for paste
28 июл 2026, 15:46
28 июл 2026, 15:46
e62552e
Код
Авторство
О чём код?
import { escapeHtml, layoutId, layoutWidths } from "../util/format.js"; export function renderLayoutInlines(content) { if (Array.isArray(content)) { return content.map(renderLayoutInline).join(""); } return escapeHtml(content ?? ""); } export function renderLayoutInline(part) { if (typeof part === "string") { return escapeHtml(part); } switch (part.type) { case "text": return escapeHtml(part.text ?? ""); case "strong": return `<strong>${renderLayoutInlines(part.content ?? part.text ?? "")}</strong>`; case "em": return `<em>${renderLayoutInlines(part.content ?? part.text ?? "")}</em>`; case "strike": return `<s>${renderLayoutInlines(part.content ?? part.text ?? "")}</s>`; case "code": return `<code data-set="inline">${escapeHtml(part.text ?? "")}</code>`; case "br": return "<br>"; case "status": return renderLayoutStatus(part); case "span": return `<span${part.style ? ` style="${escapeHtml(part.style)}"` : ""}>${renderLayoutInlines(part.content ?? part.text ?? "")}</span>`; case "hint": return `<span class="hint_jjms4" data-type="hint" data-testid="wikiUnitFormEditor.Hint" contenteditable="true">${escapeHtml(part.text ?? "")}</span>`; default: throw new Error(`Unsupported inline type: ${part.type}`); } } export function renderLayoutParagraph(content, { slice = false } = {}) { const sliceAttr = slice ? ' data-pm-slice="0 0 []"' : ""; return `<p data-id="${layoutId()}" data-indent="0"${sliceAttr} style="text-align: justify; margin-left: 0px !important;">${content}</p>`; } export function renderLayoutParagraphText(text, options) { return renderLayoutParagraph(escapeHtml(text), options); } export function renderLayoutStatus(status) { const title = status.title ?? ""; const color = status.color ?? "yellow"; return `<span data-id="${layoutId()}" title="${escapeHtml(title)}" color="${escapeHtml(color)}" data-type="status"><span>[Статус: ${escapeHtml(title)} (${escapeHtml(color)})]</span></span>`; } export function renderLayoutTableOfContent(block = {}) { const title = block.title ?? "Содержание"; return `<nav data-id="${layoutId()}" title="${escapeHtml(title)}" minlevel="${escapeHtml(block.minlevel ?? 1)}" maxlevel="${escapeHtml(block.maxlevel ?? 6)}" class="toctainer_geoc4" data-type="tableOfContent"> <span class="toctitle_geoc4"> ${escapeHtml(title)} </span> <span class="tocmessage_geoc4"> Узел документа "Содержание" не может быть сгенерирован при рендере контента в HTML </span> </nav>`; } export function layoutInlineContent(item) { if (item?.inlines) { return renderLayoutInlines(item.inlines); } return escapeHtml(item?.text ?? item ?? ""); } export function renderLayoutCellContent(cell) { if (cell === null || cell === undefined) { return ""; } if (typeof cell === "string") { return escapeHtml(cell); } if (Array.isArray(cell)) { return renderLayoutInlines(cell); } if (cell.inlines) { return renderLayoutInlines(cell.inlines); } if (cell.text !== undefined) { return escapeHtml(cell.text); } if (cell.status) { return renderLayoutStatus(cell.status); } throw new Error("Unsupported table cell format"); } export function layoutCellStyle(hex) { const colors = { "#ffffff": "background-color: rgb(255, 255, 255);", "#FFBDAD": "background-color: rgb(255, 189, 173);", "#ABF5D1": "background-color: rgb(171, 245, 209);", "#F4F5F7": "background-color: rgb(244, 245, 247);" }; return colors[hex] ?? colors["#ffffff"]; } export function renderLayoutTable(block) { const headers = block.headers ?? []; const rows = block.rows ?? []; const widths = layoutWidths(block.columnwidths, headers.length); const headerRow = `<tr data-id="${layoutId()}">${headers.map((header) => { return `<th data-id="${layoutId()}" colspan="1" rowspan="1" backgroundcolor="var(--editor-highlight-bgc)"><span>${renderLayoutParagraphText(header)}</span></th>`; }).join("")}</tr>`; const bodyRows = rows.map((row, rowIndex) => { const cells = row.map((cell) => { const bg = typeof cell === "object" && !Array.isArray(cell) && cell.bg ? cell.bg : "#ffffff"; return `<td data-id="${layoutId()}" colspan="1" rowspan="1" data-bgc="${escapeHtml(bg)}" class="numbered-column-td" style="${escapeHtml(layoutCellStyle(bg))}">${renderLayoutParagraph(renderLayoutCellContent(cell))}</td>`; }).join(""); return `<tr data-id="${layoutId()}" data-original-row-index="${rowIndex}">${cells}</tr>`; }).join(""); return `<section data-id="${layoutId()}" data-table-id="${layoutId()}" columnwidths="${escapeHtml(widths)}" numberedactive="${block.numberedactive ? "true" : "false"}" data-type="table"><table><tbody>${headerRow}${bodyRows}</tbody></table></section>`; } export function renderLayoutBlocks(blocks) { return blocks.map(renderLayoutBlock).join(""); } export function renderLayoutBlock(block) { switch (block.type) { case "heading": { const level = block.level ?? 1; if (level < 1 || level > 6) { throw new Error(`Unsupported heading level: ${level}`); } const sliceAttr = block.slice ? ' data-pm-slice="0 0 []"' : ""; // The wiki anchors headings by `id`, and it always equals `data-id`. const headingId = layoutId(); return `<h${level} data-id="${headingId}" data-indent="0" id="${headingId}"${sliceAttr} style="text-align: justify; margin-left: 0px !important;">${escapeHtml(block.text ?? "")}</h${level}>`; } case "toc": case "tableOfContent": return renderLayoutTableOfContent(block); case "paragraph": return renderLayoutParagraph(block.inlines ? renderLayoutInlines(block.inlines) : escapeHtml(block.text ?? ""), { slice: block.slice }); case "status": return renderLayoutParagraph(renderLayoutStatus(block), { slice: block.slice }); case "hint": return renderLayoutParagraph(renderLayoutInline({ type: "hint", text: block.text ?? "" }), { slice: block.slice }); case "panel": case "info": { const panelType = block.panelType ?? "info"; const title = block.title ?? ""; const showtitle = block.showtitle === false ? "false" : "true"; const showicon = block.showicon === false ? "false" : "true"; const background = block.custombackground ?? "gray"; const borderStyle = block.customborderstyle ?? "none"; const borderWidth = block.customborderwidth ?? "1"; const borderColor = block.custombordercolor ?? "gray"; // The wiki serializes a panel as two divs: a text descriptor of the node, // then the content. Mirror it — its own copy round-trips through paste. const descriptor = `[Информационная панель: тип - ${escapeHtml(panelType)}, заголовок - ${escapeHtml(title)}, показывать заголовок - ${showtitle}, показывать иконку - ${showicon}. Атрибуты для произвольного типа: цвет фона - ${escapeHtml(background)}, стиль границы - ${escapeHtml(borderStyle)}, толщина границы - ${escapeHtml(borderWidth)}, цвет границы - ${escapeHtml(borderColor)}]`; return `<div data-id="${layoutId()}" type="${escapeHtml(panelType)}" title="${escapeHtml(title)}" showtitle="${showtitle}" showicon="${showicon}" custombackground="${escapeHtml(background)}" customborderstyle="${escapeHtml(borderStyle)}" customborderwidth="${escapeHtml(borderWidth)}" custombordercolor="${escapeHtml(borderColor)}" data-type="info"><div>${descriptor}</div><div>${renderLayoutBlocks(block.blocks ?? [])}</div></div>`; } case "plantuml": { const language = block.language ?? "uml"; const codeLanguage = block.codeLanguage ?? "java"; return `<div class="plantContainer_1hv20" data-type="plantuml" data-id="${layoutId()}" plantumlid="${layoutId()}" plantumltitle="${escapeHtml(block.title ?? "")}" plantumllanguage="${escapeHtml(language)}" language="${escapeHtml(codeLanguage)}"><pre><code>${escapeHtml(block.code ?? "")}</code></pre></div>`; } case "include": { const classes = ["wrapper_1cxbv"]; if (block.bordered !== false) { classes.push("bordered_1cxbv"); } return `<div data-id="${layoutId()}" fragmentsearchstrategy="${escapeHtml(block.fragmentsearchstrategy ?? "by-fragment-id")}" class="${classes.join(" ")}" data-type="include">${escapeHtml(block.text ?? block.title ?? "Включение()")}</div>`; } case "tabs": return `<div data-id="${layoutId()}" class="tabsWrapper_d6kqf" data-type="tabs">${(block.tabs ?? []).map((tab) => { const label = tab.label ?? tab.title ?? ""; return `<div data-id="${layoutId()}" label="${escapeHtml(label)}" class="tabWrapper_z4u1j" data-type="tab"><div class="tabTitle_z4u1j">${escapeHtml(label)}</div><div>${renderLayoutBlocks(tab.blocks ?? [])}</div></div>`; }).join("")}</div>`; case "expand": { const openClass = block.open === false ? "" : " is-open"; return `<div class="expand${openClass}" data-id="${layoutId()}"><button type="button"></button><div><summary class="expand-summary">${escapeHtml(block.title ?? "")}</summary><div class="expand-content" data-type="expandContent">${renderLayoutBlocks(block.blocks ?? [])}</div></div></div>`; } case "table": return renderLayoutTable(block); case "taskList": return `<ul data-id="${layoutId()}" data-type="taskList" style="text-align: justify; margin-left: 0px !important;">${(block.items ?? []).map((item) => { const checked = typeof item === "object" ? Boolean(item.checked) : false; const text = typeof item === "object" ? item.text ?? "" : item; const checkedAttr = checked ? ' checked=""' : ""; return `<li data-id="${layoutId()}" data-checked="${checked ? "true" : "false"}" 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>${renderLayoutParagraphText(text)}</div></li>`; }).join("")}</ul>`; case "orderedList": { const sliceAttr = block.slice ? ' data-pm-slice="0 0 []"' : ""; return `<ol class="orderedList_14jqc" data-id="${layoutId()}"${sliceAttr}>${(block.items ?? []).map((item) => { const itemObj = typeof item === "object" && item !== null ? item : { text: item }; const children = itemObj.children ? renderLayoutBlock({ type: "orderedList", items: itemObj.children }) : ""; return `<li class="listItem_14jqc" data-id="${layoutId()}">${renderLayoutParagraph(layoutInlineContent(itemObj))}${children}</li>`; }).join("")}</ol>`; } case "uiButton": { const title = block.title ?? block.label ?? ""; const url = block.url ?? ""; const attrs = [ `data-id="${layoutId()}"`, `title="${escapeHtml(title)}"`, `tooltip="${escapeHtml(block.tooltip ?? url)}"`, `url="${escapeHtml(url)}"`, `color="${escapeHtml(block.color ?? "Background.surfaceSecondary")}"`, `size="${escapeHtml(block.size ?? "md")}"`, block.icon ? `icon="${escapeHtml(block.icon)}"` : "", `display="${escapeHtml(block.display ?? "")}"`, `newwindow="${block.newwindow === false ? "false" : "true"}"`, 'data-type="uiButton"' ].filter(Boolean).join(" "); return `<div ${attrs}><button>${escapeHtml(title)}</button></div>`; } case "blocksGrid": { const columns = block.columns ?? []; if (columns.length > 3) { throw new Error(`blocksGrid supports at most 3 columns, got ${columns.length}`); } const gridId = layoutId(); const template = block.template ?? columns.map(() => "minmax(0, 1fr)").join(" "); return `<section data-id="${layoutId()}" blockscount="${columns.length}" blocksgridid="${gridId}" data-type="blocksGrid" class="grid_1orlh overlap_1orlh" style="--blocksgrid-template: ${escapeHtml(template)};">${columns.map((blocks) => { return `<div data-id="${layoutId()}" blocksgridid="${gridId}" data-type="column" class="column_yu7m7"><div class="content_yu7m7">${renderLayoutBlocks(blocks)}</div></div>`; }).join("")}</section>`; } case "code": return `<pre data-set="highlighted-code" data-id="${layoutId()}"><code>${escapeHtml(block.code ?? "")}</code></pre>`; case "hr": return "<hr>"; default: throw new Error(`Unsupported block type: ${block.type}`); } }