handymd GitHub

API 参考

包名:@21stware/handymd

import {
  createEditor, HandyEditor,
  createShikiHighlighter,
  // …见下方完整导出
} from '@21stware/handymd'
import '@21stware/handymd/style.css'

createEditor(options) → HandyEditor

工厂函数,等价于 new HandyEditor(options)

HandyEditorOptions

字段类型默认说明
mountHTMLElement挂载点(会被加上 handymd class)
contentstring''初始 markdown;与 load 同时给时 load 优先
load`() => string \Promise<string>`异步拉取;失败 → phase=error
save`(md: string) => unknown \Promise<unknown>`提供后启用 L4 自动保存
autosave`Omit<AutosaveOptions, 'save' \'onStatusChange'>`见下防抖/退避等
readOnlybooleanfalse初始只读
highlight`CodeHighlighter \Promise<CodeHighlighter>`代码高亮
diagram`DiagramRenderer \Promise<DiagramRenderer>`diagram block(如 ```mermaid )渲染器;缺省时按普通代码块呈现
onOpenLink(href: string) => voidwindow.openConcealed 链接单击
onChange(md: string) => void每次 docChanged
onPhaseChange(phase: EditorPhase) => voidL1 阶段变化
onSaveStatusChange(status: SaveStatus, error?: unknown) => voidL4 状态变化
pluginsPlugin[][]追加自定义 ProseMirror 插件
historybooleantrue是否启用撤销重做
normalizeOrderedListsbooleantrue有序列表自动重编号

HandyEditor 实例

成员类型说明
view`EditorView \null`底层 ProseMirror 视图;loading/error/destroyed 时可能为 null
autosave`Autosave \null`未提供 save 时为 null
phaseEditorPhase`loading \ready \error \conflicted \destroyed`
saveStatusSaveStatus`clean \dirty \saving \retrying \offline`
readOnlyboolean当前只读态
loadErrorunknown最近一次加载错误
remoteConflict`string \null`冲突中的远端文本
getMarkdown()() => string序列化(无损)
setMarkdown(md, opts?)(string, { addToHistory?: boolean }) => void编程式替换
insertTable(opts?)(InsertTableOptions) => boolean编程式插入 GFM 表格
setReadOnly(v)(boolean) => void切换只读
focus()() => void聚焦
retry()() => voiderror → loading 重试加载
notifyRemote(md)(string) => void通知远端版本变化
resolveConflict(choice)`('local' \'remote') => void`解决冲突
flush()() => Promise<void>立即保存
destroy()() => Promise<void>flush 后销毁
on(event, handler)见下事件订阅,返回取消函数

事件

editor.on('phase', (phase: EditorPhase) => {})
editor.on('change', (markdown: string) => {})
editor.on('saveStatus', (status: SaveStatus) => {})

L4:Autosave

import { Autosave, type AutosaveOptions, type SaveStatus } from '@21stware/handymd'

const as = new Autosave(() => markdown, {
  save: async (md) => { /* PUT */ },
  debounceMs: 800,
  maxRetries: 5,
  backoffBaseMs: 500,
  backoffMaxMs: 30_000,
  listenOnline: true,
  onStatusChange: (status, error) => {},
})

as.markDirty()
as.markClean()
await as.flush()
as.retryNow()
as.destroy()
as.status  // SaveStatus
as.error

状态转移:clean → dirty → saving → clean | retrying → offline;保存期间再输入会在完成后立即再存。


L3:conceal 插件

import {
  concealPlugin, concealKey, setConcealMeta,
  isRevealed, revealSignature, buildBlockDecos,
  type ConcealState, type ConcealMeta, type ConcealOptions,
} from '@21stware/handymd'

const plugin = concealPlugin({
  readOnly: false,
  // 可选:diagram block 的渲染回调(见"图表渲染"一节)
  renderDiagram: createDiagramRenderCallback(createMermaidRenderer()),
})

// 投递配置迁移
view.dispatch(setConcealMeta(view.state.tr, { readOnly: true }))
view.dispatch(setConcealMeta(view.state.tr, { composing: true }))
view.dispatch(setConcealMeta(view.state.tr, { refresh: true }))

const st = concealKey.getState(view.state)
// st.blocks / st.set / st.composing / st.readOnly

isRevealed(el, selection, readOnly):pure hitTest。 buildBlockDecos(block, revealed[], ctx?):由元素+reveal 位生成 decoration;ctx.renderDiagram 控制 diagram block 的渲染。


L2:输入管线插件

import {
  imePlugin,                 // composition 冻结
  interactionsPlugin,        // 链接打开 / checkbox
  caretGuardPlugin,          // 隐藏前缀光标保护
  normalizePlugin,           // 有序列表重编号
  markdownKeymap,            // Enter / Backspace / Mod-b…
  continueListItem,
  toggleInline,
  indentListItem,
  dedentListItem,
  backspaceBlockFormat,
  arrowLeftSkipPrefix,
} from '@21stware/handymd'

interactionsPlugin({ onOpenLink: (href) => location.assign(href) })
toggleInline('**')  // Command

表格(编程式)

GFM 表格无输入触发;请用 editor.insertTable()insertTable command。

import {
  insertTable, buildTableMarkdown,
  goToNextTableCell, goToPrevTableCell, continueTableRow,
  parseTableRow, isTableSeparator,
  type InsertTableOptions,
} from '@21stware/handymd'

editor.insertTable({ rows: 3, cols: 3, headers: ['A', 'B', 'C'] })
buildTableMarkdown({ rows: 2, cols: 2 })
// => "|  |  |\n| --- | --- |\n|  |  |"

insertTable({ rows: 3, cols: 3 })(view.state, view.dispatch)

InsertTableOptionsrows?(含表头,默认 3)、cols?(默认 3)、withHeaderRow?(默认 true)、headers?


代码高亮

import {
  highlightPlugin, highlightKey, createShikiHighlighter,
  type CodeHighlighter, type HighlightSpan, type ShikiHighlighterOptions,
} from '@21stware/handymd'

type HighlightSpan = { text: string; color?: string }
type CodeHighlighter = (code: string, lang: string) => HighlightSpan[][] | Promise<HighlightSpan[][]>

const hl = await createShikiHighlighter({
  theme: 'github-light',
  langs: ['javascript', 'typescript', 'python', 'bash', 'json', 'html', 'css', 'markdown'],
})
highlightPlugin(hl)
// 或 highlightPlugin(createShikiHighlighter()) — 接受 Promise

图表渲染(diagram block)

```mermaid 围栏在结构化解析层就与普通代码块分开(diagramOpen / diagramLine / diagramClose),并遵循块级 Live Render 语义:光标离开围栏区域 → 源码整块隐藏、渲染为图表;光标进入(或点击图表)→ 回到围栏源码编辑,视觉与普通代码块一致。

import {
  createMermaidRenderer, createDiagramRenderCallback,
  type DiagramRenderer, type DiagramRenderCallback, type MermaidRendererOptions,
} from '@21stware/handymd'

// 渲染器契约:源码 → SVG/HTML 字符串(可异步;抛错 = 图表语法错误)
type DiagramRenderer = (code: string, lang: string) => string | Promise<string>

// 用 HandyEditor:直接传 diagram 选项(mermaid 为可选依赖,动态 import)
createEditor({ mount, diagram: createMermaidRenderer({ theme: 'neutral' }) })

// 自建 EditorView:包一层缓存回调再交给 concealPlugin
concealPlugin({ renderDiagram: createDiagramRenderCallback(createMermaidRenderer()) })

行为细节:

  • 渲染只发生在 Concealed 态(光标离开之后),编辑期间永远是源码 —— 不存在"边打字边重渲染"的抖动;
  • 结果按 (lang, code) 缓存,光标反复进出同一图表命中缓存、无闪烁;
  • 渲染失败显示错误信息(.hm-diagram-error),点击仍可进入源码修复;
  • 空围栏显示占位(.hm-diagram-empty),不会让块"消失";
  • 未配置渲染器时 diagram block 退化为普通代码块呈现(解析层仍然分类为 diagram)。

文档模型与解析

import {
  schema,
  markdownToDoc, docToMarkdown,
  parseInline, parseInlineCached,
  classifyLines, parseDoc,
  type LineInfo, type LineType, type BlockMeta,
  type ElementRange, type ElementKind, type ElementAttrs,
  type InlineKind, type BlockKind, type RelElement, type Span,
} from '@21stware/handymd'

markdownToDoc('# hi')           // Node
docToMarkdown(doc)              // string,无损
parseInline('**a** ==b==')      // RelElement[](相对坐标)
classifyLines(['# a', '```', 'x', '```'])
parseDoc(doc)                   // BlockMeta[](绝对坐标 + 元素表)

ElementRange 要点

字段说明
kind`strong \em \code \strike \mark \link \image \tag \heading \quote \todo \bullet \ordered \hr \fenceOpen \fenceClose \codeLine \diagramOpen \diagramClose \diagramLine \tableHeader \tableSep \tableRow \tableCell`
scopeinline(扩一格命中)/ block(块命中)
from / to元素整体范围
hitFrom / hitTocursorEnter/Leave 判定区间
markers需隐藏的标记符子范围
content语义内容范围
static永不参与 reveal(tag / codeLine / ordered 序号样式)
permanent永久 Concealed(quote / bullet / todo / hr)
attrslevel / checked / checkPos / href / alt / indent / num / info / lang / code / colCount / col / tableEdge

标题permanent:源码 # 在 decoration 层永远隐藏,但聚焦时要展示层级图标,因此参与 reveal 判定。


CSS 入口

import '@21stware/handymd/style.css'
// 或
import '@21stware/handymd/style.css' // package exports: "./style.css"

挂载点 class:handymd。关键类名:

class用途
.hm-concealedfont-size:0 隐藏标记
.hm-caret-pad透明空格,保证行首光标可见
.hm-marker可见(弱化)标记
.hm-strong / .hm-em / .hm-code / .hm-strike / .hm-mark / .hm-link / .hm-tag行内语义
.hm-heading / .hm-h1… / .hm-heading-badge标题与层级图标
.hm-quote / .hm-todo / .hm-bullet / .hm-ordered块级
.hm-checkbox / .hm-bullet-dot / .hm-hr / .hm-imagewidgets
.hm-code-line / .hm-fence-line / .hm-code-lang代码块
.hm-diagram / .hm-diagram-host / .hm-diagram-hidden / .hm-diagram-loading / .hm-diagram-empty / .hm-diagram-errordiagram block
.hm-table / .hm-table-header / .hm-table-row / .hm-table-cell / .hm-table-sep表格