\n` + 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*` +} diff --git a/skills/baoyu-markdown-to-html/scripts/md/extensions/footnotes.ts b/skills/baoyu-markdown-to-html/scripts/md/extensions/footnotes.ts new file mode 100644 index 0000000..5660b64 --- /dev/null +++ b/skills/baoyu-markdown-to-html/scripts/md/extensions/footnotes.ts @@ -0,0 +1,89 @@ +import type { MarkedExtension, Tokens } from 'marked' +/** + * A marked extension to support footnotes syntax. + * Syntax: + * This is a footnote reference[^1][^2]. + * + * [^1]: ..... + * [^2]: ..... + */ + +interface MapContent { + index: number + text: string +} +const fnMap = new Map
${index}.
+ ${text}
+ \u21A9\uFE0E
+ ${fnInner}` + } + if (index === fnMap.size) { + return `${fnInner}
` + } + return fnInner + }, + }, + { + name: `footnoteRef`, + level: `inline`, + start(src: string) { + return src.match(/\[\^/)?.index + }, + tokenizer(src: string) { + const match = src.match(/^\[\^(.*?)\]/) + if (match) { + const [raw, fnId] = match + if (fnMap.has(fnId)) { + return { + type: `footnoteRef`, + raw, + fnId, + } + } + } + }, + renderer(token: Tokens.Generic) { + const { fnId } = token + const { index } = fnMap.get(fnId) as MapContent + return ` + \[${index}\] + ` + }, + }, + ], + } +} diff --git a/skills/baoyu-markdown-to-html/scripts/md/extensions/index.ts b/skills/baoyu-markdown-to-html/scripts/md/extensions/index.ts new file mode 100644 index 0000000..dcf6c21 --- /dev/null +++ b/skills/baoyu-markdown-to-html/scripts/md/extensions/index.ts @@ -0,0 +1,10 @@ +// Markdown 扩展导出 +export * from './alert.js' +export * from './footnotes.js' +export * from './infographic.js' +export * from './katex.js' +export * from './markup.js' +export * from './plantuml.js' +export * from './ruby.js' +export * from './slider.js' +export * from './toc.js' diff --git a/skills/baoyu-markdown-to-html/scripts/md/extensions/infographic.ts b/skills/baoyu-markdown-to-html/scripts/md/extensions/infographic.ts new file mode 100644 index 0000000..47feedf --- /dev/null +++ b/skills/baoyu-markdown-to-html/scripts/md/extensions/infographic.ts @@ -0,0 +1,119 @@ +import type { MarkedExtension } from 'marked' + +interface InfographicOptions { + themeMode?: 'dark' | 'light' +} + +async function renderInfographic(containerId: string, code: string, options?: InfographicOptions) { + if (typeof window === 'undefined') + return + + try { + const { Infographic, setDefaultFont, setFontExtendFactor, exportToSVG } = await import('@antv/infographic') + + setFontExtendFactor(1.1) + setDefaultFont('-apple-system-font, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif') + + const findContainer = (retries = 5, delay = 100) => { + const container = document.getElementById(containerId) + if (container) { + const isDark = options?.themeMode === 'dark' + + // 从 CSS 变量中读取主题颜色 + const root = document.documentElement + const computedStyle = getComputedStyle(root) + const primaryColor = computedStyle.getPropertyValue('--md-primary-color').trim() + const backgroundColor = computedStyle.getPropertyValue('--background').trim() + + // 转换 HSL 格式 + const toHSLString = (variant: string) => { + const vars = variant.split(' ') + if (vars.length === 3) + return `hsl(${vars.join(', ')})` + if (vars.length === 4) + return `hsla(${vars.join(', ')})` + return '' + } + + const instance = new Infographic({ + container, + svg: { + style: { + width: '100%', + height: '100%', + background: isDark ? '#000' : 'transparent', + }, + background: false, + }, + theme: isDark ? 'dark' : 'default', + themeConfig: { + colorPrimary: primaryColor || undefined, + colorBg: toHSLString(backgroundColor) || undefined, + }, + }) + + instance.on('loaded', ({ node }) => { + exportToSVG(node, { removeIds: true }).then((svg) => { + container.replaceChildren(svg) + }) + }) + + instance.render(code) + + return + } + + if (retries > 0) { + setTimeout(() => findContainer(retries - 1, delay), delay) + } + } + + findContainer() + } + catch (error) { + console.error('Failed to render Infographic:', error) + const container = document.getElementById(containerId) + if (container) { + container.innerHTML = `