This page ships a Markdown twin for AI tools.

Guides

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.

Back to overview

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');