/
kan64
/
spreadsheet-lab
Обзор
Документация
Войти
/
kan64
/
spreadsheet-lab
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
client/src/utils/richTextUtils.ts
198 строк
6 KB
Anton Kravchenkov
fix(ui): Formatted text display
25 мар 2026, 00:21
25 мар 2026, 00:21
a355a4d
Код
Авторство
О чём код?
import type { TextRun } from '../types'; export function textRunsToPlainText(runs: TextRun[]): string { return runs.map((r) => r.text).join(''); } export function textRunsToHtml(runs: TextRun[]): string { if (runs.length === 0) return ''; return runs .map((run) => { let escaped = run.text .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/\n/g, '<br>'); const styles: string[] = []; if (run.bold) styles.push('font-weight:bold'); if (run.italic) styles.push('font-style:italic'); const decorations: string[] = []; if (run.underline) decorations.push('underline'); if (run.strikethrough) decorations.push('line-through'); if (decorations.length > 0) styles.push(`text-decoration:${decorations.join(' ')}`); if (run.color) styles.push(`color:${run.color}`); if (run.fontSize) styles.push(`font-size:${run.fontSize}px`); const styleAttr = styles.length > 0 ? ` style="${styles.join(';')}"` : ''; let html = `<span${styleAttr}>${escaped}</span>`; if (run.link) { const linkStyles = [...styles.filter((s) => !s.startsWith('color:') && !s.startsWith('text-decoration:'))]; linkStyles.push('color:#1a73e8'); linkStyles.push('text-decoration:underline'); const linkStyleAttr = ` style="${linkStyles.join(';')}"`; html = `<a href="${run.link.replace(/"/g, '"')}"${linkStyleAttr}>${escaped}</a>`; } return html; }) .join(''); } interface RunProps { bold: boolean; italic: boolean; underline: boolean; strikethrough: boolean; color: string | undefined; fontSize: number | undefined; link: string | undefined; } function getNodeProps(node: Node, root?: Node): RunProps { const props: RunProps = { bold: false, italic: false, underline: false, strikethrough: false, color: undefined, fontSize: undefined, link: undefined }; let current: Node | null = node; while (current && current !== root && current.nodeType === Node.ELEMENT_NODE) { const el = current as HTMLElement; const tag = el.tagName.toLowerCase(); if (tag === 'b' || tag === 'strong') props.bold = true; if (tag === 'i' || tag === 'em') props.italic = true; if (tag === 'u') props.underline = true; if (tag === 'del' || tag === 's' || tag === 'strike') props.strikethrough = true; if (tag === 'a') props.link = (el as HTMLAnchorElement).href || undefined; if (tag === 'font') { const fontColor = el.getAttribute('color'); if (fontColor) props.color = fontColor; } const style = el.style; if (style.fontWeight === 'bold' || parseInt(style.fontWeight) >= 700) props.bold = true; if (style.fontStyle === 'italic') props.italic = true; if (style.textDecoration?.includes('underline')) props.underline = true; if (style.textDecoration?.includes('line-through')) props.strikethrough = true; if (style.color) props.color = style.color; if (style.fontSize) { const fs = parseInt(style.fontSize, 10); if (fs > 0) props.fontSize = fs; } current = el.parentElement; } return props; } function propsEqual(a: RunProps, b: RunProps): boolean { return a.bold === b.bold && a.italic === b.italic && a.underline === b.underline && a.strikethrough === b.strikethrough && a.color === b.color && a.fontSize === b.fontSize && a.link === b.link; } function propsToRun(text: string, props: RunProps): TextRun { const run: TextRun = { text }; if (props.bold) run.bold = true; if (props.italic) run.italic = true; if (props.underline) run.underline = true; if (props.strikethrough) run.strikethrough = true; if (props.color) run.color = props.color; if (props.fontSize) run.fontSize = props.fontSize; if (props.link) run.link = props.link; return run; } function walkNode(node: Node, runs: TextRun[], root: Node): void { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent || ''; if (text.length === 0) return; const props = getNodeProps(node.parentElement!, root); const last = runs.length > 0 ? runs[runs.length - 1] : null; const lastProps = last ? { bold: !!last.bold, italic: !!last.italic, underline: !!last.underline, strikethrough: !!last.strikethrough, color: last.color, fontSize: last.fontSize, link: last.link } : null; if (last && lastProps && propsEqual(props, lastProps)) { last.text += text; } else { runs.push(propsToRun(text, props)); } return; } if (node.nodeType === Node.ELEMENT_NODE) { const el = node as HTMLElement; const tag = el.tagName.toLowerCase(); if (tag === 'br') { const last = runs.length > 0 ? runs[runs.length - 1] : null; if (last) { last.text += '\n'; } else { runs.push({ text: '\n' }); } return; } if (tag === 'div' || tag === 'p') { if (runs.length > 0) { const last = runs[runs.length - 1]; if (!last.text.endsWith('\n')) { last.text += '\n'; } } } for (const child of Array.from(node.childNodes)) { walkNode(child, runs, root); } return; } } export function htmlToTextRuns(element: HTMLElement): TextRun[] { const runs: TextRun[] = []; for (const child of Array.from(element.childNodes)) { walkNode(child, runs, element); } if (runs.length > 0) { const last = runs[runs.length - 1]; if (last.text.endsWith('\n')) { last.text = last.text.slice(0, -1); } } return runs.filter((r) => r.text.length > 0); } export function sanitizeRuns(runs: TextRun[]): TextRun[] { if (runs.length === 0) return []; const result: TextRun[] = [{ ...runs[0] }]; for (let i = 1; i < runs.length; i++) { const prev = result[result.length - 1]; const curr = runs[i]; if ( !!prev.bold === !!curr.bold && !!prev.italic === !!curr.italic && !!prev.underline === !!curr.underline && !!prev.strikethrough === !!curr.strikethrough && prev.color === curr.color && prev.fontSize === curr.fontSize && prev.link === curr.link ) { prev.text += curr.text; } else { result.push({ ...curr }); } } return result.filter((r) => r.text.length > 0); } export function isPlainText(runs: TextRun[]): boolean { return runs.every((r) => !r.bold && !r.italic && !r.underline && !r.strikethrough && !r.color && !r.fontSize && !r.link && !r.text.includes('\n')); }