You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
82 lines
2.3 KiB
82 lines
2.3 KiB
|
5 months ago
|
import {
|
||
|
|
getInlineText,
|
||
|
|
type WordsBlock,
|
||
|
|
type WordsDocument,
|
||
|
|
type WordsInline,
|
||
|
|
type WordsMark
|
||
|
|
} from './document';
|
||
|
|
import { sanitizeWordsUrl } from './normalize';
|
||
|
|
|
||
|
|
const MARK_TAGS = {
|
||
|
|
bold: 'strong',
|
||
|
|
italic: 'em',
|
||
|
|
underline: 'u',
|
||
|
|
strike: 's',
|
||
|
|
code: 'code'
|
||
|
|
} as const satisfies Record<WordsMark, string>;
|
||
|
|
|
||
|
|
export function serializeWordsHtml(document: WordsDocument): string {
|
||
|
|
return document.children.map(serializeBlockHtml).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
function serializeBlockHtml(block: WordsBlock): string {
|
||
|
|
if (block.type === 'list') {
|
||
|
|
const tag = block.kind === 'ordered' ? 'ol' : 'ul';
|
||
|
|
const children = block.children
|
||
|
|
.map((item) => {
|
||
|
|
const checkbox =
|
||
|
|
block.kind === 'check'
|
||
|
|
? `<input type="checkbox"${item.checked === true ? ' checked' : ''} disabled> `
|
||
|
|
: '';
|
||
|
|
return `<li>${checkbox}${serializeInlinesHtml(item.children)}</li>`;
|
||
|
|
})
|
||
|
|
.join('');
|
||
|
|
return `<${tag}>${children}</${tag}>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (block.type === 'heading') {
|
||
|
|
return `<h${block.level}>${serializeInlinesHtml(block.children)}</h${block.level}>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (block.type === 'quote') {
|
||
|
|
return `<blockquote>${serializeInlinesHtml(block.children)}</blockquote>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
return `<p>${serializeInlinesHtml(block.children)}</p>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function serializeInlinesHtml(inlines: readonly WordsInline[]): string {
|
||
|
|
return inlines.map(serializeInlineHtml).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
function serializeInlineHtml(inline: WordsInline): string {
|
||
|
|
if (inline.type === 'link') {
|
||
|
|
const href = sanitizeWordsUrl(inline.href);
|
||
|
|
if (!href) return escapeHtml(inline.children.map(getInlineText).join(''));
|
||
|
|
const title = inline.title ? ` title="${escapeHtmlAttr(inline.title)}"` : '';
|
||
|
|
return `<a href="${escapeHtmlAttr(href)}"${title}>${serializeInlinesHtml(inline.children)}</a>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
return wrapMarks(escapeHtml(inline.text), inline.marks ?? []);
|
||
|
|
}
|
||
|
|
|
||
|
|
function wrapMarks(value: string, marks: readonly WordsMark[]): string {
|
||
|
|
return marks.reduce((html, mark) => {
|
||
|
|
const tag = MARK_TAGS[mark];
|
||
|
|
return `<${tag}>${html}</${tag}>`;
|
||
|
|
}, value);
|
||
|
|
}
|
||
|
|
|
||
|
|
function escapeHtml(value: string): string {
|
||
|
|
return value
|
||
|
|
.replace(/&/g, '&')
|
||
|
|
.replace(/</g, '<')
|
||
|
|
.replace(/>/g, '>')
|
||
|
|
.replace(/"/g, '"')
|
||
|
|
.replace(/'/g, ''');
|
||
|
|
}
|
||
|
|
|
||
|
|
function escapeHtmlAttr(value: string): string {
|
||
|
|
return escapeHtml(value).replace(/`/g, '`');
|
||
|
|
}
|