Kotoba
Kotoba (言葉, "words") is rich text for Phoenix. It gives your app:
- a rich-text editor for LiveView forms, built on Lexical: bold, italic, underline, strikethrough, highlight, subscript, superscript and inline code, headings, quotes, lists and check lists, links, code blocks highlighted in 28 languages, tables, file attachments, mentions, and your own nodes;
Kotoba.Content, an Ecto type that stores the document with a cached HTML and text rendering;- the features of each editor (
features={~w(bold links lists)}) and extensions, the app's own nodes, commands, toolbar buttons and Markdown shortcuts; - safe rendering to HTML, text and Markdown, with no raw HTML anywhere.
The editor is a prebuilt JavaScript bundle in the Hex package. Your app does not run Node.js for it: you add the hook and one style sheet.
What the editor does
| Text | bold, italic, underline, strikethrough, inline code, subscript, superscript; a palette of text colors and highlights |
| Blocks | headings, quotes, bulleted, numbered and check lists, horizontal rules |
| Code | code blocks highlighted as you type in 28 languages, with a language picker |
| Tables | insert, rows and columns, header rows and columns, pasted tables |
| Links | a link form, paste a URL over text, autolinks, allowed schemes |
| Files | drop, paste or pick images and files; image galleries, reordered from the keyboard; PDFs in the browser's viewer, MP4 and WebM videos; LiveView uploads, a storage adapter with byte ranges |
| Prompts | @ mentions, emoji, tags: server searches (async too) or local lists, with queries that have spaces |
| Suggestions | text streamed from the server (a language model), shown as it comes, then accepted as one undo step or rejected; an Assist menu |
| Collaboration | shared documents, participant cursors, read-only viewers, per-author undo, offline draft recovery and accepted revisions for form submission; setup |
| Shortcuts | the usual keyboard shortcuts, and Markdown as you type (## , - , **bold**, ```) |
| Per field | the features of each editor (features={~w(bold links lists)}), checked on the server |
| Your own | nodes (mix kotoba.gen.node), extensions with commands, toolbar buttons and shortcuts, custom toolbars |
| Output | cached, safe HTML and plain text; Markdown; a strict policy for untrusted content |
| Theming | CSS custom properties, the Sumi theme, dark mode |
| Accessibility | a labelled textbox, a toolbar with one tab stop, a live region, no keyboard trap |
The Editing features guide is the tour, and the
development server of this repository (mix dev, below) shows each of
them.
Install
Kotoba needs Elixir 1.18 or later (it uses the built-in JSON module),
Phoenix 1.8 and Phoenix LiveView 1.2. CI runs the tests on Elixir 1.18
(OTP 27) in a separate floor job, as well as on the current version.
Add kotoba to the deps in mix.exs:
def deps do
[
{:kotoba, "~> 0.1"}
]
end
Then run:
mix deps.get
mix kotoba.install
The installer adds the hook to assets/js/app.js, the style sheet to
assets/css/app.css and the storage adapter to config/config.exs, and
prints what it changed. Give --dry-run to see the changes first. It does
not add what a file already has, so you can run it again.
The manual steps
To make the same changes by hand, import the hook in assets/js/app.js
and add it to the hooks of the LiveSocket:
import { Kotoba } from "kotoba"
const liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
hooks: {...colocatedHooks, Kotoba},
})
Import the style sheet in assets/css/app.css (and the Sumi theme, if you
want it: Sumi is the design system of Hattori AI, the makers of Kotoba). Put these lines below the other @import lines of app.css
(the installer puts them there):
@import "../../deps/kotoba/priv/static/kotoba.css";
/* @import "../../deps/kotoba/priv/static/kotoba-sumi.css"; */
Name the storage adapter for uploads in config/config.exs:
config :kotoba, storage: Kotoba.Storage.Local
NODE_PATH=deps
import { Kotoba } from "kotoba" resolves through the NODE_PATH of your
esbuild profile, which must have deps/. The Phoenix generators already
set it. If your profile has no NODE_PATH, add it in config/config.exs:
env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
Use
schema "posts" do
field :body, Kotoba.Content
end
<.form for={@form} phx-change="validate" phx-submit="save">
<label id="post-body-label">Body</label>
<.kotoba field={@form[:body]} id="post-body" label_id="post-body-label" />
</.form>
<.kotoba_content content={@post.body} />
Import the components in the html_helpers of your web module:
import Kotoba.Components
The editor posts the document with the form, as a JSON string that
Kotoba.Content casts, so the form's own phx-change and phx-submit
events have it. <.kotoba_content> shows the stored content as safe
HTML.
Guides
- Quickstart: install, the schema, the form and the rendered content.
- Forms and changesets: what the form posts, the changeset, the LiveView events and the server pushes.
- Editing features: the features of an editor, their shortcuts, colors, links, code blocks, tables and the toolbar.
- Rendering: the stored content in a page, HTML, text and Markdown, and content without the editor.
- Uploads: LiveView uploads, the storage adapters and how to serve the files.
- Prompts and mentions:
@menus, emoji and tags, from the LiveView or from a local list. - Suggestions: text streamed from the server, the Assist menu, and a language model example.
- Custom nodes: your own nodes, with
mix kotoba.gen.node. - Extensions: your own commands, toolbar buttons and Markdown shortcuts.
- Theming: the
--kotoba-*properties and the Sumi theme. - Security: what Kotoba checks, and what your app must do.
- Accessibility: the roles and the keyboard.
- Known limits.
Development
The editor source is TypeScript in assets/. The built files in
priv/static are not in the repository; the Hex package has them.
mix deps.get
mix kotoba.build # npm ci in assets/ when necessary, then esbuild
mix dev # the development server on http://localhost:4099
mix kotoba.build runs in the :dev environment and needs npm. It
empties priv/static and writes the four files of the package:
kotoba.esm.js, kotoba.cjs.js, kotoba.css and kotoba-sumi.css.
mix dev runs dev.exs: a Phoenix endpoint with the editor in a form,
the prompts (@ people, : emoji, + tags, ~ a slow search, ! a
failing one), uploads to tmp/uploads, an app node made with
mix kotoba.gen.node (in dev/), buttons that send each server event,
editors with fewer features and an example extension at /extensions,
and the Sumi theme at /?theme=sumi. Set PORT for another port. esbuild
watchers rebuild the bundle and the node modules, and the page reloads.
Restart mix dev after a change in dev/lib.
Tests
mix test # the ExUnit suite
mix test --include build # also mix kotoba.build and mix hex.build
mix test.e2e # the Playwright browser tests
mix precommit # compile, format, credo --strict and test
mix dialyzer
The browser tests in e2e/ use Playwright, in Chromium, Firefox and
WebKit (the engine of Safari). Install the browsers once:
cd e2e && npm ci && npx playwright install chromium firefox webkit
mix test.e2e runs every spec in the three browsers, and E2E_BROWSERS
picks some of them. In e2e/, Playwright runs one spec (it starts the
development server too):
E2E_BROWSERS=firefox mix test.e2e
cd e2e && E2E_BROWSERS=chromium,webkit npx playwright test specs/prompts.spec.ts
A failed test keeps its trace and a screenshot in e2e/test-results/;
open a trace with npx playwright show-trace <trace.zip> in e2e/. CI
runs one job for each browser, and uploads the results of a failed job as
an artifact.
mix test.e2e runs npm ci in e2e/ when e2e/node_modules is missing,
builds the bundle, and starts its own development server, without
watchers, on port 4098 (set E2E_PORT for another port). A mix dev on
port 4099 can keep running.
Release
mix release (in this repository, an alias that replaces Mix's release
task) builds the bundle, checks that priv/static holds only the
four bundle files (mix kotoba.release_check), publishes to Hex, and tags
and pushes v<version>.
License
Kotoba is released under the MIT License. See LICENSE.
The built editor includes third-party code under the MIT licence:
Lexical (Meta Platforms, Inc. and affiliates),
PrismJS (Lea Verou) and
@preact/signals-core (the Preact
team). The toolbar icons are from Lucide (ISC
licence); some of them come from Feather (MIT licence). See NOTICE for
the full notices.