翅膀
默认字体高度


NABI NOTE

开源 WYSIWYG 编辑器


NABI NOTE 是一款开源 WYSIWYG 编辑器,它把格式、对齐、表格、上传等功能从核心中分离出来,作为名为 wing 的独立模块组成。你可以只选择需要的 wing 来组装编辑器,也可以连接自己制作的 wing。它用纯 JavaScript 编写,因此可以在 React 或 Vue 等框架中使用。没有构建环境的页面也可以通过 CDN 库 立即开始。它自己的文档格式 NABI TREE 即使在没有浏览器 DOM 的 Node.js 环境中也能转换为 HTML。输入的文档会按照已注册规则整理;生成 HTML 时会转义文本和属性,并检查链接地址。不过,自定义 wing 和从服务器接收的数据仍然需要按照应用的安全策略另行检查。CSS 变量和基于 rem 的布局可以调整主题和大小,也提供适合深色模式和多语言的字体设置。表格排序、本地历史以及 AI 辅助的文档工作,也都延续在同一个文档模型之上。


字体

无衬线(默认)、衬线、等宽、手写体都准备了按语言回退的字体。若没有适合所选字体类别的字体,就会显示浏览器默认字体。编辑器的默认字体由宿主页面决定。


下面可以查看各种字体在多种语言中如何显示。


无衬线 · Serifenlos · Sans empattement · Sin serifa · Sem serifa · Без засечек · بلا زخارف · सैन्स सेरिफ़ · স্যান্স সেরিফ · سانس سیرف · Tanpa serif · بدون سریف · सॅन्स सेरिफ · Không chân · శాన్స్ సెరిఫ్ · Mara seref · Sans serif · Bila serif · சான்ஸ் செரிஃப் · ไร้เชิง · Senza grazie · 산세리프 · ゴシック体


衬线 · Serif · Avec empattement · Con serifa · Com serifa · С засечками · بزخارف · सेरिफ़ · সেরিফ · سیرف · Berserif · سریف · सेरिफ · Có chân · సెరిఫ్ · Mai seref · Yenye serif · செரிஃப் · มีเชิง · Con grazie · 세리프 · 明朝体


等宽 · Dicktengleich · Chasse fixe · Monoespaciada · Monoespaçada · Моноширинный · ثابت العرض · मोनोस्पेस · মনোস্পেস · یکساں چوڑائی · Lebar tetap · تک‌فاصله · Đơn cách · మోనోస్పేస్ · Tazara ɗaya · Eş aralıklı · Nafasi moja · ஒற்றையகலம் · ความกว้างคงที่ · Monospaziato · Monospace · 고정폭 · 等幅


手写体 · Schreibschrift · Cursive · Cursiva · Рукописный · خط اليد · हस्तलिपि · হস্তলিপি · رواں خط · Tulisan tangan · دست‌نویس · हस्ताक्षर · Chữ viết tay · చేతిరాత · Rubutun hannu · El yazısı · Mwandiko · கையெழுத்து · ลายมือ · Corsivo · 필기체 · 筆記体



文字大小

非常小

非常大



标题

在空行输入 # 并按 Space,就会变成标题。

H1

H2

H3

H4

H5
H6



加粗 · 斜体 · 下划线 · 删除线

加粗 斜体 下划线 删除线 可以一起应用在同一段文字上。

也可以叠加使用。

上下标

面积是 3.5m2,脚注可以这样添加。1

水是 H2O。



文字颜色 · 荧光笔

颜色经过配置,在浅色模式和深色模式下都易读。

文字颜色 绿色 · 珊瑚色 · 紫色 · 琥珀色 · 蓝色

荧光笔 黄色 · 浅绿 · 天蓝 · 粉色 · 紫色 · 橙色



链接

输入地址后会变成链接

除了 http:// 或 https:// 地址,也可以使用以 . 或 / 开头的同站路径。javascript: 这样的地址不允许使用。

例如输入 https://nabi.saro.me 后按 Space 或 Enter,就会自动变成链接。

链接打开方式

文档只保存链接地址,链接如何打开由显示文档的页面决定。

附件链接

图片以外的文件会以下面的附件链接形式保留下来。

附件文件 链接会保存到文档中。



对齐

左对齐

居中对齐

右对齐

标题的对齐方式也可以改变。



列表

项目符号列表

在空行输入 - 并按 Space,就会变成项目符号列表。

  • 这是项目符号列表。

    • 可以用 Tab / Shift+Tab 缩进和取消缩进。

编号列表

在空行输入 1. 并按 Space,就会变成编号列表。

  1. 第一

  2. 第二

  3. 第三

任务列表

在空行输入 [ ] 或 [x] 并按 Space,就会变成任务列表。

  • 已勾选的项目。

  • 尚未勾选的项目。



表格

点击工具栏中的表格创建后,可以添加或删除行列,并合并需要的单元格。

表格排序

点击 预览,再依次点击 库存价格 表头。

型号

库存

价格

重量

NB-7

1,200

349

1.2 kg

NB-9

20,000

99

0.9 kg

NB-12

3,500

1,299

1.4 kg

NB-80

900

8,900

2.1 kg

NB-100

待定

12,999

2.4 kg

价格列全部是数字,因此按数字排序。

库存列混有数字和文字,因此按字符串排序。



分隔线

输入 --- 后按 Enter,会变成分隔线。




图片

图片大小可从 30% 调整到 100%,也可以左对齐、居中或右对齐。



YouTube



上传

试着把图片或文件拖到编辑器里。

这个 demo 不会真的把文件发送到服务器。连接上传器后,可以保存到服务器。

上传失败的项目不会添加到文档中。



引用

在空行输入 > 并按 Space,就会变成引用。

可以使用多行。



代码

在空行输入 ``` 并按 Space 或 Enter,就会变成代码框。

像 ```java 这样输入语言名称并按 Space 或 Enter,就会变成指定了该语言的代码框。

import { createNabiWith, defaultWings } from 'nabi-note'

const { nabi } = createNabiWith(defaultWings)
const html = nabi.getHtml()



折叠

折叠块由标题和内容组成。

可以按需要保存为折起或展开状态。



快捷键

快速按两次 Shift,工具栏会显示各功能的快捷键。



自动格式

示例

操作键

说明

#

空格

标题

-

空格

项目符号列表

1.

空格

编号列表

[ ] · [x]

空格

任务列表

>

空格

引用

``` · ```ts

空格 · Enter

代码框

---

Enter

分隔线

https://…

空格 · Enter

链接



输出函数

函数

结果

getHtml()

HTML

getJson()

JSON



支持无 DOM 环境

将 NABI TREE 转换为 HTML 时,不需要 DOM

服务器(Node.js)也可以读取保存的 NABI TREE,并按照同一规则生成 HTML。


移动环境

  • 移动 UI — 按屏幕宽度布置编辑工具。

  • 移动键盘修正 — 键盘打开时调整编辑画面的高度。

  • 可变大小 — 所有大小都以 rem 单位编写。

  • 多语言 — 支持多种语言。


自定义

  • 自定义 wing — 需要的功能可以自己制作并使用。

  • 自定义 CSS — 颜色、圆角、间距都定义为 --nabi-*,深色和浅色都可以自行设置。

  • 开源 — 在 GitHub 上以开源形式提供。


查看文档 → nabi.saro.me

HTML正在加载编辑器…
JSONnabi-tree正在加载编辑器…
安装
npm install nabi-note
代码
import {
  createNabiWith, mountSurface, mountToolbar, mountContextToolbar,
  mountHints, watchSettle, defaultWings, mountUpload, mountUploadView,
  browserFileStore, mountFile, parseNodes, mountViewTools,
  browserHistoryStorage, mountLocalHistory, openHistoryPanel,
} from 'nabi-note'
import { mountDiffWing } from 'nabi-note/diff'

// The box that asks — without it the core answers "no" to everything (plug in your own)
const ask = { message: (t: string) => alert(t), confirm: (t: string) => confirm(t) }

const selected = [
  ...defaultWings,
]
const { nabi, registry } = createNabiWith(selected, { ask })

// Bring your own highlighter (Prism, highlight.js, Shiki, …) — the wing stays as it is
// and only the attach is swapped (`makeCodeAttach` ships from the same door)
// { ...codeWing, attach: makeCodeAttach({ highlight: (source, lang) => [{ text: source }] }) }

const root = document.querySelector('.nabi')!
const content = document.querySelector('.nabi-content')!

const view = mountUploadView({ nabi, surface: content })
const upload = mountUpload({
  nabi, root: content,
  // your upload goes here — report progress with task.onProgress(0–100)
  uploader: async (task) => ({ uri: 'https://cdn.example/uploaded' }),
  extensions: ['png', 'jpg', 'pdf'], maxFileSize: 10 * 1024 * 1024,
  onStart: (tasks) => view.start(tasks),
  onProgress: (id, percent) => view.progress(id, percent),
  onSettle: () => view.settle(),
  onDone: () => view.done(),
})
const file = mountFile({ nabi, registry, store: browserFileStore(document), parse: parseNodes, locale: 'zh', name: () => 'note' })
const history = mountLocalHistory({ nabi, storage: browserHistoryStorage(window) })
const diff = mountDiffWing({ nabi, registry, surface: content, locale: 'zh' })

// The locale also sets the direction — Arabic and Urdu run right to left
mountSurface({ nabi, registry, root: content, locale: 'zh', fileSink: upload.take })

const settle = watchSettle(document, { surface: content })
const shared = { nabi, registry, surface: content, settle, locale: 'zh' }
// A tool that needs a panel comes back to the host — without this the button is dead
const toolbar = mountToolbar({
  ...shared, root: document.querySelector('#toolbar')!,
  file,
  onFiles: upload.take,
  onHost: (w) => {
    if (w === 'diff') { diff.open(); return }
    if (w !== 'localHistory') return
    openHistoryPanel({
      history, surface: content, locale: 'zh', sessionId: history.sessionId,
      render: (record) => createNabiWith(selected, { doc: JSON.parse(record.body) }).nabi.getHtml(),
    })
  },
})
const context = mountContextToolbar({ ...shared, root: document.querySelector('#context')! })
mountHints({ toolbar, context, root, surface: content })
// The preview and fullscreen buttons — they stand their own box at the end of the row
mountViewTools({ ...shared, root, container: document.querySelector('#toolbar')! })

// on every change — hook up your own code here
// nabi.onChange(() => user_callback(nabi.getHtml()))