diff --git a/src/markdown/code-block-yaml-tag.ts b/src/markdown/code-block-yaml-tag.ts new file mode 100644 index 0000000..4f57f04 --- /dev/null +++ b/src/markdown/code-block-yaml-tag.ts @@ -0,0 +1,62 @@ +import type { MarkedExtension } from "marked"; +import yaml from "js-yaml"; + +export default function markedCodeBlockYamlTag(): MarkedExtension { + return { + extensions: [ + { + name: "code-block-yaml-tag", + level: "block", + start(src: string) { + return src.match(/^```yaml\/tag\s*\n/m)?.index; + }, + tokenizer(src: string) { + const rule = /^```yaml\/tag\s*\n([\s\S]*?)\n```/; + const match = rule.exec(src); + if (match) { + const yamlContent = match[1]?.trim() || ""; + let props: Record = {}; + try { + props = + (yaml.load(yamlContent) as Record) || {}; + } catch (e) { + console.error("YAML Parse Error in code-block-yaml-tag:", e); + props = { error: "Invalid YAML content" }; + } + + const tagName = (props.tag as string) || "tag-unknown"; + const { tag, ...rest } = props; + + let content = ""; + if ("body" in rest) { + content = String(rest.body || ""); + delete (rest as Record).body; + } + + const propsStr = Object.entries(rest) + .map(([key, value]) => { + const strValue = String(value); + if (strValue.includes(" ") || strValue.includes('"')) { + return `${key}="${strValue.replace(/"/g, """)}"`; + } + return `${key}="${strValue}"`; + }) + .join(" "); + + return { + type: "code-block-yaml-tag", + raw: match[0], + tagName, + props: propsStr, + content, + }; + } + }, + renderer(token: any) { + const propsAttr = token.props ? ` ${token.props}` : ""; + return `<${token.tagName}${propsAttr}>${token.content || ""}\n`; + }, + }, + ], + }; +} diff --git a/src/markdown/index.ts b/src/markdown/index.ts index a2d752b..91db7a5 100644 --- a/src/markdown/index.ts +++ b/src/markdown/index.ts @@ -1,148 +1,103 @@ -import { Marked, type MarkedExtension } from 'marked'; -import {createDirectives, presetDirectiveConfigs} from 'marked-directive'; -import yaml from 'js-yaml'; +import { Marked, type MarkedExtension } from "marked"; +import { createDirectives, presetDirectiveConfigs } from "marked-directive"; import markedAlert from "marked-alert"; import markedMermaid from "./mermaid"; import markedTable from "./table"; -import {gfmHeadingId} from "marked-gfm-heading-id"; +import { gfmHeadingId } from "marked-gfm-heading-id"; import markedColumns from "./columns"; +import markedCodeBlockYamlTag from "./code-block-yaml-tag"; let globalIconPrefix: string | undefined = undefined; -function overrideIconPrefix(path?: string){ - globalIconPrefix = path; - return { - [Symbol.dispose](){ - globalIconPrefix = undefined; - } - } +function overrideIconPrefix(path?: string) { + globalIconPrefix = path; + return { + [Symbol.dispose]() { + globalIconPrefix = undefined; + }, + }; } // 使用 marked-directive 来支持指令语法 const marked = new Marked() - .use(gfmHeadingId()) - .use(markedAlert()) - .use(markedMermaid()) - .use(markedTable()) - .use(createDirectives([ - ...presetDirectiveConfigs, - { - marker: '::::', - level: 'container' - }, - { - marker: ':::::', - level: 'container' - }, - { - level: 'inline', - marker: ':', + .use(gfmHeadingId()) + .use(markedAlert()) + .use(markedMermaid()) + .use(markedTable()) + .use(markedCodeBlockYamlTag()) + .use( + createDirectives([ + ...presetDirectiveConfigs, + { + marker: "::::", + level: "container", + }, + { + marker: ":::::", + level: "container", + }, + { + level: "inline", + marker: ":", // :[icon] 或 :[icon.ext] 语法 // 支持的扩展名: .svg, .png, .gif, .jpg, .jpeg, .webp // 如果不指定扩展名,默认为 .png renderer(token) { - if (!token.meta.name) { - const iconText = token.text || ''; - - // 已知支持的图片扩展名 - const supportedExtensions = ['svg', 'png', 'gif', 'jpg', 'jpeg', 'webp']; - - // 检查是否包含扩展名(查找最后一个点) - let iconName = iconText; - let extension = 'png'; // 默认扩展名 - - const lastDotIndex = iconName.lastIndexOf('.'); - if (lastDotIndex > 0) { - const potentialExt = iconName.slice(lastDotIndex + 1).toLowerCase(); - if (supportedExtensions.includes(potentialExt)) { - extension = potentialExt; - iconName = iconName.slice(0, lastDotIndex); - } - } - - const label = token.attrs?.label as string | undefined; - const inner = label ? `${label}${label}` : ""; - const style = globalIconPrefix ? `style="--icon-src: url('${globalIconPrefix}/${iconName}.${extension}')"` : ''; - const iconHtml = `${inner}`; - - const repeat = parseInt(`${token.attrs?.repeat || ''}`); - const join = token.attrs?.join || ''; - const separator = join ? `<${join}>` : ''; - if(isNaN(repeat) || repeat < 1) return iconHtml; - - return Array(repeat).fill(iconHtml).join(separator); + if (!token.meta.name) { + const iconText = token.text || ""; + + // 已知支持的图片扩展名 + const supportedExtensions = [ + "svg", + "png", + "gif", + "jpg", + "jpeg", + "webp", + ]; + + // 检查是否包含扩展名(查找最后一个点) + let iconName = iconText; + let extension = "png"; // 默认扩展名 + + const lastDotIndex = iconName.lastIndexOf("."); + if (lastDotIndex > 0) { + const potentialExt = iconName + .slice(lastDotIndex + 1) + .toLowerCase(); + if (supportedExtensions.includes(potentialExt)) { + extension = potentialExt; + iconName = iconName.slice(0, lastDotIndex); + } } - return false; - } - }, -]), { - extensions: [ - ...markedColumns(), + + const label = token.attrs?.label as string | undefined; + const inner = label + ? `${label}${label}` + : ""; + const style = globalIconPrefix + ? `style="--icon-src: url('${globalIconPrefix}/${iconName}.${extension}')"` + : ""; + const iconHtml = `${inner}`; + + const repeat = parseInt(`${token.attrs?.repeat || ""}`); + const join = token.attrs?.join || ""; + const separator = join ? `<${join}>` : ""; + if (isNaN(repeat) || repeat < 1) return iconHtml; + + return Array(repeat).fill(iconHtml).join(separator); + } + return false; + }, + }, + ]), { - // 自定义代码块渲染器,支持 yaml/tag 格式 - name: 'code-block-yaml-tag', - level: 'block', - start(src: string) { - // 检测 ```yaml/tag 开头的代码块 - return src.match(/^```yaml\/tag\s*\n/m)?.index; - }, - tokenizer(src: string) { - const rule = /^```yaml\/tag\s*\n([\s\S]*?)\n```/; - const match = rule.exec(src); - if (match) { - const yamlContent = match[1]?.trim() || ''; - let props: Record = {}; - try { - props = (yaml.load(yamlContent) as Record) || {}; - } catch (e) { - console.error("YAML Parse Error in code-block-yaml-tag:", e); - props = { error: "Invalid YAML content" }; - } - - // 提取 tag 名称,默认为 tag-unknown - const tagName = (props.tag as string) || 'tag-unknown'; - - // 移除 tag 属性,剩下的作为 HTML 属性 - const { tag, ...rest } = props; - - // 提取 innerText 内容(如果有 body 字段) - let content = ''; - if ('body' in rest) { - content = String(rest.body || ''); - delete (rest as Record).body; - } - - // 构建属性字符串 - const propsStr = Object.entries(rest) - .map(([key, value]) => { - const strValue = String(value); - // 如果值包含空格或特殊字符,添加引号 - if (strValue.includes(' ') || strValue.includes('"')) { - return `${key}="${strValue.replace(/"/g, '"')}"`; - } - return `${key}="${strValue}"`; - }) - .join(' '); - - return { - type: 'code-block-yaml-tag', - raw: match[0], - tagName, - props: propsStr, - content - }; - } - }, - renderer(token: any) { - // 渲染为自定义 HTML 标签 - const propsAttr = token.props ? ` ${token.props}` : ''; - return `<${token.tagName}${propsAttr}>${token.content || ''}\n`; - } - }] -}); + extensions: [...markedColumns()], + }, + ); export function parseMarkdown(content: string, iconPrefix?: string): string { - using prefix = overrideIconPrefix(iconPrefix); - return marked.parse(content.trimStart()) as string; + using prefix = overrideIconPrefix(iconPrefix); + return marked.parse(content.trimStart()) as string; } export { marked };