Quillon.HTML (Quillon v0.4.0)

Copy Markdown View Source

Render Quillon documents to HTML.

Rendering is one-way: an AST goes in, a string of HTML comes out. Text and attribute values are escaped, so a document holding untrusted text is safe to embed in a page.

iex> doc = Quillon.document([Quillon.paragraph("Hello")])
iex> Quillon.HTML.to_html(doc)
~s(<div class="quillon"><p>Hello</p></div>)

Layout and styling tokens

Block nodes carry constrained tokens (align: :center, spacing: :lg) rather than CSS. By default those become data-* attributes, so the output is framework-agnostic and any stylesheet can target them:

iex> Quillon.HTML.to_html(Quillon.paragraph("Hi", align: :center))
~s(<p data-align="center">Hi</p>)

To map tokens onto a design system, pass :classes - either a module implementing Quillon.HTML.Classes or a two-argument function. Quillon ships Quillon.HTML.Tailwind:

iex> Quillon.HTML.to_html(Quillon.paragraph("Hi", align: :center),
...>   classes: Quillon.HTML.Tailwind)
~s(<p class="leading-relaxed text-center">Hi</p>)

A mapper receives every token as class(property, value) and the node itself as class(:node, type). Returning nil emits nothing. When a mapper is given the data-* attributes are dropped - the design system owns presentation.

Custom node types

A node type the renderer does not know about still renders. It becomes a <span> when it holds only inline content and a <div> otherwise, tagged with the type so styling and round-tripping have something to hold on to:

iex> line = {:line, %{page: 2}, [Quillon.text("Hello")]}
iex> Quillon.HTML.to_html({:paragraph, %{}, [line]})
~s(<p><span data-node="line">Hello</span></p>)

The same applies to marks, which render as <span data-mark="...">.

Cursors

Pass :cursors to draw other people's carets into the page. Each entry is a map holding a Quillon.Selection and whatever identifies its owner:

iex> doc = Quillon.document([Quillon.paragraph("Hello")])
iex> Quillon.HTML.to_html(doc,
...>   cursors: [%{label: "alice", selection: Quillon.Selection.cursor([0], 2)}])
~s(<div class="quillon"><p>He<span data-cursor data-cursor-label="alice"></span>llo</p></div>)

A collapsed selection becomes an empty <span data-cursor> at the caret. An expanded one also wraps what it covers in <span data-cursor-range>, as does a node selection around its node. Position them with CSS; the renderer emits the hooks and takes no view on how a caret looks.

Cursors are never part of the document. They are passed in per render, so the same document draws differently for each viewer.

Summary

Types

A token-to-class mapper: a module implementing Quillon.HTML.Classes, or a function taking the property and its value.

Someone's caret: a Quillon.Selection plus whatever identifies its owner. The :id, :label and :color keys are optional and become data-cursor-* attributes.

Rendering options

Functions

Escape a string for use in HTML text or attribute values.

Render a node and its subtree to an HTML string.

Render a node to iodata.

Types

classes()

@type classes() :: module() | (atom(), any() -> String.t() | nil) | nil

A token-to-class mapper: a module implementing Quillon.HTML.Classes, or a function taking the property and its value.

cursor()

@type cursor() :: %{
  :selection => Quillon.Selection.t(),
  optional(:id) => String.t(),
  optional(:label) => String.t(),
  optional(:color) => String.t()
}

Someone's caret: a Quillon.Selection plus whatever identifies its owner. The :id, :label and :color keys are optional and become data-cursor-* attributes.

opts()

@type opts() :: [classes: classes(), cursors: [cursor()]]

Rendering options

Functions

escape(text)

@spec escape(String.t()) :: String.t()

Escape a string for use in HTML text or attribute values.

Examples

iex> Quillon.HTML.escape(~s(<a href="x">Tom & Jerry</a>))
"&lt;a href=&quot;x&quot;&gt;Tom &amp; Jerry&lt;/a&gt;"

to_html(node, opts \\ [])

@spec to_html(tuple(), opts()) :: String.t()

Render a node and its subtree to an HTML string.

Examples

iex> Quillon.HTML.to_html(Quillon.heading(2, "Title"))
~s(<h2>Title</h2>)

iex> Quillon.HTML.to_html(Quillon.text("Bold", [:bold]))
~s(<strong>Bold</strong>)

iex> Quillon.HTML.to_html(Quillon.text("5 > 3 & <b>"))
~s(5 &gt; 3 &amp; &lt;b&gt;)

to_iodata(node, opts \\ [])

@spec to_iodata(tuple(), opts()) :: iodata()

Render a node to iodata.

Use this when the result is written straight to a socket or file - it skips building an intermediate binary.

iex> Quillon.HTML.to_iodata(Quillon.paragraph("Hi")) |> IO.iodata_to_binary()
~s(<p>Hi</p>)