Markdown

渲染 Markdown 内容,支持语法感知样式,以及可选的 Tree-sitter 代码块高亮。

基本用法

Renderable API

import { MarkdownRenderable, SyntaxStyle, RGBA, createCliRenderer } from "@opentui/core"

const renderer = await createCliRenderer()

const syntaxStyle = SyntaxStyle.fromStyles({
  "markup.heading.1": { fg: RGBA.fromHex("#58A6FF"), bold: true },
  "markup.list": { fg: RGBA.fromHex("#FF7B72") },
  "markup.raw": { fg: RGBA.fromHex("#A5D6FF") },
  default: { fg: RGBA.fromHex("#E6EDF3") },
})

const markdown = new MarkdownRenderable(renderer, {
  id: "readme",
  width: 60,
  content: "# Hello\n\n- One\n- Two\n\n```ts\nconst x = 1\n```",
  syntaxStyle,
})

renderer.root.add(markdown)

代码围栏语言标准化

代码围栏的 info 字符串会在 Tree-sitter 高亮之前进行标准化处理。

  • tsx -> typescriptreact
  • .jsx -> javascriptreact
  • TSX title=Button.tsx -> typescriptreact
  • Dockerfile -> dockerfile

标准化使用 infoStringToFiletype() 实现。你可以在运行时扩展或覆盖映射关系:

import { extensionToFiletype, basenameToFiletype } from "@opentui/core"

extensionToFiletype.set("templ", "html")
basenameToFiletype.set("mytoolrc", "yaml")

内容隐藏

concealtrue 时,隐藏 Markdown 标记符号(反引号、强调标记等):

const markdown = new MarkdownRenderable(renderer, {
  content: "**bold** and `code`",
  syntaxStyle,
  conceal: true,
})

使用 concealCode 可单独控制代码围栏内部的标记隐藏行为(默认为 false)。

流式更新

启用流式模式以进行增量更新。在追加内容块时保持 true,完成时设置 markdown.streaming = false 以完成末尾块的解析。表格包含尾部不完整的行,缺失的单元格将渲染为空。

const markdown = new MarkdownRenderable(renderer, {
  content: "",
  syntaxStyle,
  streaming: true,
})

markdown.content += "# Live log\n"
markdown.content += "- line 1\n"
markdown.streaming = false

稳定块前缀

parseMarkdownIncremental 会报告流头部有多少个已解析的 token 是稳定的——即追加更多内容后不太可能发生变化的部分。Renderable 将其暴露为顶层渲染块的数量,你可以在不稳定尾部继续更新的同时安全地将这些块提交到其他地方。

使用方式:以 internalBlockMode: "top-level" 进行渲染。Renderable 会将每个顶层 Markdown 块(标题、段落、列表、表格、代码围栏)作为独立的子 Renderable,并暴露 markdown._stableBlockCount——当前树头部已稳定的块数量。这与 ScrollbackSurface 逐行提交流式输出时的结构相匹配。

import { MarkdownRenderable, RGBA, SyntaxStyle } from "@opentui/core"

const syntaxStyle = SyntaxStyle.fromStyles({
  default: { fg: RGBA.fromHex("#E6EDF3") },
})

const md = new MarkdownRenderable(renderer, {
  content: "",
  syntaxStyle,
  streaming: true,
  internalBlockMode: "top-level",
})

md.content = "# Title\n\nPara 1"
// md._stableBlockCount 可能为 1 —— "# Title" 已稳定,"Para 1" 仍可能增长

md.content = "# Title\n\nPara 1\n\nPara 2"
// 随着前面的块被空行封闭,md._stableBlockCount 递增

internalBlockMode 是一个实验性的可选标志,用于驱动内置的回滚流式演示。对于非流式渲染,请保持默认值("coalesced"),它会将相邻块合并在一起,并保持历史布局行为。

Markdown 表格

Markdown 表格支持通过 tableOptions 配置样式、尺寸、换行、边框和选择功能。

const markdown = new MarkdownRenderable(renderer, {
  content: "| Service | Status |\n| --- | --- |\n| api | ok |",
  syntaxStyle,
  tableOptions: {
    style: "grid",
    widthMode: "full",
    columnFitter: "balanced",
    wrapMode: "word",
    cellPadding: 1,
    borders: true,
    outerBorder: true,
    borderStyle: "rounded",
    borderColor: "#6b7280",
    selectable: true,
  },
})

tableOptions

选项类型默认值描述
style"grid" | "columns"取决于 block mode视觉预设(参见表格样式
widthMode"content" | "full"取决于 style"full" 将列扩展至填满可用宽度
columnFitter"proportional" | "balanced""proportional"空间不足时列的缩放方式
wrapMode"none" | "char" | "word""word"表格单元格内的换行模式
cellPaddingnumber0每个单元格四周的内边距
bordersboolean取决于 style启用内边框和外边框
outerBorderbooleanborders覆盖外边框的可见性
borderStyleBorderStyle"single"表格边框字符集
borderColorColorInput隐藏前景色或 #888888Markdown 表格的边框颜色
selectablebooleantrue启用表格单元格文本选择

表格样式

tableOptions.style 选择一个预设,同时调整 bordersouterBorderwidthMode 的默认值:

  • "grid":带可见边框的表格。默认值为 borders: truewidthMode: "full"。这是标准的 Markdown 盒装表格渲染方式。
  • **"columns":无边框的分栏布局,栏间距为 2 个字符列,默认值为 widthMode: "content"。适用于追加式输出场景,避免全宽网格带来的视觉负担。

如果你不传入 style,当 internalBlockMode"top-level" 时默认使用 "columns",否则使用 "grid"。你仍然可以覆盖单个字段(例如 borders: true)来调整为不同的外观。

自定义节点渲染

覆盖特定 token 的渲染逻辑,并回退到默认渲染:

const markdown = new MarkdownRenderable(renderer, {
  content: "# Title\n\nHello",
  syntaxStyle,
  renderNode: (token, context) => {
    if (token.type === "heading") {
      return context.defaultRender()
    }
    return undefined
  },
})

自定义代码围栏语言

当你只想替换特定的代码围栏语言时,可以使用 createMarkdownCodeBlockRenderer。语言键会与标准化后的围栏 info 字符串进行匹配,因此 tsx 围栏会映射到 typescriptreact,自定义 DSL 名称(如 taskflow)也可以直接匹配。

import { BoxRenderable, MarkdownRenderable, TextRenderable, createMarkdownCodeBlockRenderer } from "@opentui/core"

const renderTaskFlow = (renderer) => (token) => {
  const steps = token.text
    .split("\n")
    .filter((line) => line.startsWith("step "))
    .map((line) => line.slice("step ".length))

  const card = new BoxRenderable(renderer, {
    border: true,
    borderStyle: "rounded",
    borderColor: "#38BDF8",
    paddingX: 1,
    flexDirection: "column",
    width: "100%",
  })

  for (const step of steps) {
    card.add(new TextRenderable(renderer, { content: `- ${step}`, width: "100%" }))
  }

  return card
}

const markdown = new MarkdownRenderable(renderer, {
  content: "```taskflow\nstep Parse markdown done\nstep Render widget active\n```",
  syntaxStyle,
  renderNode: createMarkdownCodeBlockRenderer({
    taskflow: renderTaskFlow(renderer),
  }),
})

Construct API

暂未提供。目前请使用 MarkdownRenderable

属性

属性类型默认值描述
contentstring""Markdown 源内容
syntaxStyleSyntaxStyle必填token 的样式定义
fgColorInput-基础前景色(传递到内部代码块)
bgColorInput-基础背景色(传递到内部代码块)
concealbooleantrue隐藏 Markdown 文本中的标记符号
concealCodebooleanfalse隐藏代码围栏内部的标记符号
streamingbooleanfalse增量模式;设置为 false 以完成解析
tableOptionsMarkdownTableOptions-Markdown 表格渲染选项
internalBlockMode"coalesced" | "top-level""coalesced"实验性功能:将顶层块暴露为独立的 Renderable
treeSitterClientTreeSitterClient-用于代码块的自定义 Tree-sitter 客户端
renderNode(token: Token, context: RenderNodeContext) => Renderable | null | undefined-每个 Markdown 块的自定义渲染钩子