Skip to content

Latest commit

 

History

History
490 lines (343 loc) · 15.2 KB

File metadata and controls

490 lines (343 loc) · 15.2 KB

stream-markdown-parser

NPM version English Docs NPM downloads Bundle size License

纯 JavaScript Markdown 解析器和渲染工具,支持流式处理 - 框架无关。

该包包含从 markstream-vue 中提取的核心 Markdown 解析逻辑,使其可以在任何 JavaScript/TypeScript 项目中使用,无需 Vue 依赖。

特性

  • 🚀 纯 JavaScript - 无框架依赖
  • 📦 轻量级 - 最小打包体积
  • 🔧 可扩展 - 基于插件的架构
  • 🎯 类型安全 - 完整的 TypeScript 支持
  • 高性能 - 性能优化
  • 🌊 流式友好 - 支持渐进式解析

ℹ️ 自当前版本起我们基于 markdown-it-ts(一个 TypeScript 优先的 markdown-it 发行版)进行构建。API 与 markdown-it 保持一致,但内部仅依赖其解析流程,并提供更丰富的 token 类型定义。

文档

完整的使用说明与集成教程见 markstream-vue 文档站:

本 README 聚焦解析器 API;如需 VitePress/Vite/Nuxt 集成、Worker 流式解析、Tailwind/UnoCSS 配置等指南,请查阅上述文档。

安装

pnpm add stream-markdown-parser
#
npm install stream-markdown-parser
#
yarn add stream-markdown-parser

快速 API 速览

  • getMarkdown(msgId?, options?) — 返回一个预配置的 markdown-it-ts 实例;支持 pluginapplyi18n 等选项(内置任务列表、上下标、数学等插件)。
  • registerMarkdownPlugin(plugin) / clearRegisteredMarkdownPlugins() — 全局注册/清除插件,在所有 getMarkdown() 调用中生效(适合特性开关或测试环境)。
  • parseMarkdownToStructure(markdown, md, parseOptions) — 将 Markdown 转换为可供 markstream-vue 等渲染器使用的 AST。
  • processTokens(tokens) / parseInlineTokens(children, content?, preToken?, options?) — 更底层的 token → 节点工具,方便自定义管线。
  • applyMathapplyContainersnormalizeStandaloneBackslashTfindMatchingClose 等 — 用于构建自定义解析、lint 或内容清洗流程。

使用

流式解析流程

Markdown 字符串
   ↓ getMarkdown() → 带插件的 markdown-it-ts 实例
parseMarkdownToStructure(markdown, md) → AST (ParsedNode[])
   ↓ 交给你的渲染器(markstream-vue、自定义 UI、Worker 等)

多次解析时复用同一个 md 实例可以避免重复注册插件。与 markstream-vue 一起使用时,你可以把 AST 传给 <MarkdownRender :nodes="nodes" />,或仅传入原始 content 并共享同一套解析配置。

增量 / 流式示例

处理 AI/SSE 流时,可以复用同一个 md 实例不停地对累积缓冲区解析,并把 AST 推送给 UI(例如 markstream-vue):

import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'

const md = getMarkdown()
let buffer = ''

async function handleChunk(chunk: string) {
  buffer += chunk
  const nodes = parseMarkdownToStructure(buffer, md)
  postMessage({ type: 'markdown:update', nodes })
}

在前端通过 <MarkdownRender :nodes="nodes" /> 渲染即可避免重复解析。具体串联示例见文档用法指南

基础示例

import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'

// 创建一个带有默认插件的 markdown-it-ts 实例
const md = getMarkdown()

// 将 Markdown 解析为流式友好的 AST 结构
const nodes = parseMarkdownToStructure('# Hello World', md)
console.log(nodes)
// [{ type: 'heading', level: 1, children: [...] }]

// 如果仍需 HTML 输出,markdown-it-ts 依旧提供 render()
const html = md.render?.('# Hello World\n\nThis is **bold**.')

配置数学公式选项

import { getMarkdown, setDefaultMathOptions } from 'stream-markdown-parser'

// 设置全局数学公式选项
setDefaultMathOptions({
  commands: ['infty', 'perp', 'alpha'],
  escapeExclamation: true
})

const md = getMarkdown()

自定义国际化

import { getMarkdown } from 'stream-markdown-parser'

// 使用翻译映射
const md = getMarkdown('editor-1', {
  i18n: {
    'common.copy': '复制',
  }
})

// 或使用翻译函数
const md = getMarkdown('editor-1', {
  i18n: (key: string) => translateFunction(key)
})

使用插件

import customPlugin from 'markdown-it-custom-plugin'
import { getMarkdown } from 'stream-markdown-parser'

const md = getMarkdown('editor-1', {
  plugin: [
    [customPlugin, { /* 选项 */ }]
  ]
})

高级:自定义规则

import { getMarkdown } from 'stream-markdown-parser'

const md = getMarkdown('editor-1', {
  apply: [
    (md) => {
      // 添加自定义内联规则
      md.inline.ruler.before('emphasis', 'custom', (state, silent) => {
        // 你的自定义逻辑
        return false
      })
    }
  ]
})

全局扩展

想在所有 getMarkdown() 实例上启用同一个插件,而无需修改调用点?可使用内置 helper:

import {
  clearRegisteredMarkdownPlugins,
  registerMarkdownPlugin,
} from 'stream-markdown-parser'

registerMarkdownPlugin(myPlugin)

const md = getMarkdown()
// 现在 md 会自动包含 myPlugin

// 测试或清理阶段:
clearRegisteredMarkdownPlugins()
  • plugin 选项 → 针对单次 getMarkdown 调用传入 md.use(...)
  • apply 选项 → 直接操作实例(如 md.inline.ruler.before(...))。如果需要严格模式,可自行包裹 try/catch;默认会打印错误保持兼容。
  • registerMarkdownPlugin → 全局注册表,适用于 SSR / Worker 等场景统一开启功能。

API

主要函数

getMarkdown(msgId?, options?)

创建一个配置好的 markdown-it-ts 实例(与 markdown-it API 兼容)。

参数:

  • msgId (string, 可选): 该实例的唯一标识符。默认值:editor-${Date.now()}
  • options (GetMarkdownOptions, 可选): 配置选项

选项:

interface GetMarkdownOptions {
  // 要使用的 markdown-it / markdown-it-ts 插件数组
  plugin?: Array<Plugin | [Plugin, any]>

  // 修改 md 实例的函数数组
  apply?: Array<(md: MarkdownIt) => void>

  // 翻译函数或翻译映射
  i18n?: ((key: string) => string) | Record<string, string>
}

parseMarkdownToStructure(content, md, options?)

将 Markdown 内容解析为结构化节点树。

参数:

  • content (string): 要解析的 Markdown 内容
  • md (MarkdownItCore): 由 getMarkdown() 创建的 markdown-it-ts 实例
  • options (ParseOptions, 可选): 带有钩子的解析选项

返回值: ParsedNode[] - 解析后的节点数组

processTokens(tokens)

将原始 markdown-it tokens 处理为扁平数组。

parseInlineTokens(tokens, content?, preToken?, options?)

解析内联 markdown-it-ts tokens 并产出节点。可传入原始 content(来自父 token)、可选的前一个 token,以及 inline 解析选项(requireClosingStrongcustomHtmlTagsvalidateLink)。

配置函数

setDefaultMathOptions(options)

设置全局数学公式渲染选项。

参数:

  • options (MathOptions): 数学公式配置选项
interface MathOptions {
  commands?: readonly string[] // 要转义的 LaTeX 命令
  escapeExclamation?: boolean // 转义独立的 '!' (默认: true)
}

解析钩子(精细化变换)

ParseOptions 支持以下钩子与标志:

interface ParseOptions {
  preTransformTokens?: (tokens: Token[]) => Token[]
  postTransformTokens?: (tokens: Token[]) => Token[]
  // 自定义 HTML 类标签,作为自定义节点输出(如 ['thinking'])
  customHtmlTags?: string[]
  // 输出 link 节点前校验 href;返回 false 时降级为纯文本
  validateLink?: (url: string) => boolean
  // true 表示输入已结束(end-of-stream)
  final?: boolean
  // 解析 strong 时要求闭合 `**`(默认 false)
  requireClosingStrong?: boolean
}

示例 —— 标记 AI "思考" 块:

const parseOptions = {
  customHtmlTags: ['thinking'],
}

const nodes = parseMarkdownToStructure(markdown, md, parseOptions)
const tagged = nodes.map(node =>
  node.type === 'html_block' && /<thinking>/.test((node as any).content ?? '')
    ? { ...node, meta: { type: 'thinking' } }
    : node,
)

在渲染器中读取 node.meta 即可渲染自定义 UI,而无需直接修改 Markdown 文本。

示例 —— 限制链接协议,拦截不安全链接:

const md = getMarkdown('safe-links')
md.set?.({
  validateLink: (url: string) => !/^\s*javascript:/i.test(url.trim()),
})

const nodes = parseMarkdownToStructure(
  '[ok](https://example.com) [bad](javascript:alert(1))',
  md,
  { final: true },
)
// "ok" 保持为 link 节点;"bad" 会降级为纯文本

未知 HTML 类标签

默认情况下,非标准的 HTML 类标签(例如 <question>)在完整闭合时会按原生 HTML 渲染(作为自定义元素输出);流式场景下未闭合的片段会先按纯文本处理以避免闪烁。若希望它们作为自定义节点输出(type: 'question',携带 attrs/content),需要在 customHtmlTags 中显式声明。

工具函数

isMathLike(content)

启发式函数,用于检测内容是否类似数学符号。

参数:

  • content (string): 要检查的内容

返回值: boolean

findMatchingClose(src, startIdx, open, close)

在字符串中查找匹配的闭合分隔符,处理嵌套对。

参数:

  • src (string): 源字符串
  • startIdx (number): 开始搜索的索引
  • open (string): 开启分隔符
  • close (string): 闭合分隔符

返回值: number - 匹配闭合的索引,如果未找到则返回 -1

使用建议与排障

  • 复用解析实例:缓存 getMarkdown() 的结果,避免重复注册插件。
  • 服务端解析:在服务端运行 parseMarkdownToStructure 后把 AST 下发给客户端,配合 markstream-vue 实现确定性输出。
  • 自定义 HTML 组件:在解析前先把 <MyWidget> 这类片段替换为占位符,渲染时再注入,避免在 html_block 上进行脆弱的字符串操作。
  • 样式提示:如果将节点交给 markstream-vue,务必按照文档的 CSS 排查清单 调整 reset / layer,防止 Tailwind/UnoCSS 覆盖样式。
  • 错误处理apply 钩子内部默认捕获异常后打印日志,如需在 CI/生产中抛出错误,可在传入前自行封装并 rethrow。

parseFenceToken(token)

将代码围栏 token 解析为 CodeBlockNode。

参数:

  • token (MarkdownToken): markdown-it token

返回值: CodeBlockNode

normalizeStandaloneBackslashT(content, options?)

规范化数学内容中的反斜杠-t 序列。

参数:

  • content (string): 要规范化的内容
  • options (MathOptions, 可选): 数学选项

返回值: string

低阶辅助函数

需要更细粒度地控制 token → AST 流程时,可直接使用以下导出:

import type { MarkdownToken } from 'stream-markdown-parser'
import {

  parseInlineTokens,
  processTokens
} from 'stream-markdown-parser'

const tokens: MarkdownToken[] = md.parse(markdown, {})
const nodes = processTokens(tokens)
// 或仅解析内联内容:
const inlineNodes = parseInlineTokens(tokens[0].children ?? [], tokens[0].content ?? '')

processTokensparseMarkdownToStructure 内部使用的同一个转换器,可在自定义管线中复用,避免重复实现 Markdown-it 遍历。

插件函数

applyMath(md, options?)

将数学插件应用到 markdown-it 实例。

参数:

  • md (MarkdownIt): markdown-it 实例
  • options (MathOptions, 可选): 数学渲染选项

applyContainers(md)

将容器插件应用到 markdown-it 实例。

参数:

  • md (MarkdownIt): markdown-it 实例

常量

KATEX_COMMANDS

用于转义的常用 KaTeX 命令数组。

TEX_BRACE_COMMANDS

使用大括号的 TeX 命令数组。

ESCAPED_TEX_BRACE_COMMANDS

用于正则表达式的 TEX_BRACE_COMMANDS 转义版本。

类型

所有 TypeScript 类型都已导出:

import type {
  // 节点类型
  CodeBlockNode,
  GetMarkdownOptions,
  HeadingNode,
  ListItemNode,
  ListNode,
  MathOptions,
  ParagraphNode,
  ParsedNode,
  ParseOptions,
  // ... 更多
} from 'stream-markdown-parser'

节点类型

解析器导出各种表示不同 Markdown 元素的节点类型:

  • TextNode, HeadingNode, ParagraphNode
  • ListNode, ListItemNode
  • CodeBlockNode, InlineCodeNode
  • LinkNode, ImageNode
  • BlockquoteNode, TableNode
  • MathBlockNode, MathInlineNode
  • 以及更多...

默认插件

该包预配置了以下 markdown-it 插件:

  • markdown-it-sub - 下标支持(H~2~O
  • markdown-it-sup - 上标支持(x^2^
  • markdown-it-mark - 高亮/标记支持(==highlighted==
  • markdown-it-task-checkbox - 任务列表支持(- [ ] Todo
  • markdown-it-ins - 插入标签支持(++inserted++
  • markdown-it-footnote - 脚注支持
  • markdown-it-container - 自定义容器支持(::: warning, ::: tip 等)
  • 数学公式支持 - 使用 $...$$$...$$ 渲染 LaTeX 数学公式

框架集成

虽然该包与框架无关,但它被设计为可以无缝配合以下框架使用:

  • Node.js - 服务器端渲染
  • Vue 3 - 配合 markstream-vue(或你的自定义渲染层)使用
  • React - 使用解析的节点进行自定义渲染
  • Vanilla JS - 直接 HTML 渲染
  • 任何框架 - 解析为 AST 并按需渲染

markstream-vue 迁移(解析器导出)

如果你当前是从 markstream-vue 引入解析器相关 helper,可以切换为使用独立包:

// 之前
import { getMarkdown } from 'markstream-vue'

// 现在
import { getMarkdown } from 'stream-markdown-parser'

所有 API 保持不变。详见迁移指南

性能

  • 轻量级: ~65KB 压缩后(13KB gzipped)
  • 快速: 针对实时解析优化
  • Tree-shakeable: 只导入你需要的部分
  • 依赖很少: markdown-it-ts + 少量 markdown-it 插件

贡献

欢迎提交 Issues 和 PRs!请阅读贡献指南

许可证

MIT © Simon He

相关项目