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,就会变成编号列表。
第一
第二
第三
任务列表
在空行输入 [ ] 或 [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
npm install nabi-noteimport {
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()))