A Kotoba.Content holds the document and, cached, its HTML and its plain text. This guide shows how to show the content in a page, how to render a document as HTML, text or Markdown elsewhere (an email, a search index, a feed), how to style the HTML, and how to make content without the editor.

In a page

<.kotoba_content content={@post.body} class="prose" />

Kotoba.Components.kotoba_content/1 renders the content in a div.kotoba-content, with the class and the other attributes that you give. nil renders an empty div.

It uses the cached HTML, which Kotoba.Content made when it cast the content, so a page does not render the document again. It renders again when you give a policy other than :default, when you give nodes, or when the cache is from another version of Kotoba:

<%!-- A comment from a person that the app does not trust --%>
<.kotoba_content content={@comment.body} policy={:untrusted} />

<%!-- App nodes that are not in `config :kotoba, nodes:` --%>
<.kotoba_content content={@post.body} nodes={[MyApp.Nodes.Chart]} />

The HTML is always safe: Kotoba escapes every string and never passes raw HTML through, and each node passes the checks of Kotoba.Sanitizer before it renders. See the Security guide.

The policies

  • :default: links are anchors (with rel="noopener nofollow"), and attachments are figures with their image or a download link.
  • :untrusted: links render as their text, with no anchor, and attachments as their caption (else their file name), with no image and no link.

The HTML

ContentHTML
paragraph, heading, quotep, h1 to h4, blockquote
listsul, ol (with start), li
check listul.kotoba-check, with li.kotoba-checked and li.kotoba-unchecked
formatsstrong, em, u, s, mark, sub, sup, code
text color, highlight colorspan.kotoba-color-<name>, mark.kotoba-highlight-<name>
case formatsspan.kotoba-lowercase, span.kotoba-uppercase, span.kotoba-capitalize
code blockpre with code.language-<language>
line break, rulebr, hr
tablediv.kotoba-table-scroll with a table.kotoba-table: thead for a header row, tbody, th scope="col"/scope="row", colspan and rowspan
linka with href and rel="noopener nofollow"
attachmentfigure.kotoba-attachment with an img (its alt the caption, else the file name), and a figcaption with the caption (else the file name); a PDF (figure.kotoba-attachment-pdf) has an object (or a link to it with the preview image), a video (figure.kotoba-attachment-video) a video with controls; other files a download link
gallerydiv.kotoba-gallery with the figure of each attachment (a grid in kotoba.css)
mentionspan.kotoba-mention with data-kind and data-id
an app nodewhat its render_html/2 gives (see Custom nodes)
a node that fails a check, or of an unknown typean empty span.kotoba-unknown with data-type

Kotoba.Renderer has the full table.

Style the HTML

kotoba.css gives the rendered HTML a readable baseline. It styles headings, paragraphs, lists, quotes, links, inline code and code blocks, as well as the structures that need their own layout:

  • tables: borders, padding, header cells, and the sideways scroll;
  • check lists: a box, checked or not, in front of each item;
  • mentions and attachments;
  • the text colors and highlights of the palette (see Theming);
  • the case formats.

Each rule of the baseline has the weight of one class (.kotoba-content :where(a)): it wins over element resets such as a { color: inherit } or Tailwind's preflight, and a rule of your app with a class (.article .kotoba-content a) wins over it. With Tailwind, the typography plugin is an option; import kotoba.css in a layer so that the plugin's utilities come after it:

@import "../../deps/kotoba/priv/static/kotoba.css" layer(components);
<.kotoba_content content={@post.body} class="prose dark:prose-invert" />

A rendered code block has its language in the class (language-elixir). The server returns plain code text; for syntax colors in a browser, import highlightRenderedContent from kotoba and call it after the page loads or LiveView replaces the rendered document. It uses the Prism grammars already bundled with the editor and does not change the page's own window.Prism:

import { highlightRenderedContent } from "kotoba"
window.addEventListener("DOMContentLoaded", () => highlightRenderedContent())
window.addEventListener("phx:page-loading-stop", () => highlightRenderedContent())

HTML, text and Markdown anywhere

Kotoba.Content has the HTML (content.html, a string) and the plain text (content.text, the blocks one per line). Use the text for a search index, a preview or a notification:

String.slice(post.body.text, 0, 140)

Kotoba.Renderer renders a Kotoba.Document again, for another policy or for Markdown:

{:ok, doc} = Kotoba.Document.parse(post.body.doc)

doc |> Kotoba.Renderer.to_html(policy: :untrusted) |> Phoenix.HTML.safe_to_string()
Kotoba.Renderer.to_text(doc)
Kotoba.Renderer.to_markdown(doc)

to_markdown/2 writes headings, lists and check lists, quotes, code blocks, bold, italic, strikethrough, inline code, links, images, rules, tables with their column alignment, and footnotes (GitHub Flavored Markdown) as Markdown, and escapes the text, so that Kotoba.Content.from_markdown/2 reads it back to the same document. Other formats (underline, highlights, colors, subscript, superscript) and mentions give their text. An app node gives what its render_markdown/2 gives.

A blank line that a person typed (an empty paragraph between blocks) keeps its height in HTML and is &nbsp; in Markdown. To leave blank lines out, render with blank_lines: :collapse, or give the editor blank_lines={:collapse}, which leaves them out of the saved document.

For an email, render the HTML with the :untrusted policy when the content comes from people outside the app, and inline the styles that the email needs: an email client does not load kotoba.css.

Read a document

Kotoba.Document.parse/2 gives the document's nodes, as structs (Kotoba.Nodes):

{:ok, doc} = Kotoba.Document.parse(post.body.doc)

Kotoba.Document.mentions(doc)     # every Kotoba.Nodes.Mention
Kotoba.Document.attachments(doc)  # every Kotoba.Nodes.Attachment
Kotoba.Document.empty?(doc)       # no text, no attachment, no other node
Kotoba.Features.used(doc)         # the features it uses

Kotoba.Document.reduce/3 walks every node, for anything else.

Content without the editor

  • Kotoba.Content.cast/1 takes the JSON that the editor posts, a document envelope, a bare Lexical root node, or a Kotoba.Content.
  • Kotoba.Content.from_markdown/2 makes content from Markdown: CommonMark and GitHub Flavored Markdown (tables, task lists, strikethrough, autolinks, footnotes), read by MDEx. Raw HTML is text. A list item holds inline content: an item with several paragraphs has line breaks, and a code block, a quote or a table in an item comes after the list. features: gives the features of a field: a construct whose feature is off becomes its closest form. It is handy for seeds, tests and imports, and the editor uses it for pasted Markdown.
  • Kotoba.Content.empty/0 is an empty document.
Repo.insert!(%Post{title: "Hello", body: Kotoba.Content.from_markdown("# Hello\n\nFirst **post**.")})

After a change of the registry

The cached HTML stays as it was rendered. After a change of your node registry (config :kotoba, nodes:), of a node's render_html/2, or of config :kotoba, allowed_link_schemes:, render the stored rows again with Kotoba.Content.rerender/2. See the Known limits guide.