import type { MarkedExtension, Tokens } from 'marked' export interface AlertOptions { className?: string variants?: AlertVariantItem[] withoutStyle?: boolean } export interface AlertVariantItem { type: string icon: string title?: string titleClassName?: string } function ucfirst(str: string) { return str.slice(0, 1).toUpperCase() + str.slice(1).toLowerCase() } /** * https://github.com/bent10/marked-extensions/tree/main/packages/alert * To support theme, we need to modify the source code. * A [marked](https://marked.js.org/) extension to support [GFM alerts](https://github.com/orgs/community/discussions/16925). */ export function markedAlert(options: AlertOptions = {}): MarkedExtension { const { className = `markdown-alert`, variants = [], withoutStyle = false } = options const resolvedVariants = resolveVariants(variants) // 提取公共的元数据构建逻辑 function buildMeta(variantType: string, matchedVariant: AlertVariantItem, fromContainer = false) { return { className, variant: variantType, icon: matchedVariant.icon, title: matchedVariant.title ?? ucfirst(variantType), titleClassName: `${className}-title`, fromContainer, } } // 提取公共的渲染逻辑 function renderAlert(token: any) { const { meta, tokens = [] } = token // @ts-expect-error marked renderer context has parser property const text = this.parser.parse(tokens) // 新主题系统:使用 CSS 选择器而非内联样式 let tmpl = `
\n` tmpl += `

` if (!withoutStyle) { // 给 SVG 添加 class,通过 CSS 控制颜色 tmpl += meta.icon.replace( `\n` tmpl += text tmpl += `

\n` return tmpl } return { walkTokens(token) { if (token.type !== `blockquote`) return const matchedVariant = resolvedVariants.find(({ type }) => new RegExp(createSyntaxPattern(type), `i`).test(token.text), ) if (matchedVariant) { const { type: variantType } = matchedVariant const typeRegexp = new RegExp(createSyntaxPattern(variantType), `i`) Object.assign(token, { type: `alert`, meta: buildMeta(variantType, matchedVariant), }) const firstLine = token.tokens?.[0] as Tokens.Paragraph const firstLineText = firstLine.raw?.replace(typeRegexp, ``).trim() if (firstLineText) { const patternToken = firstLine.tokens[0] as Tokens.Text Object.assign(patternToken, { raw: patternToken.raw.replace(typeRegexp, ``), text: patternToken.text.replace(typeRegexp, ``), }) if (firstLine.tokens[1]?.type === `br`) { firstLine.tokens.splice(1, 1) } } else { token.tokens?.shift() } } }, extensions: [ { name: `alert`, level: `block`, renderer: renderAlert, }, { name: `alertContainer`, level: `block`, start(src) { return src.match(/^:::/)?.index }, tokenizer(src, _tokens) { // eslint-disable-next-line regexp/no-super-linear-backtracking const match = /^:::\s*(\w+)\s*\n([\s\S]*?)\n:::/.exec(src) if (match) { const [raw, variant, content] = match const matchedVariant = resolvedVariants.find(v => v.type === variant) if (!matchedVariant) return return { type: `alert`, raw, text: content.trim(), tokens: this.lexer.blockTokens(content.trim()), meta: buildMeta(variant, matchedVariant, true), } } }, renderer: renderAlert, }, ], } } /** * The default configuration for alert variants. */ const defaultAlertVariant: AlertVariantItem[] = [ { type: `note`, icon: ``, }, { type: `info`, icon: ``, }, { type: `tip`, icon: ``, }, { type: `important`, icon: ``, }, { type: `warning`, icon: ``, }, { type: `caution`, icon: ``, }, // Obsidian-style callouts { type: `abstract`, title: `Abstract`, icon: ``, }, { type: `summary`, title: `Summary`, icon: ``, }, { type: `tldr`, title: `TL;DR`, icon: ``, }, { type: `todo`, title: `Todo`, icon: ``, }, { type: `success`, title: `Success`, icon: ``, }, { type: `done`, title: `Done`, icon: ``, }, { type: `question`, title: `Question`, icon: ``, }, { type: `help`, title: `Help`, icon: ``, }, { type: `faq`, title: `FAQ`, icon: ``, }, { type: `failure`, title: `Failure`, icon: ``, }, { type: `fail`, title: `Fail`, icon: ``, }, { type: `missing`, title: `Missing`, icon: ``, }, { type: `danger`, title: `Danger`, icon: ``, }, { type: `error`, title: `Error`, icon: ``, }, { type: `bug`, title: `Bug`, icon: ``, }, { type: `example`, title: `Example`, icon: ``, }, { type: `quote`, title: `Quote`, icon: ``, }, { type: `cite`, title: `Cite`, icon: ``, }, ] /** * Resolves the variants configuration, combining the provided variants with * the default variants. */ export function resolveVariants(variants: AlertVariantItem[]) { if (!variants.length) return defaultAlertVariant return Object.values( [...defaultAlertVariant, ...variants].reduce( (map, item) => { map[item.type] = item return map }, {} as { [key: string]: AlertVariantItem }, ), ) } /** * Returns regex pattern to match alert syntax. */ export function createSyntaxPattern(type: string) { return `^(?:\\[!${type}])\\s*?\n*` }