@stackoverfloweth/prefect-design
Version:
A collection of low-level Vue components.
231 lines (182 loc) • 6.96 kB
text/typescript
import { marked } from 'marked'
import { VNode, h, createTextVNode as t } from 'vue'
import { PCheckbox, PCode, PCodeHighlight, PDivider, PLink, PSanitizeHtml, PHashLink, PTable } from '@/components'
import { isSupportedLanguage } from '@/types/codeHighlight'
import {
Token,
ParserOptions,
VNodeChildren,
hasChildren,
isCodeBlock,
isCodeSpan,
isTable,
isList,
isListItem,
isHeading,
isLink,
isImage,
isBlockquote,
isHtml,
isSpace,
isBreak,
isHorizontalRule,
isTextToken,
isEscape
} from '@/types/markdownRenderer'
import { ColumnClassesMethod } from '@/types/tables'
import { randomId } from '@/utilities'
import { unescapeHtml } from '@/utilities/strings'
const baseElement = 'div'
const baseClass = 'markdown-renderer'
const defaultHeadingClasses = ['text-4xl', 'text-3xl', 'text-2xl', 'text-lg', 'text-base', 'text-sm']
const mapChildTokens = (tokens: Token[], options: ParserOptions): VNodeChildren => tokens.flatMap((token) => getVNode(token, options))
const getVNode = (token: Token, options: ParserOptions): VNode | VNode[] => {
const { headingClasses = defaultHeadingClasses } = options
let children: VNodeChildren = []
if (hasChildren(token)) {
children = mapChildTokens(token.tokens, options)
}
const props = { class: [`${baseClass}__token`] }
const { type } = token
if (isEscape(token)) {
return t(unescapeHtml(token.text))
}
if (isTextToken(token)) {
// This is because text tokens can have embedded elements
// like links, images, etc. and text nodes can't have children
if (children.length) {
const classList = [`${baseClass}__text`, `${baseClass}__text--${type}`]
return h(baseElement, { class: classList }, children)
}
return t(unescapeHtml(token.text))
}
if (isSpace(token) || isBreak(token)) {
const classList = [`${baseClass}__space`]
return h(baseElement, { class: classList }, children)
}
if (isHorizontalRule(token)) {
const classList = [`${baseClass}__divider`]
return h(PDivider, { class: classList })
}
if (isImage(token)) {
const { href, text, title } = token
const classList = [`${baseClass}__image`]
return h('img', { src: href, alt: text, class: classList, title })
}
if (isHtml(token)) {
const { text } = token
return h(PSanitizeHtml, { html: text, class: [`${baseClass}__html`] })
}
if (isCodeBlock(token)) {
return getCodeVNode(token)
}
if (isCodeSpan(token)) {
const classList = [`${baseClass}__codespan`]
return h(PCode, { inline: true, class: classList }, { default: () => unescapeHtml(token.text) })
}
if (isTable(token)) {
return getTableVNode(token, options)
}
if (isList(token)) {
return getListVNode(token, options, children)
}
if (isListItem(token)) {
const { task, checked } = token
if (task || typeof checked === 'boolean') {
return getCheckboxVNode(token)
}
const classList = [`${baseClass}__list-item`]
const node = h('li', { class: classList }, children)
return node
}
if (isBlockquote(token)) {
const classList = [`${baseClass}__blockquote`]
return h('blockquote', { class: classList }, children)
}
if (isHeading(token)) {
const { depth, text } = token
const classList = [headingClasses[depth], `${baseClass}__heading`, `${baseClass}__heading--h${depth}`]
const heading = h(PHashLink, { hash: text, depth, class: [...classList, `${baseClass}__heading-wrapper`] }, { default: () => children })
if (depth < 2) {
return [heading, h(PDivider)]
}
return heading
}
if (isLink(token)) {
const { href, title } = token
const classList = [`${baseClass}__link`]
return h(PLink, { to: href, title, class: classList, rel: 'noopener' }, { default: () => token.text })
}
return h(baseElement, props, children)
}
const getTableVNode = (token: Token & { type: 'table' }, options: ParserOptions): VNode => {
const { header, align, rows } = token
const classList = [`${baseClass}__table`]
type TableDataValue = Record<string, unknown> & { _markdownMetadata: { text: string, tokens: Token[] } }
type TableData = Record<string, TableDataValue>
const data: TableData[] = []
const columns: string[] = []
const slots: Record<string, unknown> = {}
header.forEach(({ tokens }, index) => {
const slotName = randomId()
columns.push(slotName)
const headerChildren = mapChildTokens(tokens, options)
const classList = [`${baseClass}__table-heading`]
const alignValue = align[index]
if (alignValue) {
classList.push(`${baseClass}__table-column--${alignValue}`)
}
slots[`${slotName}-heading`] = () => h(baseElement, { class: classList }, headerChildren)
})
rows.forEach((row) => {
const rowData: TableData = {}
row.forEach(({ text, tokens }, i) => {
const slotName = randomId()
rowData[columns[i]] = { [slotName]: text, _markdownMetadata: { text, tokens } }
})
data.push(rowData)
})
columns.forEach((column) => {
slots[column] = ({ value }: { value: TableDataValue }) => {
const { _markdownMetadata: { tokens } } = value
const cellChildren = mapChildTokens(tokens, options)
if (!cellChildren.length) {
return h(baseElement, { class: [`${baseClass}__table-cell`] }, { default: () => value[column] })
}
return cellChildren
}
})
const columnClasses: ColumnClassesMethod<TableData> = (column, value, index) => {
const alignValue = align[index]
const classList = [`${baseClass}__table-column`]
if (alignValue) {
classList.push(`${baseClass}__table-column--${alignValue}`)
}
return classList
}
return h(PTable, { class: classList, data, columnClasses }, slots)
}
const getCheckboxVNode = (token: marked.Tokens.ListItem): VNode => {
const { text, checked } = token
const classList = [`${baseClass}__checkbox`]
return h(PCheckbox, { modelValue: checked, disabled: true, label: text, checked, class: classList })
}
const getListVNode = (token: marked.Tokens.List, options: ParserOptions, children: VNodeChildren = []): VNode => {
const { ordered, items } = token
const base = ordered ? 'ol' : 'ul'
const classList = [`${baseClass}__list`, `${baseClass}__list--${ordered ? 'ordered' : 'unordered'}`]
const listItems = mapChildTokens(items, options)
return h(base, { class: classList }, [...children, ...listItems])
}
const getCodeVNode = (token: marked.Tokens.Code): VNode => {
const classList = [`${baseClass}__code`]
const { text, lang } = token
if (isSupportedLanguage(lang)) {
return h(PCodeHighlight, { text, lang, class: classList })
}
return h(PCode, { class: classList }, { default: () => unescapeHtml(text) })
}
export const getRootVNode = (tokens: marked.TokensList | [], options: ParserOptions): VNode => {
const children = mapChildTokens(tokens, options)
return h('article', { class: [baseClass] }, children)
}