Skip to content
EspañolEnglish

Rich text editor

RichTextEditor is the text editor of @amset/ui, built in-house with no third-party dependencies. It lives in a subpath, @amset/ui/rich-text, so anyone who does not use it loads not a single byte. It stores the document as JSON, exports it to HTML, Markdown and Word, renders it on the server with RichTextView, and lets you put the library’s own components inside the text.

Try it here

These are not screenshots: it is the library editor running on this page. Type, paste, press “/” and see what is stored.

The editor and what it stores

Type, paste something from Word or a web page, or press “/” at the start of a line to insert a table, a callout or a collapsible section. Below, the same document in each format, as you type.

Rich text editor. Alt+F10 moves to the toolbar.
Code
<h2>Meeting notes</h2><p>Write here with <strong>bold</strong>, <em>italics</em> or a <a href="https://amset.tech">link</a>. Try <code>/</code> as well to insert blocks.</p><ul><li><p>Review the budget</p></li><li><p>Send the proposal to the client</p></li></ul><table><tbody><tr><th><p>Task</p></th><th><p>Owner</p></th></tr><tr><td><p>Design</p></td><td><p>Anna</p></td></tr><tr><td><p>Development</p></td><td><p>Louis</p></td></tr></tbody></table>

With the library components inside

The card and the grid hold document text inside: type in them. Type “/” and the name of a component (Badge, Progress, Alert…) to insert it; with the cursor on one, the toolbar shows its properties. The chart takes its figures from this page’s data and the button calls an action of the page.

Rich text editor. Alt+F10 moves to the toolbar.

What the reader sees

Quarter summary

Before you start

The figures are updated every night. Type in here as anywhere else.

Orders

342

12 pending

Invoiced

18.420 €

this month

Press the report button: inside the editor it does nothing, in the view it calls the page action.

The same document as React code

Code
import { BarChart, Button, Card, Grid, Stat } from '@amset/ui' export function Report({ actions, data }: { actions: Record<string, () => void>; data: Record<string, any> }) {  return (    <>      <h2>Quarter summary</h2>      <Card title="Before you start">        <p>The figures are updated <strong>every night</strong>. Type in here as anywhere else.</p>      </Card>      <Grid columns={2}>        <Stat label="Orders" value="342" detail="12 pending" />        <Stat label="Invoiced" value="18.420 €" detail="this month" />      </Grid>      <BarChart label="Monthly sales" data={data.sales} />      <Button onPress={actions.save}>        Save draft      </Button>    </>  )}

The safe HTML viewer

This HTML brings a script, an image with code, a JavaScript link and a form asking for a password. Change it as you like: the viewer draws it without running anything and says what it removed.

How it is drawn

Autumn news

Wool jackets with a 20 % discount.

See the collection

Click here

Log in

Nothing had to be removed.

What it brings

A field like the rest

Label, hint, error, counter, optional limit, read-only and disabled, like any other field in the library. It has a toolbar, a floating bar over the selection and the “/” menu to insert blocks.

Text and structure

Headings, bold, italic, underline, strikethrough, code, links, text colour, highlight, subscript and superscript, alignment, nested lists, task lists, quotes, code blocks, images, tables with merged cells, text in columns and formulas written in TeX.

Blocks

Callouts (note, tip, warning, danger), collapsible sections and @ mentions. And your own blocks: you register a type with its render and the document stores only its name and its data.

Warns while you type

An image without a description, a table without a header, a heading that skips a level or a link that says “here” show up in a strip under the text, with a button to go there and, where it is safe, to fix it. Plus undo and redo, find in the text and full screen.

Team review

Spellchecker, comments on a piece of text, suggestions that are accepted or rejected, and version history with comparison. Each piece is downloaded only when it is opened.

Faithful paste

What comes from Word, Google Docs or a web page is cleaned up and what carries meaning (headings, lists, tables, links) is kept, without dragging foreign styles along.

For agents

An API for an assistant to read the document block by block and propose changes that a person accepts or rejects before they are applied.

Getting started

The editor works controlled or not, with the document as JSON or HTML. JSON is what you should store: it loses nothing. RichTextView renders the same document without loading the editor.

Code
import { RichTextEditor, RichTextView } from '@amset/ui/rich-text' export function Notes({ html, onChange }: { html: string; onChange: (html: string) => void }) {  return <RichTextEditor label="Notes" format="html" defaultValue={html} onChange={onChange} rows={8} />} // Solo lectura, sin cargar el editor (también en el servidor)export function Preview({ html }: { html: string }) {  return <RichTextView format="html" value={html} />}

Formats

Each converter is imported only if you use it; the editor loads none of them.

  • HTML: toHtml and fromHtml. Everything the editor can do comes out as valid HTML and is read back.
  • Markdown: toMarkdown and fromMarkdown (CommonMark, plus GFM tables, tasks and strikethrough). What Markdown cannot say (underline, colour, alignment, merged cells) is written as HTML inside the Markdown, so the document survives the round trip. Formulas are stored between $$. A test checks it with 1500 random documents.
  • Word: toDocx and fromDocx write and read .docx files with headings, nested lists, tables with merged cells, links, images and mentions. Suggestions come out as tracked changes and comments as Word comments, and are read back. Columns come out as a single column and formulas as the TeX, in code style.
  • React code: toTsx turns the document into a React component that imports from @amset/ui.
Code
import { toMarkdown, fromMarkdown, toDocx } from '@amset/ui/rich-text' const markdown = toMarkdown(doc)const back = fromMarkdown(markdown)const bytes = await toDocx(doc)

Your own blocks

A block is a piece of yours (a product card, a chart, a form) inside the text. The document stores only its type and some JSON data with limits on size and depth; what you see is decided by your render, used the same in the editor, in RichTextView and on the server. A block nobody knows how to draw is kept as it is, not lost.

Code
const blocks = {  'product-card': {    label: 'Product card',    defaultProps: { name: 'Product', price: '0 €' },    render: (props) => <ProductCard {...props} />,  },} <RichTextEditor label="Page" blocks={blocks} />

Review and proofread as a team

What several people need to work on the same text. Each piece is switched on with a prop, and whatever you do not switch on adds no weight.

Spellchecker

Mistakes get a wavy underline; right-click shows suggestions, ignore and add to dictionary. The word you are typing is not marked until you move on. The editor ships no dictionary, because it would weigh megabytes and there is one per language: you plug in yours with createWordSpellchecker or createLanguageToolSpellchecker.

Comments

Select a piece of text and comment on it. Each comment opens a thread where people reply, resolve or delete. You store the threads wherever you like (threads and onThreadsChange); the document only keeps the mark of which text they refer to.

Suggestions

In “Suggest changes” mode what you type is underlined and what you delete is struck through, with author and date, instead of changing the text. Whoever has permission accepts or rejects each one, or all at once. Formatting and structure changes are not recorded as suggestions.

Version history

A list of saved versions with their name. “View changes” shows what was added underlined and what was removed struck through, from that version to now, and restoring can be undone. diffDocuments(before, after) gives the same comparison outside the editor.

Columns

A block of two to four columns the text flows through, as in a newspaper. On a narrow screen it becomes a single column.

Formulas

Written in TeX in a box with a live preview and drawn as MathML, with our own converter and no dependencies. If the TeX is not understood it is shown as typed and the box says why.
Code
import { RichTextEditor, createLanguageToolSpellchecker, diffDocuments } from '@amset/ui/rich-text' <RichTextEditor  label="Contract"  spellchecker={createLanguageToolSpellchecker({ url: '/api/spellcheck', language: 'en' })}  review={{ author: 'Ana', threads, onThreadsChange: setThreads, suggesting, onSuggestingChange: setSuggesting }}  versions={{ versions, load: loadVersion, onSave: saveVersion, onRestore: restoreVersion }}/> // The same comparison outside the editorconst changes = diffDocuments(before, after)

Real-time editing, with several people typing at once and their cursors in view, is not here yet. It needs a sync server and per-user credentials, and will come later. Meanwhile, comments, suggestions and versions work on a document that is saved and reopened.

AMSET UI components inside the document

With amsetBlocks() the library components (159 today) become blocks: type “/” and the name, and the real component is inserted.

Generated catalogue

It comes from the TypeScript compiler: the JSON schema of each component’s props is extracted from it. A test fails if the catalogue falls behind the library, so a new component gets in on its own.

Loaded on demand

Each component is downloaded the first time a document uses it. A document without charts does not download the charts.

What the document stores

The document stores the component name and its props as JSON, validated against its schema before drawing: what does not fit is dropped, and an incomplete block says what it lacks instead of breaking. className, style, ref, id, raw HTML and functions are never stored, and links and images go through the same URL checks as the rest of the editor.

Components with content

Card, Alert, Grid, Stack, Center, Collapsible and Container hold document blocks inside, written like any other text. In the editor they show a frame with their name; the toolbar offers their properties, a form generated from the schema, and “Remove and keep content”.

Your application’s actions and data

A button in the document carries no function: it carries an identifier (save). A chart carries no figures: it carries the name of a data source (sales). Your application resolves them with amsetBlocks({ actions, data }). Nothing runs inside the editor, and the properties form lets you choose where each list comes from.
Code
import { RichTextEditor, amsetBlocks } from '@amset/ui/rich-text' const blocks = amsetBlocks({  actions: { save: { label: 'Save draft', run: () => saveDraft() } },  data: { sales: { label: 'Monthly sales', value: sales } },}) <RichTextEditor label="Report" blocks={blocks} />
document
{  "type": "component",  "kind": "amset-card",  "props": { "title": "Summary" },  "content": [{ "type": "paragraph", "inline": [{ "type": "text", "text": "Quarter figures", "marks": [] }] }]}

A safe HTML viewer

HtmlViewer draws HTML you do not control: an email template, the body of a message, the HTML a model returns.

Document mode

The HTML is parsed, only what is allowed is kept (tags, attributes, URL schemes and CSS properties) and it is written again from the tree. Whatever a parser could misread is never copied. What was removed is reported in onReport.

Page mode

It respects the design and styles of whoever wrote the HTML, inside an <iframe sandbox> without allow-same-origin and with a security policy that allows no network, forms, frames or foreign scripts. Only a small script of ours runs, with a nonce, which reports the height and link clicks.
Code
import { HtmlViewer } from '@amset/ui/rich-text' <HtmlViewer html={untrustedHtml} images="https" onReport={(report) => log(report)} /> // El diseño del autor, aislado en un iframe<HtmlViewer html={newsletter} mode="page" images="https" />

To code and to agents

toTsx(doc) returns the code of a React component with the library components, actions as actions.save and data as data.sales, plus a list of warnings for what could not be carried over as is. For an assistant, listComponents(), describeComponent(kind) and createComponent(kind, props) give the catalogue, a component’s sheet and an already validated block, with the errors explained instead of a broken block.

Code
import { toTsx, listComponents, createComponent } from '@amset/ui/rich-text' const { code, warnings } = toTsx(doc, { componentName: 'Report' }) listComponents()                       // the catalogueawait createComponent('amset-stat', { label: 'Orders', value: '342' })

Security

  • What is pasted, imported and loaded goes through allowlists; nothing unlisted reaches the document.
  • The document never holds functions, scripts or arbitrary styles. Custom and library blocks are validated against their schema and with size limits.
  • Links accept http, https, mailto, tel and relative paths; images accept http, https and own paths, never data:.
  • The HTML viewer is tested with more than 50 attack vectors and 600 randomly mutated documents.

Accessibility and weight

The field is a role="textbox" with its label, the toolbar works with the keyboard, important changes are announced and every story passes axe. The full editor weighs about 99 KB gzipped and RichTextView, which only reads, about 21 KB. The review and version panels, the formula converter, the AMSET UI components and the Markdown and Word converters load only if used.

Keep reading