# Rich text editor docs

> Install @texuli/editor, read and write Markdown, paste HTML safely, set block types from a menu, add emoji and export to chat dialects. Paste through @texuli/core/html, the block menu, emoji and chat export are in development.

Language: en
Canonical: https://texuli.com/guides/solutions/editor/
Markdown: https://texuli.com/guides/solutions/editor.md (this page). Every texuli.com page has a Markdown twin at its URL plus `.md` (the home page is https://texuli.com/index.md), and any page URL answers `Accept: text/markdown` with its twin instead of HTML.
Last Updated: 2026-11-13

---

Guides

Install @texuli/editor, read and write Markdown, paste HTML safely, set block types from a menu, add emoji and export to chat dialects. Paste through @texuli/core/html, the block menu, emoji and chat export are in development.

[Back to overview](https://texuli.com/solutions/editor/)

## Install and status

Install the published editor with `pnpm add @texuli/editor`. It is pre-1.0, so a new minor version may break the API. The editor surface, toolbar, link dialog, controller, Markdown codec and clipboard import are on npm.

Clipboard HTML through `@texuli/core/html`, Paste as Markdown, the block-type menu, the emoji button and the export to chat dialects were released in `0.18.0`. The editor ships no emoji data: the emoji picker comes from `texuli/emoji`, and the `:shortcode:` rule uses a lookup you pass.

```
<script lang="ts">
import { RichTextEditor } from '@texuli/editor/editor';
</script>

<RichTextEditor label="Notes" placeholder="Write the update…" />
```

## Markdown in and out

`markdownToDocument` reads Markdown into a valid editor document and `documentToMarkdown` writes it back, so the saved text reads back to the same document. Both live in `@texuli/editor/markdown`, outside the package barrel.

- Reading is untrusted by default: raw HTML stays literal text and every link passes the URL policy, so a refused link keeps only its text.
- The editor holds headings 1 to 4, paragraphs, bulleted, numbered and todo lists, quotes, fenced code, dividers and the strong, em, strike, code and link marks. Deeper headings become level 4, with a diagnostic.
- Input is capped and read in linear time; nothing here produces HTML.

```
import { documentToMarkdown, markdownToDocument } from '@texuli/editor/markdown';

const { doc, diagnostics } = markdownToDocument(agentText);
const saved = documentToMarkdown(doc);
```

## Safe paste

Clipboard HTML is read without a DOM by `htmlToMarkdownTree` from `@texuli/core/html` at the untrusted tier, then mapped onto the editor vocabulary. In development: released in `0.18.0`.

- Scripts, styles, frames, SVG and other raw-text elements are dropped with their content, and every attribute but the few the editor maps is gone, event handlers included.
- A link the URL policy refuses, such as `javascript:`, keeps its text and counts in `linksRemoved`.
- Images become their alt text, and table cells become paragraphs.
- Plain text stays plain. Paste as Markdown is a separate command: Mod+Alt+Shift+V in the editor, or `controller.pasteMarkdown(text)`, which loads the Markdown codec on first use.

```
import { EditorController } from '@texuli/editor/controller';

const controller = new EditorController();
const result = controller.paste(event.clipboardData);
// result.linksRemoved: links refused by the URL policy
```

## Block-type menu

Pass `blockMenu` to `RichTextEditor` or `EditorToolbar`, or use `BlockTypeMenu` from `@texuli/editor/block-menu`. In development: released in `0.18.0`.

- It follows the APG menu button pattern: the button names the current kind, and the menu holds item radios for paragraph, headings 1 to 4, bulleted, numbered and check lists, quote and code block.
- Enter, Space and Down Arrow open it on the first item, Up Arrow on the last; Escape closes it and returns focus.
- A choice is one undo step and is announced. A plain block leaves the lists and the quote around the caret.

```
<RichTextEditor value={doc} blockMenu label="Release notes" />
```

## Emoji

Pass `emoji` to `RichTextEditor` or `EditorToolbar` for an emoji button, or use `EditorEmojiButton` from `@texuli/editor/emoji`. In development: released in `0.18.0`.

- The button opens `EmojiPicker` from `texuli/emoji` in a popover: search, a keyboard grid and skin tones, with recents kept by the host. The picker and its catalog load when the popover opens.
- A chosen emoji is one undo step, apart from typing, and focus returns to the text.
- A typed `:name:` becomes its emoji when you pass a `shortcodes` lookup; one undo restores the shortcode. It never runs inside code.
- `getEmoji` matches the catalog's canonical shortcodes, such as `:party_popper:`. To accept common aliases such as `:tada:` too, build a map from the aliases in `emojiCatalog.records`, as the demo on the landing does.

```
import { getEmoji } from '@texuli/emojis/catalog';
import { EditorController } from '@texuli/editor/controller';

const controller = new EditorController({
  shortcodes: (name) => getEmoji(`:${name}:`)?.unicode,
});
```

## Export to chat

`documentToDialect` in `@texuli/editor/markdown` writes the document for Slack, Discord, WhatsApp, Telegram (MarkdownV2 or HTML), Microsoft Teams, Google Chat, Mattermost or plain text. `DialectExport` from `@texuli/editor/export` shows the text, a Copy button and the losses. In development: released in `0.18.0`.

- Every link is checked again by the URL policy, untrusted by default.
- Broadcast mentions are never written: `@everyone`, `@here` and `@channel` stay text.
- What a format cannot carry is returned in `losses`, grouped with a count. No chat format has checklists, so items keep their state as ☑ or ☐.
- Agents get the same conversion through the `md.convert` MCP operation.

```
import { documentToDialect } from '@texuli/editor/markdown';

const { value, losses } = documentToDialect(doc, 'slack');
```
