Rich text

Every rich text field (a product's text, an entry's teaser, the texts a body's blocks hold, a quiz's text) takes HTML in one closed format. A write is validated and stored in a canonical form; a text outside the format is refused as invalid, with context.path naming the field (into a body's JSON for a block's text) and context.reason what is wrong. When the service says where in the text, context.offset is the character it stopped at (for a text a block holds, where that block starts in the text holding it), context.name the tag, attribute or style it names, and context.excerpt quotes the text around it; for a text sent as Markdown, that is the markup it was converted into, unless the conversion itself refused it. A publish refused points into the staged body (["args", "target", …]), so it comes with no excerpt.

Allowed

Attributes, by tag: p (style), h1 (style), h2 (style), h3 (style), span (style), a (href, target, rel), ol (start), li (data-checked), th (colspan, rowspan), td (colspan, rowspan), code (class), details (open), block (type, props). Styles, by tag: p (text-align), h1 (text-align), h2 (text-align), h3 (text-align), span (color, background-color). Every other tag, attribute, style property or value is refused, as are comments, a link or a block inside a link, and a misplaced summary or figcaption.

Limits

A text holds at most 1 MiB of UTF-8, before and after it is stored, within its field's own limit (in its schema), and its markup nests at most 32 elements deep, counting the empty paragraph an empty container is stored with. A table whose rows are so uneven that evening them out would outgrow the text, or whose merged cells take too long to lay out, is refused, and a text's tables cover at most 10000 cells in all: each table counts its rows times its widest row, a row counting the columns cells from earlier rows span into it.

Reads

A read answers the stored canonical form, wrapped in <rich-text version="1">: attributes in a fixed order, entities and spacing normalized, loose inline content in a paragraph, an empty list item, cell, quote, details or figure holding an empty paragraph, an empty list holding one empty item, and a table made rectangular (a short row padded with empty cells, a column span cut where it runs into a row span, a table with no cell dropped). Sending it back unchanged keeps the text as it is.

Markdown

A write holding rich text also takes text_format: "html", the default, or "markdown". In Markdown (CommonMark with tables, task lists, strikethrough and links written out in full, as URLs or emails), every rich text field of the call (a body's texts included) is converted into the format and then validated the same way; raw HTML in it is kept as written, so a block element works, and a text already starting with <rich-text is kept as it is. Each text converts from at most 64 KiB of Markdown; a longer one is sent as HTML. Headings past the third level become subheadings, a list numbered from 0 starts at 1, a code block in a language outside the list keeps its code without one, a link's title and a table column's alignment are dropped, comments are dropped (one HTML ends at --!> before its --> is refused, since the Markdown after it would go unread), and an image is refused, since an image is a block. The texts inside a <block> element's props are always HTML.

Example

<h1>Welcome</h1><p style="text-align: center">Start with <strong>the intro</strong>, then <a href="https://example.com/notes" target="_blank">read the notes</a>.</p><ul><li data-checked="true"><p>Watch the intro</p></li><li data-checked="false"><p>Take the <span style="color: #047857">quiz</span></p></li></ul><details><summary>Why it works</summary><p>Short lessons stick.</p></details>

A block, in a field that takes images:

<block type="image" props="{&quot;id&quot;:&quot;cover&quot;,&quot;fileId&quot;:&quot;FILE_ID&quot;,&quot;alt&quot;:&quot;The cover&quot;}"></block>

A text in Markdown, with text_format: "markdown":

# Welcome

Start with **the intro**, then [read the notes](https://example.com/notes).

- [x] Watch the intro
- [ ] Take the quiz