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 (withrel="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
| Content | HTML |
|---|---|
| paragraph, heading, quote | p, h1 to h4, blockquote |
| lists | ul, ol (with start), li |
| check list | ul.kotoba-check, with li.kotoba-checked and li.kotoba-unchecked |
| formats | strong, em, u, s, mark, sub, sup, code |
| text color, highlight color | span.kotoba-color-<name>, mark.kotoba-highlight-<name> |
| case formats | span.kotoba-lowercase, span.kotoba-uppercase, span.kotoba-capitalize |
| code block | pre with code.language-<language> |
| line break, rule | br, hr |
| table | div.kotoba-table-scroll with a table.kotoba-table: thead for a header row, tbody, th scope="col"/scope="row", colspan and rowspan |
| link | a with href and rel="noopener nofollow" |
| attachment | figure.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 |
| gallery | div.kotoba-gallery with the figure of each attachment (a grid in kotoba.css) |
| mention | span.kotoba-mention with data-kind and data-id |
| an app node | what its render_html/2 gives (see Custom nodes) |
| a node that fails a check, or of an unknown type | an 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 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 usesKotoba.Document.reduce/3 walks every node, for anything else.
Content without the editor
Kotoba.Content.cast/1takes the JSON that the editor posts, a document envelope, a bare Lexical root node, or aKotoba.Content.Kotoba.Content.from_markdown/2makes 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/0is 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.