NABI NOTE
Редактор WYSIWYG с открытым кодом
NABI NOTE — редактор WYSIWYG с открытым кодом, в котором каждая крупная возможность — оформление, выравнивание, таблицы, загрузки и прочее — вынесена из ядра в отдельный модуль, называемый «wing», так что разработчик может дополнять его без всяких границ. Он написан на чистом Vanilla JS с прицелом на НОЛЬ зависимостей от фреймворков, поэтому одинаково ложится и в React, и во Vue, и куда угодно, а для проектов без сборки есть библиотека с CDN. У него свой формат JSON — NABI TREE, поэтому превращение HTML в текст и обратно можно подготовить там, где DOM нет вовсе (Node.js, SSR); а поскольку документ собирается заново из разрешённого словаря, а не латается, скрипты XSS перекрыты у корня без отдельной библиотеки очистки. В оформлении принят подход переменных CSS, так что фирменный цвет меняется легко, а вёрстка в rem сохраняет плавность мобильного интерфейса при любом масштабе; цвета под тёмную и светлую тему, маркеры и многоязычные шрифты уже на месте. Кроме того, доступны сортировка столбцов таблицы с учётом типа, локальная история и поддержка vibe coding.
Шрифт
Гротеск (по умолчанию), антиква, моноширинный и рукописный — в каждой семье шрифты сложены по системам письма, поэтому любой язык сохраняет лицо этой семьи; письменность, у которой в этой семье нет рукописного начертания, откатывается к шрифту браузера. Шрифт по умолчанию задаёт хост.
Ниже каждая семья показана на разных языках.
Без засечек · بلا زخارف · सैन्स सेरिफ़ · স্যান্স সেরিফ · سانس سیرف · Tanpa serif · بدون سریف · सॅन्स सेरिफ · Không chân · శాన్స్ సెరిఫ్ · Mara seref · Sans serif · Bila serif · சான்ஸ் செரிஃப் · ไร้เชิง · Senza grazie · 산세리프 · ゴシック体 · 无衬线 · Serifenlos · Sans empattement · Sin serifa · Sem serifa
С засечками · بزخارف · सेरिफ़ · সেরিফ · سیرف · Berserif · سریف · सेरिफ · Có chân · సెరిఫ్ · Mai seref · Serif · Yenye serif · செரிஃப் · มีเชิง · Con grazie · 세리프 · 明朝体 · 衬线 · Avec empattement · Con serifa · Com serifa
Моноширинный · ثابت العرض · मोनोस्पेस · মনোস্পেস · یکساں چوڑائی · Lebar tetap · تکفاصله · Đơn cách · మోనోస్పేస్ · Tazara ɗaya · Eş aralıklı · Nafasi moja · ஒற்றையகலம் · ความกว้างคงที่ · Monospaziato · Monospace · 고정폭 · 等幅 · 等宽 · Dicktengleich · Chasse fixe · Monoespaciada · Monoespaçada
Рукописный · خط اليد · हस्तलिपि · হস্তলিপি · رواں خط · Tulisan tangan · دستنویس · हस्ताक्षर · Chữ viết tay · చేతిరాత · Rubutun hannu · El yazısı · Mwandiko · கையெழுத்து · ลายมือ · Corsivo · Cursive · 필기체 · 筆記体 · 手写体 · Schreibschrift · Cursiva
Размер текста
Очень мелко
Мелко
Крупно
Очень крупно
Заголовок
На пустой строке наберите # и нажмите пробел — тут же станет заголовком.
H1
H2
H3
H4
H5
H6
Жирный · Курсив · Подчёркивание · Зачёркивание
Жирный курсив подчёркнутый зачёркнутый — вот пример.
Их можно и накладывать друг на друга.
Верхний и нижний индекс
Площадь — 3,5 м2, а сноска ставится вот так1.
Вода — это H2O.
Цвет текста · Маркер
Палитра подобрана так, чтобы одинаково читаться и в светлой, и в тёмной теме.
Цвет текста Зелёный · Коралловый · Фиолетовый · Янтарный · Синий
Маркер Жёлтый · Зелёный · Голубой · Розовый · Пурпурный · Оранжевый
Ссылка
Вставьте адрес — и получится ссылка.
Разрешены только http:// и https://; что-то вроде javascript: не пройдёт.
Например, наберите https://nabi.saro.me и нажмите пробел или Enter — превратится само, как здесь.
Открытие ссылок
По умолчанию ссылка того же происхождения открывается в этом окне, а любой другой сайт — в новом; правило задаётся при объявлении редактора.
Ссылка на вложение
Если загрузить не картинку, останется ссылка в виде файла, как ниже.
Вложение вот так она и остаётся.
Выравнивание
По левому краю
По центру
По правому краю
Заголовки тоже выравниваются.
Списки
Маркированный список
На пустой строке наберите - и нажмите пробел — тут же станет маркированным списком.
Это пункт списка
Tab / Shift+Tab делают отступ и снимают его.
Нумерованный список
На пустой строке наберите 1. и нажмите пробел — получится нумерованный список.
Первое
Второе
Третье
Список задач
На пустой строке наберите [ ] или [x] и нажмите пробел — получится список задач.
Эта задача сделана.
А эта пока нет.
Таблица
Создайте её через таблицу на панели; строки и столбцы можно добавлять, убирать и объединять.
Сортировка столбцов
Нажмите Просмотр, а затем щёлкните по заголовкам Остаток и Цена — один за другим.
Модель | Остаток | Цена | Вес |
|---|---|---|---|
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
Загрузка
Перетащите картинку или файл прямо в редактор.
Загрузка в этом примере поддельная; одной настройкой она подключается к вашему серверу.
Если загрузка сорвётся, картинка или файл убирается из редактора.
Цитата
На пустой строке наберите > и нажмите пробел — получится блок цитаты.
Он может занимать несколько строк.
Код
На пустой строке наберите ``` и нажмите пробел или Enter — получится блок кода.
Напишите заодно и язык, например ```java, затем пробел или 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
Чтобы превратить JSON в HTML, DOM не нужен.
Сервер (Node.js) читает сохранённое дерево наби как есть и собирает HTML, попутно отсекая XSS.
Дружелюбен к телефону
Мобильный вид — отзывчивая вёрстка держит мобильный вид.
Поправка на клавиатуру — когда клавиатура выезжает, её высота учитывается.
Гибкие размеры — все размеры записаны в 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: 'ru', name: () => 'note' })
const history = mountLocalHistory({ nabi, storage: browserHistoryStorage(window) })
const diff = mountDiffWing({ nabi, registry, surface: content, locale: 'ru' })
// The locale also sets the direction — Arabic and Urdu run right to left
mountSurface({ nabi, registry, root: content, locale: 'ru', fileSink: upload.take })
const settle = watchSettle(document, { surface: content })
const shared = { nabi, registry, surface: content, settle, locale: 'ru' }
// 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: 'ru', 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()))