:::meta
description: How to give Tern's element names meaning, look and behaviour: the schema and every schema field, window.TERN.schema, add-ons, tern.define, custom transforms, styling, and labels in other languages.
:::
::site-nav
# Vocabulary and behaviour {#top}
::toc
Tern's core knows how a name is *written*, as in `:::theorem`, `::toc` or `:kbd[Ctrl]`, but not what it *means*. A **schema** gives names their meaning: the element, a label such as "Theorem 2", a counter, how a reference reads, a title slot, an end mark, a table of contents. The schema's `css` and `dom` fields, `tern.style` and `tern.define` give them a look and a behaviour.
This page covers all of it, from a note that declares three names in its head to an add-on shared by a hundred notes.
The live examples run with **no schema** unless they say `schema=demo`. The demo schema is the small vocabulary listed at the [end of this page](#demo-schema).
## Why names need a schema {#why}
The core gives a name no meaning. `:::theorem` is `div.t-block.theorem` until a schema says otherwise: no label, no number, and a reference to it reads its title.
```tern {.live}
:::theorem[Rank–nullity]{#rn}
$\dim V = \dim \ker T + \dim \operatorname{im} T$.
:::
See @rn.
```
The same note with the demo schema, which declares `theorem`:
```tern {.live schema=demo}
:::theorem[Rank–nullity]{#rn}
$\dim V = \dim \ker T + \dim \operatorname{im} T$.
:::
See @rn.
```
The difference is one entry:
```js
theorem: { tag: 'section', counter: 'theorem', label: 'Theorem', ref: '{label} {n}' }
```
**Why the core declares nothing.** Every subject needs its own names: a lecture has theorems, a lab notebook has protocols, a cookbook has steps. Labels differ from one language to the next. And a note must render from one script line, without a standard library to keep in step with it. So a note, or an add-on it loads, declares exactly the names it uses.
A name without an entry still renders. It keeps its class and its `data-t`, so CSS and behaviours can find it, and it becomes an element on its own when the name is one:
- an allowlisted element: `:::aside` is an ``;
- a custom element: `:::tide-panel` is a ``.
[Elements and output](elements.html#names) has the whole resolution.
:::table[What a schema entry adds]{#tbl-adds}
| | Without an entry | With an entry |
|---|---|---|
| Element | `div` or `span`, or the name itself when it is an element | `tag` |
| Classes and attributes | `t-block`, the name, the author's | `class`, `attrs` |
| Label | none | `label` |
| Number | none | `counter` |
| Reference text | the title, else the id | the label and number, through `ref` |
| Title element | written titles only | `title`, `titleDefault`, and a slot for the label |
| End mark | none | `end` |
| Body | blocks, or verbatim with `{raw}` | `body` |
| Generated content | none | `transform: 'toc'` |
| Look | the base stylesheet | `css` |
| Behaviour | `tern.define` only | `dom` |
:::
**Where vocabulary comes from.**
1. **The note's own head:** `window.TERN.schema`, in a script before the tern.js line ([below](#window-tern)). Best for a note that stands alone.
2. **An add-on** listed in `data-use`, which calls `tern.block`, `tern.leaf` and `tern.inline` ([Add-ons](#addons)). Best for vocabulary shared by several notes.
3. **Node**, for scripts that call the engine: the same three functions, or a `schema` option on each call ([JavaScript API](api.html#registry)).
When a note and an add-on declare the same name, the note's entry wins.
## The schema {#schema}
### The registry {#the-registry}
A schema is plain data: an object with three levels, each mapping a name to an entry.
```js
{
block: { theorem: { tag: 'section', counter: 'theorem', label: 'Theorem' } },
leaf: { toc: { tag: 'nav', transform: 'toc' } },
inline: { key: { tag: 'kbd' } },
}
```
In the browser this object is `tern.schema`, the registry. Three functions write it:
:::syntax
```js
tern.block(name, spec) // :::name … :::
tern.leaf(name, spec) // ::name[…]{…}
tern.inline(name, spec) // :name[…]{…}
```
:::
- **The entry.** Each function stores a shallow copy of `spec` and returns it. Changing your `spec` object afterwards changes nothing; changing the stored entry does: `tern.schema.block.note.label = 'Hinweis'`.
- **Again.** Registering a name a second time replaces its entry whole.
- **Refused.** They throw a `TypeError`, and register nothing, when:
- the name is not a NAME: a letter, then letters, marks, digits, `_` or `-`;
- the name is reserved: `base body head html link main meta noscript script style template title`, `tern`, any `t-…` name, and `end`;
- a block name is one of the core containers `meta`, `macros`, `script`, `style` or `html`;
- `spec` is not an object.
- **When.** The registry is read each time a note is rendered. Declare entries in an add-on or in `window.TERN.schema`: both are applied before the note is mounted. A `
::toc
## The theorem
:::theorem[Rank–nullity]{#rn}
$\dim V = \dim \ker T + \dim \operatorname{im} T$.
:::
:::proof
Extend a basis of $\ker T$ to a basis of $V$.
:::
## Using it
By @rn, … Press :key[Ctrl].
```
The toc is a `nav` with two entries, the theorem a `section` labelled "Theorem 1", the proof ends with ∎, `@rn` reads "Theorem 1", and `:key[Ctrl]` is a ``.
:::note
The examples in this section are source, not live. A live example renders with the engine alone, and `tern.toHTML(source, {head})` reads the head only for what `:::meta` and the labels need: its ``, the `lang` and `dir` of ``, and its ` ` tags. It runs no script. Running the head's scripts is the job of the browser runtime and the command line. Under node, pass the vocabulary as the `schema` option instead.
:::
**The rules.**
- **Shape.** `window.TERN.schema` holds `block`, `leaf` and `inline`, each optional, with entries as for `tern.block`. Function fields (`label`, `dom`) and `css` work as in an add-on.
- **Where.** In a classic `
```
or in a [`:::use` block](syntax-blocks.html#use) at the top of the note, one or more per line:
```html
:::use
lib/callout.js
lib/theme.css
:::
```
### Listing add-ons {#data-use}
- **The list** is space-separated on the tag, and one or more entries per line in the block. Each entry is a URL, relative to the page like any `src`.
- **Two places, one list.** `data-use` is fine for a single add-on; `:::use` is the readable form for several. Both are honoured and merge: the tag's entries load first, then the block's, in order.
- **`:::use` must lead the note:** at the top level, before any content; only `:::meta`, blank lines and other `:::use` blocks may come before it. tern.js reads it from the file as it arrives, so its add-ons start loading almost as early as the tag's, and the tools read it before they analyse. One anywhere else gets [`use.late`](diagnostics.html#use-late) and is ignored. The block emits nothing.
- **Stylesheets and scripts.** An entry whose path ends in `.css` is a stylesheet; anything else is a script.
- **Order matters.** Scripts run in the order listed, so a later add-on sees an earlier one's entries and can change them. Stylesheets cascade in the order listed: a theme listed after an add-on wins at equal specificity.
- **Under node**, the command line reads the entries the same way: every entry that does not end in `.css` is run as a classic script, `.mjs` or no extension included. One that does not parse as a classic script, or names a directory, is `addon.failed`.
### Options in the query string {#use-options}
An add-on takes options from its URL's query string, in `tern.config.use`:
```html
```
```js
tern.config.use.callout // {collapse: 'all', accent: 'teal'}
```
A `:::use` entry takes the same query string, `lib/callout.min.js?collapse=all&accent=teal`, with `&` written as it is: the block's body is verbatim.
- **The key** is the file name without its directory, `.min`, and `.js`, `.mjs` or `.css`: `lib/callout.min.js` is `callout`.
- **Every entry** gets an object, empty when there is no query.
- **Values** are strings. A key without `=` gets `''`.
- **Defaults.** `window.TERN.use = {callout: {collapse: 'none'}}` gives defaults; the query string wins key by key.
- **`&`** is the strict HTML spelling of `&` in an attribute; both work.
- **Under node, `tern.config.use` is empty.** Read options only for behaviour and style, never for vocabulary, or `tern check` and `tern build` would not see what the browser sees ([Under node](#addons-under-node)).
### How they load {#addon-loading}
1. When tern.js starts, it adds a preload for every script add-on, then appends them all at once: they **download in parallel** and **run in the order listed**. Stylesheets are added as links, in order.
2. The note is rendered once the document is parsed and **every add-on has loaded or failed**, or 12 seconds after the document was parsed, whichever comes first ([the lifecycle](api.html#capture)).
3. So an add-on runs **before the mount**. `window.tern` exists; `window.TERN.schema` is not applied yet; the note's elements do not exist yet.
An add-on therefore registers things, for the runtime to apply when the note is rendered:
- vocabulary: `tern.block`, `tern.leaf`, `tern.inline`;
- behaviours: `tern.define`;
- listeners: `tern.on`;
- transforms: `tern.transform`;
- styles: `tern.style`.
It does not look for the note's elements itself: they are not there yet.
### When an add-on fails {#addon-failed}
**`addon.failed`** is an error at the tern.js line. The note always renders. It is reported when:
- a script or stylesheet does not load (a wrong path, a network error): the note renders without it;
- a script throws while it runs: the note renders with what the add-on registered before the error, and the browser's console shows where it threw;
- a transform the add-on registered throws when it runs: that transform is skipped ([Custom transforms](#transforms)).
The command line and the language server report the same cases, and a few of their own:
- a missing file, or a directory;
- a script that does not parse as a classic script;
- a script that uses what the [node shim](#addons-under-node) does not provide.
**A remote add-on** (`https://…`) is not loaded under node. That is **`addon.remote`**, an info at the tern.js line: names it declares are unknown to `tern check`, which does not fail because of it. `tern build` warns about it: the page it writes uses those names without their schema entries.
**Diagnostics:** [`addon.failed`](diagnostics.html#addon-failed), [`addon.remote`](diagnostics.html#addon-remote).
## Writing an add-on {#writing-an-addon}
### A complete add-on {#callout-addon}
This add-on declares three callouts, `note`, `tip` and `warning`, with labels in three languages, a stylesheet, and a behaviour that folds a callout away.
```js [callout.js, a complete add-on]{#lst-callout}
// callout.js: callouts for Tern notes, in English, German and French.
//
//
(function () {
'use strict';
const tern = window.tern;
// 1. Vocabulary: three containers with a label in three languages.
const labels = {
note: { en: 'Note', de: 'Hinweis', fr: 'Remarque' },
tip: { en: 'Tip', de: 'Tipp', fr: 'Astuce' },
warning: { en: 'Warning', de: 'Warnung', fr: 'Attention' },
};
for (const name of Object.keys(labels)) {
tern.block(name, {
tag: 'aside',
class: 'callout',
attrs: { role: 'note' },
label: labels[name],
dom: collapsible,
});
}
// 2. Look: keyed on the classes, never on the tag.
tern.style(`
.tern .callout { border-inline-start: 4px solid var(--callout, var(--t-link));
background: var(--t-soft); padding: .6rem 1rem; border-radius: 6px; }
.tern .callout.tip { --callout: #2f8a46; }
.tern .callout.warning { --callout: var(--t-warn); }
.callout-toggle { margin-inline-start: .5rem; font: inherit; cursor: pointer; }
@media print { .callout-toggle { display: none; } }
`);
// 3. Behaviour: a callout with data-collapse, or every callout with
// ?collapse=all, gets a button that folds its body away.
const options = tern.config.use.callout || {}; // empty under node
function collapsible(el) {
const title = el.querySelector(':scope > .t-title');
if (!title || !(options.collapse === 'all' || el.hasAttribute('data-collapse'))) return;
const button = document.createElement('button');
button.type = 'button';
button.className = 'callout-toggle';
button.setAttribute('aria-expanded', 'true');
button.textContent = 'Hide';
button.addEventListener('click', () => {
const open = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', String(!open));
button.textContent = open ? 'Show' : 'Hide';
for (const child of el.children) if (child !== title) child.hidden = open;
});
title.append(button);
}
})();
```
A note that uses it:
```html
:::warning[Strom]{#w data-collapse}
Erst den Stecker ziehen.
:::
```
The engine writes this (as `tern build` shows it, without positions), and the behaviour then adds the button to the title:
```html
Warnung Strom
Erst den Stecker ziehen.
```
What each part does:
- **The wrapper** keeps the add-on's names out of the page's globals, and reads `window.tern`, which tern.js defined before any add-on runs.
- **The vocabulary.** The label map picks "Warnung" from `` ([Labels in other languages](#languages)). `class` and `attrs` give every callout the class `callout` and `role="note"`.
- **The look.** `tern.style` puts the rules at this add-on's place in the cascade and adds the page's nonce. Most of the rules use tern's custom properties, so they follow dark mode ([Styling](#styling)).
- **The behaviour** reads a per-element option from a `data-*` attribute (`{data-collapse}`), and a per-note option from the query string.
### Guidelines {#addon-guidelines}
- **Register everything while the script runs.** The mount comes after; nothing registered later applies to the note until the next render.
- **Key CSS and behaviours on the class or `data-t`, never on the tag.** An author can change the tag with `{tag=…}`.
- **Use `tern.style`** rather than adding your own `
```
**The cascade.** Tern's stylesheets form one group in the head, placed before the head's first stylesheet of your own, in this order:
1. the base, `tern.css`;
2. each add-on in `data-use` order: a CSS add-on's link, or the `tern.style` sheets a script add-on adds;
3. other `tern.style` sheets: from `window.TERN.schema` entries, from note scripts.
Your own head styles come after the group, and a `