MishkaCmsCore.Runtime.ContentComponents (Mishka CMS Core v0.0.1)

Copy Markdown View Source

Components inside content: a marker an author writes into stored HTML — a post's body — drawn as the runtime component it names. WordPress calls these shortcodes; Joomla, content plugins.

[[component chelekom-alert title="Heads up"]]Text, <b>with</b> HTML[[/component]]
[[component chelekom-badge size="small" /]]

A template draws such content with rich_content/1, imported into every runtime template — see MishkaCmsCore.Runtime.LiveViewHelpers.rich_content/1:

<.rich_content
  id={"#{@id}-body"}
  html={@body}
  site={site_name()}
  current_user={@current_user}
/>

The marker

  • [[component NAME ATTRS]]INNER[[/component]] — INNER is HTML, and becomes the component's inner_block slot when the component declares one.
  • [[component NAME ATTRS /]] — no inner content.
  • NAME is letters, digits, - and _. It names a component of the site being drawn, or a global one when the site has none of that name.
  • Each of ATTRS is key="value", key='value', key=value or a bare key, which is true. A quote may arrive as an entity — &quot;, &#34;, &#39;, &#x27;, &apos; — and a value is entity-decoded. A value cannot hold both kinds of quote: the sanitizer content is stored through decodes every quote entity back into the quote itself.
  • Whitespace is spaces, tabs, line breaks and the non-breaking space, written or as an entity.

A marker is found only in text: never inside a tag, and never inside <pre> or <code>, so a page can show the syntax. A marker that does not parse, an opening marker nothing closes and a closing marker nothing opened are left in the text as they are.

Inside a component

INNER may hold markers of its own, drawn inside the component — a timeline holding its sections, a card holding its title:

[[component chelekom-timeline]]
  [[component chelekom-timeline-section title="Started" /]]
  [[component chelekom-timeline-section title="Shipped" /]]
[[/component]]

and, for a component that declares named slots, a slot marker for each entry it takes:

[[component chelekom-video]][[slot source src="/intro.mp4" type="video/mp4" /]][[/component]]
[[slot item title="Question"]]The answer, as <b>HTML</b>[[/slot]]

A slot marker is read with the same rules as a component's, belongs to the component it sits directly inside, and is passed as that slot when the component declares it — its attributes cast against the slot's own. A slot marker anywhere else is text. Markers are drawn eight levels deep; the content of a deeper one is drawn as it is written.

What a component hands its content — render_slot(@inner_block, value), or a slot entry's render_slot(entry, value) — reaches each marker inside that content, at any depth, as runtime:render:before_content_component's slot_arg meta. A marker outside the component never sees it, and a component inside that hands nothing (render_slot(@inner_block)) passes on the one it was given.

A paragraph holding one marker and nothing else — whitespace and non-breaking spaces aside — is drawn as the component alone, without the <p> an editor wraps a line in: a block inside a <p> is HTML a browser takes apart. A marker sharing its paragraph with other text keeps it.

What is drawn

Only a component the request's site may render: active, and the site's own or a global one — the rule MishkaCmsCore.Runtime.LiveViewHelpers.component/1 holds every template to. The name is first checked, as a string, against the active components SiteState knows the site and the global scope have: a name an author typed that names none of them makes no module name, no atom and no remembered failure. A name that resolves to nothing, an inactive component, another site's, one that draws content itself (it could draw the content that holds it, forever), one missing an attribute it requires, and one whose list or map value cannot be read (see Lists and maps) are drawn as nothing. The same render in a preview (MishkaCmsCore.Runtime.StaticHelpers) draws a notice saying why in their place. On a page, one that passes every check and then raises while it draws is drawn as a small box, and the content around it draws — see MishkaCmsCore.Runtime.ComponentBoundary.

A component is handed what <.component> in a template would hand it: the attributes the marker writes, and for one it leaves out the default the component declares, or nothing at all when it declares none, as Phoenix leaves it. A clause matching %{navigate: _} is taken only when the marker writes navigate, and a template reads an attribute with no default as assigns[:label], never @label.

Only the attributes the component declares are read from the marker, each cast to its declared type: string and any as written, boolean from true/false or a bare key, integer and float from a number, list and map from JSON (see Lists and maps), and anything that declares values only as one of them — which is the only way an atom is taken. id, site, component_name, current_user, rest and any phx- name are never an author's. A stateful component, and one declaring id, gets the id "<id>-<n>", n counting the content's markers from 0 — "<id>-<n>-<m>" for the mth inside it — so it stays the same component across renders.

A component declaring a global attribute (attr :rest, :global, include: ~w(disabled circle)) takes, through it, the HTML attributes that attribute names and a few every element has: each name in its include list, aria-*, data-*, and accesskey, autocapitalize, autocorrect, autofocus, dir, draggable, enterkeyhint, hidden, inert, inputmode, lang, role, spellcheck, tabindex, title and translate. A bare key or "true" is true, "false" leaves it out, and anything else is the text written. Never an event handler (on…), style, a phx- or data-phx- name, data-method, data-to, data-csrf, method, csrf_token or a name the component declares, and a link or source (href, src, action, formaction, poster, cite, ping) only when it is a path or an http, https, mailto or tel address:

[[component my-video controls muted]][[slot source src="/intro.mp4" /]][[/component]]
[[component my-button circle disabled aria-label="Scan"]][[/component]]

A slot the component declares and the content does not fill is passed empty, and one it requires draws the component as nothing — as a required attribute does. A slot's attribute the marker leaves out is passed as its declared default, and an entry with nothing inside it draws nothing where the component renders its content.

The HTML around the markers is drawn as it is given: pass only HTML that was sanitized.

Lists and maps

An attribute declared list takes a JSON array, and one declared map a JSON object, written in single quotes around JSON's own double ones:

[[component chelekom-headless-tree nodes='[{"value":"src","label":"src","children":[{"value":"app","label":"app.ex"}]}]' /]]
[[component chelekom-headless-sparkline values='[3,5,2,8,6]' /]]
[[component chelekom-headless-segmented-control options='["Day","Week","Month"]' value="Week" /]]
[[component chelekom-headless-countdown seconds="93784" show_labels labels='{"days":"d","hours":"h"}' /]]

An any attribute that declares no values takes an array or an object the same way, and every other value as it is written: option='{"series":[]}' is a map, option="{draft" the text.

The value reaches the component as data: strings, numbers, true, false and null as Elixir's own, an array as a list, an object as a map. Nothing in it is run, and no string in it becomes a module or an atom. An object's keys are atoms when it is a record — an object inside an array, such as a tree's node read as node.label, and the object of an attribute whose declared default is a map with atom keys. Any other object keeps its keys as written: a lookup such as a countdown's labels, read by the strings its units hold. A key becomes an atom only when the VM already has that atom — as it has every key the component's code reads — and a key starting with __ is always a string.

A value is at most 32 KB as written, and 8 levels deep, each array and object a level: a tree's node is two. A ' inside the JSON is written \u0027, and &, < and >, as in any value, as entities. A list or map value that is not JSON of its kind, that is larger or deeper, or that is a bare key draws the component as nothing, and a preview says which attribute and why. An any value is then handed as it is written.

Cost

Content holding no [[component is drawn as it is, and nothing is parsed. A marker asks only what the runtime keeps in memory: SiteState for the site and its component names, then the loaded code table for the compiled module. The first render of a component nothing has built yet builds it, once, as component/1 does.

Styles

A site's stylesheet is built from the templates it reaches (MishkaCmsCore.Runtime.Compilers.CssClosure), and the components content names are in no template. Whoever stores content answers runtime:compile:content_components with the names its content uses — see MishkaCmsCore.Runtime.Compilers.TailwindCompiler.content_components/1 — and a template that draws content reaches exactly those. names/1 reads them out of content, to store beside it when it is written.

The icons content names are in no template either: a marker's icon="hero-inbox" reaches the component as an expression, and a <span class="hero-inbox"> in the HTML is never scanned. The same event answers them as icons — see MishkaCmsCore.Runtime.Compilers.TailwindCompiler.content_icons/1 — and icons/1 reads them out of content.

Nor are the classes content writes: a <div class="bg-[#282828]"> in the HTML, a marker's class, its *_class attributes, and the attributes its component takes a class string in. The same event answers them as classes — see MishkaCmsCore.Runtime.Compilers.TailwindCompiler.content_classes/1 — and site_classes/2 reads them out of content.

The editor

The admin's content editor holds content as segments/1 splits it: the HTML it edits, and each component standing alone as a block it shows drawn. The block writes its marker back as a paragraph of its own, so content saved from the editor stores what it was opened with. Its palette is palette/2.

Events

  • runtime:render:before_content_component — before each embedded component is drawn, with %{component_name: name, site: site, attrs: attrs, inner: inner_html | nil} and %{site_id: site_id, index: n, marker_attrs: written, slot_arg: arg, slot_args: args} as meta. attrs is atom-keyed and already cast; marker_attrs are the attributes as the marker writes them, string-keyed and uncast. slot_arg is the value the nearest component around the marker hands its content (nil when none does), and slot_args every such value, nearest first. A plugin answers the map with other attrs or inner to change what is drawn — an attribute taken out of attrs is not handed at all, for a component's own clause to fill, as a form field's name from its field — or suppress: true to draw nothing.
  • runtime:component:crashed — a marker's component raised while it was drawn, and a box was drawn in its place; see MishkaCmsCore.Runtime.ComponentBoundary.
  • runtime:content:editor_palette — before the content editor's palette is shown, with %{site_id: site_id, components: entries} (embeddable/2's entries) and %{current_user: user} as meta. A plugin answers the map with other components to narrow or extend what an author may place; the editor places only what the palette lists.

Summary

Types

An attribute a marker can set, as embeddable/2 lists it: group is the heading the page builder's Inspector files it under, untranslated, and media whether it holds a picture or a video.

A piece of content: HTML drawn as it is, or a marker.

A component an author may embed, as embeddable/2 lists it.

One marker, as parse/1 found it.

A piece of content as the editor holds it: HTML, or a component standing alone. marker is the marker as written, source the bytes it was cut from, the paragraph around it included.

Which clause of each of a component's helpers draws it: one list of clauses per helper, in order, each the attributes and the values it matches — %{} matching any — or :unknown when it matches on something no attribute holds; and the attributes whose every use in the template is through those helpers.

One component marker read apart, as read/1 gives it and write/1 takes it: its name, its attributes, and what it holds, in order — text, the components inside it, and its slot entries.

Functions

The attributes of given a component declaring declared takes, cast to their declared types.

The palette heading a component is listed under — "text", "media", "layout", "buttons", "data", "disclosure", "feedback", "navigation", "overlays", "forms", a kit's own type, or "other" — from its kit type and the words of its name.

A palette heading's name, in the reader's language.

Whether token could be a class: at most 512 bytes — an arbitrary value such as mask-linear-[to_bottom,transparent_0,black_min(40px,var(--edge)),…] runs past a hundred — printable ASCII holding a letter, its brackets and parentheses balanced, and none of ", \, {, }, <, ; or a backtick in it.

The class tokens html names, each once — what a resource stores beside its content when it is written, for the site's stylesheet. The HTML's own class lists come first, then the markers'

Draws assigns.html with its markers drawn as components — the body of rich_content/1.

Whether a component template draws content itself — calls rich_content. Such a component is never drawn inside content, which could hold a marker for it.

The components an author may embed on the site site_id, with the attributes a marker can set — what the editor's picker offers. nil is the global components alone.

The palette entry for a component row — a stored component, or anything with its fields — as embeddable/2 lists it.

The icon classes html names, each once, in the order they first appear — a marker's attribute ([[slot item icon="hero-inbox"]]) and the HTML's own classes (<span class="hero-inbox">) alike — what a resource stores beside its content when it is written, for the site's stylesheet.

Whether the attribute name of the palette entry draws nothing with the other attributes at values — the marker's, the rest at their defaults: the values of the other attributes that pick a clause of every helper it goes to that matches all of its own values alike, or nil when it draws something, or when that cannot be told.

The marker for component name with attrs and inner content — what the editor's Insert writes.

The component names the markers in html name, each once, in the order they first appear — those inside other markers too — what a resource stores beside its content when it is written, for the site's stylesheet.

The attributes each component names names takes a class string in — one declaring examples and no values, a string or an any — as classes/2 takes them. Read off the active components of the site site_id and the global ones, a site's own hiding the global one of the same name, as the system: the stylesheet is the same whoever wrote the content.

The components the content editor's palette offers on the site site_id, read as actor — embeddable/2, as a plugin answering runtime:content:editor_palette leaves it.

Splits html into the HTML drawn as it is and the markers in it, in order.

One component marker read apart into a tree/0 — what the content editor's settings edit. :error when marker is not exactly one component marker.

Splits html into the HTML the content editor edits and the components standing alone in it, which it holds as blocks.

The class tokens html names on the site site_id — classes/2, with the attributes the components it names take a class string in (open_attrs/2).

The slot marker for the slot name with attrs and inner content, written as marker/3 writes a component's.

categories' names in the order the palette shows them: text first, the ones above in their order, a kit's own after them by name, and Other last.

The classes a preview's notice is drawn with — a marker that draws nothing, or a component that could not be drawn — for whatever builds the stylesheet a preview is drawn with.

The marker for tree, with every component and slot inside it — read/1 the other way. nil when a name or a key in it could not be read back (marker/3).

Types

attr_entry()

@type attr_entry() :: %{
  name: String.t(),
  type: String.t(),
  values: [term()],
  default: term(),
  doc: String.t() | nil,
  required: boolean(),
  group: String.t(),
  media: boolean()
}

An attribute a marker can set, as embeddable/2 lists it: group is the heading the page builder's Inspector files it under, untranslated, and media whether it holds a picture or a video.

chunk()

@type chunk() :: {:html, String.t()} | {:component, marker()}

A piece of content: HTML drawn as it is, or a marker.

entry()

@type entry() :: %{
  name: String.t(),
  description: String.t() | nil,
  type: String.t() | nil,
  stateful: boolean(),
  global: boolean(),
  inner_block: boolean(),
  category: String.t(),
  examples: [String.t()],
  furnishing: MishkaCmsCore.Builder.Furnishing.t() | nil,
  switches: switches(),
  attrs: [attr_entry()],
  slots: [
    %{
      name: String.t(),
      doc: String.t() | nil,
      required: boolean(),
      inner: boolean(),
      attrs: [attr_entry()]
    }
  ]
}

A component an author may embed, as embeddable/2 lists it.

marker()

@type marker() :: %{
  name: String.t(),
  attrs: %{required(String.t()) => String.t() | true},
  inner: String.t() | nil,
  index: non_neg_integer()
}

One marker, as parse/1 found it.

segment()

@type segment() ::
  {:html, String.t()}
  | {:component,
     %{
       name: String.t(),
       attrs: %{required(String.t()) => String.t() | true},
       inner: String.t() | nil,
       marker: String.t(),
       source: String.t()
     }}

A piece of content as the editor holds it: HTML, or a component standing alone. marker is the marker as written, source the bytes it was cut from, the paragraph around it included.

switches()

@type switches() :: %{
  helpers: [[%{required(String.t()) => [String.t()]} | :unknown]],
  through: [String.t()]
}

Which clause of each of a component's helpers draws it: one list of clauses per helper, in order, each the attributes and the values it matches — %{} matching any — or :unknown when it matches on something no attribute holds; and the attributes whose every use in the template is through those helpers.

tree()

@type tree() :: %{
  name: String.t(),
  attrs: %{required(String.t()) => String.t() | true},
  parts: [
    text: String.t(),
    component: tree(),
    slot: %{name: String.t(), attrs: map(), parts: list()}
  ]
}

One component marker read apart, as read/1 gives it and write/1 takes it: its name, its attributes, and what it holds, in order — text, the components inside it, and its slot entries.

Functions

cast(declared, given)

@spec cast([map()], %{required(String.t()) => String.t() | true}) :: %{
  required(atom()) => term()
}

The attributes of given a component declaring declared takes, cast to their declared types.

declared is the component's attribute declarations, as __component_information__/1 or the row holds them; given is a marker's attrs. What is not declared, cannot be cast, or is one of the names an author never sets is left out. The HTML attributes a global attribute takes are gathered under its name, as the module documentation says.

cast([%{name: "count", type: "integer"}, %{name: "open", type: "boolean"}],
     %{"count" => "3", "open" => true, "phx-click" => "boom"})
#=> %{count: 3, open: true}

cast([%{name: "rest", type: "global", opts: %{"include" => ["circle"]}}],
     %{"circle" => true, "aria-label" => "Scan", "onclick" => "boom"})
#=> %{rest: %{:circle => true, "aria-label" => "Scan"}}

cast([%{name: "nodes", type: "list"}, %{name: "labels", type: "map"}],
     %{"nodes" => ~s([{"label":"src"}]), "labels" => ~s({"days":"d"})})
#=> %{nodes: [%{label: "src"}], labels: %{"days" => "d"}}

category(name, type)

@spec category(String.t(), String.t() | nil) :: String.t()

The palette heading a component is listed under — "text", "media", "layout", "buttons", "data", "disclosure", "feedback", "navigation", "overlays", "forms", a kit's own type, or "other" — from its kit type and the words of its name.

category("chelekom-h2", "general")   #=> "text"
category("chelekom-video", "media")  #=> "media"
category("acme-hero", "marketing")   #=> "marketing"

category_label(kit)

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

A palette heading's name, in the reader's language.

class?(token)

@spec class?(term()) :: boolean()

Whether token could be a class: at most 512 bytes — an arbitrary value such as mask-linear-[to_bottom,transparent_0,black_min(40px,var(--edge)),…] runs past a hundred — printable ASCII holding a letter, its brackets and parentheses balanced, and none of ", \, {, }, <, ; or a backtick in it.

class?("bg-[#282828]")
#=> true
class?("{brace}")
#=> false

classes(html, open \\ %{}, declared \\ %{})

@spec classes(String.t() | nil, %{required(String.t()) => [String.t()]}, %{
  required(String.t()) => map()
}) :: [String.t()]

The class tokens html names, each once — what a resource stores beside its content when it is written, for the site's stylesheet. The HTML's own class lists come first, then the markers':

  • every class="…" of an HTML tag, entities decoded;
  • a component's or a slot's class attribute, and every attribute named *_class;
  • a component's attribute open names for it — %{"chelekom-badge" => ["size", "variant"]}, what open_attrs/2 reads off the components a site reaches: an attribute that takes one of its examples or a class string of the author's own;
  • any other attribute of a component or a slot whose value is nothing but utilities — every token holding a -, : or [, and none an icon: height="h-16", relative="md:relative". Of a component declared holds the declaration of — %{"my-badge" => %{attrs: [...], slots: [...]}}, what site_classes/2 reads off the components a site reaches — and of its slots, only an attribute that declaration takes the value in (cast/2): a value outside an attribute's values never reaches the component.

A token is kept when it could be a class (class?/1). At most 1000 are kept.

classes(~s(<p class="text-sm bg-[#282828]">x</p>[[component chelekom-badge size="py-2 px-4" class="ml-1" /]]),
  %{"chelekom-badge" => ["size"]})
#=> ["text-sm", "bg-[#282828]", "py-2", "px-4", "ml-1"]

draw(assigns, mode)

@spec draw(map(), :live | :static) :: Phoenix.LiveView.Rendered.t()

Draws assigns.html with its markers drawn as components — the body of rich_content/1.

mode is :live where LiveView runs the result, and :static where it is rendered to a string: a stateful component is then drawn cold, and a marker that draws nothing says why.

draws_content?(template)

@spec draws_content?(String.t() | nil) :: boolean()

Whether a component template draws content itself — calls rich_content. Such a component is never drawn inside content, which could hold a marker for it.

embeddable(actor, site_id)

@spec embeddable(map() | nil, String.t() | nil) :: {:ok, [entry()]} | {:error, term()}

The components an author may embed on the site site_id, with the attributes a marker can set — what the editor's picker offers. nil is the global components alone.

Active components of the site and global ones, every one of them, read as actor through :for_tenant_with_globals; a site's own component hides the global one of the same name, as it does when drawn. Components that draw content themselves are left out, and so are those requiring an attribute the editor has no field for — a list or a map, which a marker is written with by hand (see Lists and maps), or a struct — and every such attribute, and every one cast/2 would drop.

entry(row)

@spec entry(map()) :: entry()

The palette entry for a component row — a stored component, or anything with its fields — as embeddable/2 lists it.

icons(html)

@spec icons(String.t() | nil) :: [String.t()]

The icon classes html names, each once, in the order they first appear — a marker's attribute ([[slot item icon="hero-inbox"]]) and the HTML's own classes (<span class="hero-inbox">) alike — what a resource stores beside its content when it is written, for the site's stylesheet.

A class is found by the prefixes MishkaCmsCore.Runtime.PageBuilder.Icons registers (Icons.classes_in/1), and kept only when a registered set ships it (Icons.known?/1): a hero-section in the text, or a prefix nothing registers, is left out.

icons(~s([[slot item icon="hero-cloud" /]]<span class="hero-inbox size-4"></span>hero-nope))
#=> ["hero-cloud", "hero-inbox"]

inert(arg1, name, values)

@spec inert(entry(), String.t(), map()) :: %{required(String.t()) => String.t()} | nil

Whether the attribute name of the palette entry draws nothing with the other attributes at values — the marker's, the rest at their defaults: the values of the other attributes that pick a clause of every helper it goes to that matches all of its own values alike, or nil when it draws something, or when that cannot be told.

inert(blockquote, "color", %{"variant" => "base"})
#=> %{"variant" => "base"}

marker(name, attrs, inner)

@spec marker(String.t(), map() | nil, String.t() | nil) :: String.t() | nil

The marker for component name with attrs and inner content — what the editor's Insert writes.

A blank value is left out, so the component's own default applies; a blank inner writes the self-closing form. Attributes are written in name order, &, < and > in a value as entities. A value holding both kinds of quote has its double quotes written as single ones, since stored content cannot carry an escaped quote. nil when name, or a key, could not be read back as one, or is a key no author sets.

marker("chelekom-alert", %{"title" => "Heads up", "size" => ""}, "Read <b>this</b>")
#=> ~s([[component chelekom-alert title="Heads up"]]Read <b>this</b>[[/component]])

names(html)

@spec names(String.t() | nil) :: [String.t()]

The component names the markers in html name, each once, in the order they first appear — those inside other markers too — what a resource stores beside its content when it is written, for the site's stylesheet.

names(~s([[component chelekom-alert /]]<p>x</p>[[component chelekom-card]][[component chelekom-alert /]][[/component]]))
#=> ["chelekom-alert", "chelekom-card"]

open_attrs(names, site_id)

@spec open_attrs([String.t()], String.t() | nil) :: %{
  required(String.t()) => [String.t()]
}

The attributes each component names names takes a class string in — one declaring examples and no values, a string or an any — as classes/2 takes them. Read off the active components of the site site_id and the global ones, a site's own hiding the global one of the same name, as the system: the stylesheet is the same whoever wrote the content.

open_attrs(["chelekom-badge"], site_id)
#=> %{"chelekom-badge" => ["size", "variant", "rounded"]}

palette(actor, site_id)

@spec palette(map() | nil, String.t() | nil) :: {:ok, [entry()]} | {:error, term()}

The components the content editor's palette offers on the site site_id, read as actor — embeddable/2, as a plugin answering runtime:content:editor_palette leaves it.

parse(html)

@spec parse(String.t()) :: [chunk()]

Splits html into the HTML drawn as it is and the markers in it, in order.

parse(~s(<p>Hi</p>[[component chelekom-badge size="small" /]]))
#=> [
#=>   {:html, "<p>Hi</p>"},
#=>   {:component, %{name: "chelekom-badge", attrs: %{"size" => "small"}, inner: nil, index: 0}}
#=> ]

Content with no [[component in it is one {:html, html}, found with a single binary match. Only the outermost markers are pieces: a marker inside another is part of that one's inner.

read(marker)

@spec read(String.t()) :: {:ok, tree()} | :error

One component marker read apart into a tree/0 — what the content editor's settings edit. :error when marker is not exactly one component marker.

read(~s([[component chelekom-card]][[component chelekom-card-title title="Hi" /]]Text[[/component]]))
#=> {:ok, %{name: "chelekom-card", attrs: %{}, parts: [
#=>   {:component, %{name: "chelekom-card-title", attrs: %{"title" => "Hi"}, parts: []}},
#=>   {:text, "Text"}
#=> ]}}

segments(html)

@spec segments(String.t()) :: [segment()]

Splits html into the HTML the content editor edits and the components standing alone in it, which it holds as blocks.

A marker stands alone when its paragraph holds nothing else — the marker parse/1 draws without the <p> — or when it sits inside no element at all. A marker sharing its paragraph with text stays in the HTML around it. Each component's source is what it was cut from, so the pieces' HTML and the components' source, joined in order, are html again.

segments(~s(<p>Hi</p><p>[[component chelekom-badge size="small" /]]</p>))
#=> [
#=>   {:html, "<p>Hi</p>"},
#=>   {:component, %{name: "chelekom-badge", attrs: %{"size" => "small"}, inner: nil,
#=>     marker: ~s([[component chelekom-badge size="small" /]]),
#=>     source: ~s(<p>[[component chelekom-badge size="small" /]]</p>)}}
#=> ]

site_classes(html, site_id)

@spec site_classes(String.t() | nil, String.t() | nil) :: [String.t()]

The class tokens html names on the site site_id — classes/2, with the attributes the components it names take a class string in (open_attrs/2).

slot_marker(name, attrs, inner)

@spec slot_marker(String.t(), map() | nil, String.t() | nil) :: String.t() | nil

The slot marker for the slot name with attrs and inner content, written as marker/3 writes a component's.

slot_marker("source", %{"src" => "/a.mp4", "type" => "video/mp4"}, nil)
#=> ~s([[slot source src="/a.mp4" type="video/mp4" /]])

sort_categories(categories)

@spec sort_categories([String.t()]) :: [String.t()]

categories' names in the order the palette shows them: text first, the ones above in their order, a kit's own after them by name, and Other last.

unavailable_class()

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

The classes a preview's notice is drawn with — a marker that draws nothing, or a component that could not be drawn — for whatever builds the stylesheet a preview is drawn with.

write(arg1)

@spec write(tree()) :: String.t() | nil

The marker for tree, with every component and slot inside it — read/1 the other way. nil when a name or a key in it could not be read back (marker/3).