Kotoba

Kotoba — Rich text for Phoenix

Kotoba (言葉, "words") is rich text for Phoenix. It gives your app:

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

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.