Skip to content

Latest commit

 

History

History
566 lines (445 loc) · 96.3 KB

reference.md

File metadata and controls

566 lines (445 loc) · 96.3 KB

Reference

English | 日本語

Every user-facing option, syntax and key, one line each, taken from the code. The normative text of the slide and script syntax, the timeline, emphasis and morphs is the format specification §8 and §5.5; this page is a quick reference to Studio's implementation, and contract.md lists which section says what and adds Studio's own notes. Workflow guides: README, dev-server.md, video.md.

Contents: Slide syntax · Script · Timing · Themes · Parts · Presenting · Dev server · Quality checker · Build · TTS · Video export · Tools · Environment variables

Slide syntax

Normative: format spec §8.0 (slides, steps, kinds of slide, math, references) and §8.1 (slide ids). The rows below add Studio's positions, hooks and warnings.

Files in the working folder

File Read by Purpose
deck.html build the slides (any name; examples/minimal/deck.html is the template)
narration.md build, TTS, export script per step
timeline.json build per-step timing overrides
macros.tex build KaTeX macros (\newcommand, \renewcommand, \def; added to the default macros)
audio/manifest.json build written by tts/synth.py and the editor app's Audio panel (each step: file, dur, cues, sync, kind, inputHash, for TTS recipe; optional processing and startOffset, the encoder delay at which playback starts)
voice.json TTS voices, language, engine
figs/*.svg, data/*.json build for {{FIG:…}} / {{JSON:…}}
components/<name>/index.js build lesson-local parts (found before the library's)
egatari.json build, Egatari file docId, metadata, attachments; components pins library part versions ({"bars": "1.0.0"}, format spec §3.3; an Egatari file's copy carries the versions its export used, and unpacking writes them, so a rebuild does not switch versions; without a pin the build uses the default in components.lock.json)
themes/<name>/ build lesson-local themes
comments.json, .cache/edits/ dev server written by the dev server
out/<name>.html — default build output

Sections and classes

Markup Meaning
<section class="slide"> inside <div id="stage"> one slide (1280×720). <h2> heading, <div class="body"> body, <div class="concl"><span>…</span></div> conclusion line
class="slide cover" cover. Its children are .venue (venue or course, top 120px), <h1> (title, top 200px; a subtitle goes on a second line after <br>, as in <h1>Title<br>Subtitle</h1>, or into .venue) and .au (authors and affiliation, top 450px; several lines with <br>); theme logos are added at the bottom left (.logos). These are absolutely positioned, and the cover has no .body: any other element (such as <p class="sub">) is not placed and lands at the top edge over the band. Place other text yourself with position:absolute and top in the slides' <style>, in free space. The quality check warns when text goes off the slide or overlaps other text or the band (textOffSlide, textOverlap)
data-nologos on the cover no theme logos on this cover (also skipped when the cover already has a .logos element)
class="slide sec" chapter divider: the title is <h1> (an <h2> is shown the same way, and the quality checker warns) (<h1> at top 300px, .lead at top 390px; other text is placed as on the cover). Starts a chapter for --full video export
class="slide sec appendix" appendix divider. Page numbers after it read 付録 / Appendix. Without appendix, a sec slide whose text contains 付録 or Appendix also counts
class="… static" no steps at all: everything visible, .concl is not a separate step
class="… nostep" do not turn list items into steps automatically
class="… noconcl" no conclusion line: the body may extend to 680px instead of 565px (by default; a theme's metrics moves both)
class="… layout-tate" fully vertical slide: the heading in a vertical column at the right edge, the body in vertical writing to its left, the conclusion as a vertical column at the left edge (Japanese language arts)
id="…" on a section the slide's id, which is its key (format §8.1). Every slide has one. See Slide keys
data-morph / data-morph="key" morph in from the previous slide / from the slide with that key (contract §8)
data-chapter="name" start a chapter here with this name (like a sec slide)
<div class="pn"> as a direct child your own page number (otherwise one is added from slide 2 on: i/N)
<html lang="en"> English lesson: Source Sans 3 body font, English UI text, English quality-check standards, voxcpm2 TTS
<html data-theme="name"> theme (overridden by --theme)
<html data-mode="talk"> default mode when the URL has no ?mode= (talk or video)
<html data-feedback-url="URL"> where Send feedback points (default https://github.com/egatari/egatari-studio/issues/new). For a GitHub /issues/new URL, template=feedback.yml is added; version, browser, os and context (lang, mode) are added as query parameters
<html data-feedback="off"> no feedback link in the ? help or the presenter view (DECK.FEEDBACK_URL = null)
<html data-favicon="off"> no tab icon. By default a built lesson (normal, --portable, --play-only) gets the Egatari mark as a <link rel="icon"> data: URI at the top of <head> (light and dark colours in one SVG, no network request); slides that have their own <link rel="icon"> keep it and get nothing added
<html data-font="mincho"> body text in BIZ UDPMincho (--font-sans: var(--font-mincho)); see Japanese language arts

Body classes (all colors come from the theme): .sub subheading · .big main equation · .note note (27px) · .thm theorem box · .box tinted box · .cols side-by-side columns · .arrow centered arrow line · .fig / .figcap figure and caption · .cite citation (20px) · .plot box for DECK.Plot · .readout .num tabular number · td.r right-aligned cell · .term, .emph, .muted, .b colors (meanings in format §8.0) and bold · .tate vertical writing · .mincho Mincho · .tcy upright digits in vertical text · <ruby> readings (Japanese language arts).

Step attributes

Attribute Meaning
data-step="k" appears at step k (k ≥ 1). Step 0 = what is visible on entering
(none on the slide) if a slide has no data-step and no data-from, each .body > ul > li and .body > ol > li becomes steps 1, 2, …
.concl always the last step (max + 1), step key concl
data-at="s" within its step, start s seconds later (fade and .draw)
class="draw" SVG stroke drawn in (needs pathLength="1"). Step = closest data-step ancestor, its own data-step, or 0. 0.7 s
class="draw drawslow" same, 1.4 s
data-from="k" (part) part's first step is slide step k (default 1)
steps="n" (part) override the part's step count
data-id="ID" morph pairing (contract §8)
id="ID" / data-mark="ID" target of {mark:ID} (contract §7)
data-key, data-title added by build (slide key, plain heading); not written by authors (format §8.0)

Math

Syntax Meaning
\( … \) / \[ … \] inline / display math, rendered by KaTeX at build time (not inside <script>, <style>, <svg>, <textarea>, comments)
\hlE{…} \hlT{…} \hlM{…} color part of a formula as conclusion (--emph) / term (--term) / known result (--muted)
\morph{ID}{…} morph pairing inside a formula (= \htmlData{id=ID}{…})
\htmlId{ID}{…}, \htmlData{mark=ID}{…} emphasis target inside a formula
\htmlClass{name}{…} CSS class inside a formula. Only these three \html… commands are allowed
macros.tex \newcommand{\R}{\mathbb{R}}, \newcommand{\v}[1]{…}, \def\x{…}; nested braces up to 3 levels
<script type="text/x-tex-macros">…</script> more macros in the same syntax, inside deck.html
KaTeX errors build warns and shows the source in red (.katex-error); the quality checker reports it as an error
DECK.svgMath(…) in a part or slide script KaTeX is also embedded at run time (with the same macros)

Includes

Syntax Meaning
/*INCLUDE:file*/ file contents inserted verbatim (working folder, then kit/). A path that leaves these folders (../, absolute, symbolic link) is refused with a warning and replaced by nothing
{{FIG:name}} figs/name.svg (or name.svg in the working folder) inlined; ids prefixed with name-, width/height removed
{{JSON:file}} JSON from data/ (or the working folder), minified. Put it in <script type="application/json"> inside a part to read it as this.data
<m-name …> part; components/name/index.js in the lesson, else the library copy vendor/components/name/<version>/index.js, loaded automatically

{{FIG:…}}, {{JSON:…}}, narration.md, timeline.json, macros.tex and the files in audio/manifest.json are read only if they resolve (after following symbolic links) inside the working folder; otherwise the build warns (refused: …) and skips them.

Assets (embedded at build time)

Reference Becomes
<img src>, <source src/srcset>, <img srcset>, <video src/poster>, <audio src>, <track src>, SVG <image href> data: URI
url(…) and @import in the slides' <style> and style="" attributes data: URI / the imported CSS inlined (url() in a linked or imported CSS file is relative to that file)
<link rel="stylesheet" href="file.css"> <style> with the file's contents
<link rel="icon"> and other <link> files, <script src="file.js"> data: URI / inline script

Relative paths are resolved from the working folder and must stay inside it. data:, blob:, #fragment and template values (${…}, {{…}}) are left as they are. Warnings: missing: … (file not found), refused: … (outside the working folder), external: … (an http(s) reference in an asset position, including <iframe>/<object>: left as it is, so the lesson is not fully offline). Ordinary links (<a href>) are not touched. Audio from audio/manifest.json is embedded only with --embed-audio.

Slide keys

A slide's key is its id (format §8.1: ^[A-Za-z][A-Za-z0-9_-]{0,63}$, unique among every id in the lesson, including mark targets and \htmlId{…}). npm run new gives every slide an id; npm run ids -- <deck> adds them to a lesson that lacks them. So that such a lesson can still be opened and fixed, the build keys a slide without an id by its heading and the quality checker reports it (legacyIndex); duplicate keys are an error, and the build warns (slide key: …) and gives the later slides a suffix #2, #3, …. The final key is written to the section's data-key (contract §4).

Script (narration.md)

Normative: format spec §8.1–§8.3.

Syntax Meaning
## heading {#s-k3f9q2} the slide with that id (the heading text is for readers)
[0] [1] … [concl] script for that step; following lines continue it. Steps are 0–999 without leading zeros ([01] is not a step)
@x-<name>-<field> <value> extension line inside a step: kept as data for the step, never shown or spoken, not hashed
Expert: / Novice: (E: / N:, full-width colon allowed) speaker turn. Lines without a speaker are Expert (single speaker) or continue the previous turn
{cue} sync point within a step; matches the part's cues(k) in order
{mark:ID} underline the target at this word
{mark:ID|effect} effect: underline, circle, box, spot, laser
{mark:ID|effect|hold=2} disappear after 2 s (default: end of step; laser 1.5 s)
{mark:clear} remove this step's emphasis now
{φ|ファイ} shown as φ, read as ファイ
# title ignored
<!-- … --> comment on its own line(s), ignored
a step written twice the last one wins

Build warns about a ## that matches no slide (the quality checker makes this an error), a step number that is not canonical, an invalid {#id}, unknown emphasis effects, and emphasis targets not found in the slides.

Timing (timeline.json)

dur = override.dur ?? max(anim, audio ? lead + audio + pad_after : max(hold, marksEnd + pad_after)) (normative: format spec §5.5 and §8.0; contract §5).

Key in "slideKey:step" Default Meaning
dur — fixed length (s); audio past lead + dur is cut
hold 1.2 length of a step without audio
lead 0.25 with audio, else 0 silence before the audio
pad_after 0.6 pause after the audio (or after the last emphasis)

Example: {"Update rule:2": {"pad_after": 1.0}, "Update rule:concl": {"hold": 2}}. Rebuild after editing.

Built-in durations (DECK.T, seconds): fade 0.45, draw 0.7, drawSlow 1.4, drawDelay 0.1, slide change 0.35, morph 0.9. Emphasis (DECK.MK): draw 0.5, fadeOut 0.3, gap without audio 0.9, laserHold 1.5. With prefers-reduced-motion (except ?mode=video) every step is shown in its final state.

Themes

Resolution: --theme > <html data-theme> > default. A name is looked up as a path relative to the working folder, then <deck>/themes/<name>/, then in your theme folders (each folder of EGATARI_THEMES in order, then ~/.egatari/themes if it exists), then the bundled themes/<name>/; the folder must contain tokens.css. EGATARI_THEMES is a list of folders separated by the OS path delimiter (:, or ; on Windows); a folder in it that does not exist is skipped with a warning. Before anything is read, theme.json, tokens.css, logos and every parent theme (extends, at any depth) must resolve, by real path, inside the theme's authorized root: the working folder for a theme of the lesson, themes/ for a registry theme, the theme folder (its real path, computed once, so ~/.egatari/themes may be a symbolic link) for a theme in your theme folders, the theme folder itself for one given with --theme. A parent may also come from your theme folders or themes/; "extends": "default" is the bundled default unless a theme folder has its own default. Otherwise the build warns (refused: …) and does not read it; a refused lesson theme falls back to default (format spec §11). A theme named by the lesson (or by extends) that is found nowhere is not an error: the build warns theme not found: "<name>" … and uses default (or no parent); a --theme that is not found is an error. The editor app does not see EGATARI_THEMES or ~/.egatari/themes: there you choose one theme folder (Load theme… → Choose folder…, read only), which takes the place of your theme folders in the same order and under the same rules (editor); in any browser, including those without a folder picker, you can instead load themes from a ZIP file (Load theme… → Choose ZIP file…), which the editor saves in the browser as “My themes” and reads at the same place and under the same rules (when your folder and My themes both have a theme with the same name, the one chosen or loaded last wins); without it, or with the lesson's themes/, a lesson whose theme is only in your theme folders shows that warning. An Egatari file (.ega.html) stores a theme from your theme folders as the lesson's themes/<name>/, so it rebuilds with the same theme anywhere. Deeper than 16 levels of extends is an error.

theme.json field Meaning
extends parent theme: its tokens.css is placed first (looked up next to this theme, then in your theme folders, then in themes/)
logos image files (relative to the theme folder; png, jpg, svg, webp) placed on every cover. Inherited through extends when omitted; "logos": [] removes them
name informational
version the theme's version (bundled themes listed in themes.lock.json; see “Theme versions” below)
fonts the fonts the theme chooses from the list Studio can embed (“Fonts” below): {"body": "Zen Maru Gothic", "heading": "Zen Maru Gothic"}. The build embeds them (only the files with characters the lesson uses); the theme's tokens.css still sets the CSS (--font-sans). A family from the list that the theme's or the slides' CSS names is embedded too
look which fixed shapes the theme uses for the heading, the cover and the conclusion line ({"heading": "band", "cover": "left-bar", "concl": "left-rule"}), from kit/core/theme-shapes.mjs. Bundled themes copy each shape's CSS into tokens.css between /* look <key>: <id> */ and /* /look */; tools/test-themes.mjs checks the copy. The build does not read it
metrics the theme's frame geometry for the quality checker and authoring tools ("v": 1; table below). Inherited field by field through extends; omitted fields take the defaults. Never used to draw (contract §12)

metrics fields (stage pixels of 1280×720, numbers, decimals allowed). The defaults are today's kit/layouts.css; the limits derived from them are the checker's old constants (565, 680, 1236, 1162, 124, 44). The margins (6 and 4) are policy, not theme values. A theme that moves these boxes in its CSS should declare them (bundled and official library themes must); node tools/theme-metrics.mjs measure <theme> prints the block to paste.

Field Meaning Default Used by the checker as
body.left, body.right, body.top the horizontal .body box (left and right as distances from the edges) 48, 48, 122 right limit 1280 − body.right + 4 (bodyRight)
body.floor how far the body may go down without a conclusion line (noconcl; above the page number) 680 bottom limit with noconcl
body.line the height of one body line, only for the “about n lines” in findings and fix requests 46 messages
concl.top, concl.height the horizontal conclusion line box 571, 72 bottom limit concl.top − 6 (bodyBottom)
tate.rule x of the rule of the vertical heading (layout-tate) 1166 right limit tate.rule − 4 (tateBodyHead)
tate.conclRight, tate.left right edge of the vertical conclusion column; left of the body without a conclusion 120, 48 left limit tate.conclRight + 4 (bodyConclCol) / tate.left − 4 (bodyLeft)
tate.floor bottom limit of the vertical body 680 bottom limit of layout-tate

A value that is not a number, outside 0–1280 (x) or 0–720 (y), an unknown field, v other than 1, or values that do not fit together (the body top below the conclusion line, a body narrower than 320px, …) is a build warning (theme-metrics: …) and the parent's (or default) value is used. The checker compares the declaration with what the theme renders (themeMetricsDrift, below).

Bundled themes. New lessons choose from seven themes, shown by Japanese colour names with a small note on what each suits: 藍 Indigo (default; research and classes), 墨 Ink (sumi; reports and proposals), 国語 Kokugo (kokugo; Japanese language arts), 常盤 Evergreen (tokiwa; science, nature, schools), 江戸紫 Edo purple (murasaki; research and public talks), 焦茶 Dark brown (kogecha; humanities, history, local studies), 浅葱 Teal (asagi, in Zen Maru Gothic; primary schools, publicity, guides). The list and the notes are kit/core/themes-list.mjs. business is no longer offered for new lessons (the editor's lists, npm run new); it stays bundled, so a lesson that names it or recorded its version builds and plays as before. sumi takes its place. All seven keep the role colours and the boxes of default (the checker's limits are the same); they change colours, rules, bands, corners and fonts only. Theme CSS is written with at least the specificity of the built-in rules (.slide h2 is 0,1,1): never wrap it in :where(), which drops it to 0 so that some declarations lose to the built-in ones (a heading band with the heading's own dark text on it). node tools/test-themes.mjs builds a sample lesson in every bundled theme and checks: the quality check (no textContrast, secHeading, fontNotEmbedded or metrics warnings), text contrast 4.5:1 for every role colour on --bg, the conclusion line on --concl-bg, the divider, the cover title and the heading on its own ground; and colour vision simulated with Machado et al. (2009) at severity 1.0: --emph against --term at least ΔE*ab 35 for normal, protan and deutan vision and ΔE00 20 for all four, --emph against --fg ΔE00 20 for protan; and the pen colours (below).

CSS order: the build writes, in <style id="deck-base">, the fonts, KaTeX, kit/base.css, kit/layouts.css, the theme's tokens.css (parent themes first) and, for lessons whose lang is not Japanese, kit/latin.css. The theme comes after the layout CSS, so a theme rule with the same specificity overrides a layout rule (for example .cover h1{top:260px} or .body{left:96px;right:96px}); no html prefix is needed. The meaning of the color tokens (contract §12) and the quality checker's color checks still apply to such a theme. This is the same in every build path (CLI, --dev and the dev server, --portable, --play-only, the editor app).

Token Meaning
--base base color; the others derive from it in themes/default/tokens.css
--emph-hue hue of --emph: a theme that sets it gets --emph = oklch(0.52 0.21 <hue>) (kokugo: 38). default does not set it and fixes --emph at #BE3012 (from 1.1.0)
--fg --bg --title --term --emph --muted --line --rule text, background, headings, terms, conclusions, known results, thin lines, heading rule
--complement --accent2 --base-tint --base-wash --comp-tint --term-tint --emph-tint derived colors and tints (--term-tint: .thm; --base-wash: .box; --emph-tint: not used by the kit now; kept so that existing themes and lessons keep working)
--series-1 … --series-4 figure series colors
--preview-opacity show later steps faintly: opacity of steps not yet reached (0.45; a theme may set it within 0.45–0.5 so that it stays readable on a projector; --lecture-dim is an alias)
--font-sans, --weight-body font family and body weight
--cover-bg, --logo-h cover background, logo height (128px)
--sec-bg, --sec-fg divider background (--base) and text (white)
--concl-bg conclusion line background (default: #F5F5F5 from 1.2.0; transparent before)
--marker list markers in .body (--term)
--bar, --bar-h progress bar color (--emph; --term during autoplay) and height (3px)
--stage-bg area around the slide (#2f2f2f)
--cap-size, --cap-bottom, --cap-side, --cap-max-lines caption font size (27px), distance from the bottom (18px), side margins (96px), max lines per caption chunk (3)
--cap-rate caption reading speed when presenting by hand (characters/s; default ja 7, en 15; also DECK.captionRate)

Theme versions. A bundled theme that changes how lessons look gets a new version in its theme.json, and the earlier version stays in vendor/themes/<name>/<version>/, listed with its identity hash and the colors that changed in themes.lock.json (npm run themes:check checks the copies against the lock; in npm test). default 1.1.0 (--emph #C50516 → #BE3012, --muted #6E6E6E → #5C5853) is the first such change; 1.0.0 is default as of Studio 0.4.6. business 1.0.1 (--emph #C2410C → #BA3C05) darkens its orange so that text on --emph-tint reaches 4.5:1. default 1.2.0 marks the conclusion row with a left bar in --emph, bold text, its fixed position and a light grey ground (--concl-bg #F5F5F5), left-aligned instead of a centred frame; position and height are unchanged, and vertical (layout-tate) slides keep the frame. Why these colors: themes/default/README.md. Documents at the top of a theme folder (README*, LICENSE*, *.md) are not part of the theme: the build never reads them (not even when listed as a logo) and they are not counted in its hash (spec §6). When an Egatari file carries a user theme or a theme in the lesson's own themes/ folder, the theme's LICENSE* files go with it (themes/<name>/LICENSE…, back in the folder on unpack), since a theme's licence (fonts, an institution's logo terms) must travel with its assets (§3.3); README* and other *.md files are left out. As for parts, a lesson is never restyled silently: an Egatari file records each theme's name, version and hash (index.themes) and its egatari.json pins the version ("themes": {"default": "1.1.0"}; unpacking an older file without it pins the version whose hash the file recorded). A build of a folder pinned to an earlier version uses that copy and warns theme pinned: … with the colors that would change; --update-themes (or setting the pin to the current version) updates it. A folder without a pin follows the current version. The editor rebuilds an opened file with the theme version it recorded and offers Update theme (editor). A theme that extends default uses the pinned default too. An Egatari file keeps playing with the theme CSS it embeds, whatever the editor's version.

Fonts: Japanese lessons (lang is ja or missing) embed BIZ UDPGothic; other languages embed Source Sans 3 and use BIZ UDPGothic only for characters past U+024F. Only the font files (or, with fonts/*.ttf and uv, the glyphs) that the lesson uses are embedded. BIZ UDPMincho is embedded as well, for the same characters, only when the lesson uses it: var(--font-mincho) in the lesson, its theme or a part it uses, class="mincho", or <html data-font="mincho"> (the definition of --font-mincho in kit/layouts.css does not count). --font-mincho is defined with :where(:root), so a theme can override it. Theme kokugo: Mincho body text, indigo base, vermilion emphasis, off-white background, captions in BIZ UDPGothic (--font-cap).

English italic: Source Sans 3 italic is embedded only for text the lesson sets in italic (<em>, <i>, <cite>, <dfn>, <var>, font-style: italic in a style attribute, a lesson <style> that sets italic, or a part's code that sets italic text; a part's math symbols in KaTeX_Math do not count): 400 italic for that text, 700 italic only for italic text inside bold (<b>, <strong>, headings, <th>, the conclusion line, .sub), and of each only the files with those characters. A lesson without italic text embeds none. In a Japanese lesson only Latin characters count, and Latin text in <em>, <i> and <cite> is set in Source Sans 3 italic.

A theme whose body font is a Japanese font from the list (asagi: Zen Maru Gothic) embeds BIZ UDPGothic only for characters that font does not cover.

The fonts a file can carry are a fixed list, all under the SIL Open Font License and from @fontsource (FONT_LIST in kit/core/build-core.mjs). Only the fonts a lesson chooses go into its file, so a longer list does not make files heavier. A font is chosen by the theme's theme.json fonts, or by naming its family in the theme's or the slides' CSS.

Group Fonts
Japanese BIZ UDPGothic (default), BIZ UDPMincho, Shippori Mincho, Zen Maru Gothic, Klee One (400 and 600)
English Source Sans 3 (default), Source Serif 4, Andika, Atkinson Hyperlegible Next (each with italic)
Monospace Source Code Pro

CSS may name any other font (one installed on the device, for example), but it is not embedded: each device shows its own font, or the next one in the list. The checker warns fontNotEmbedded once per lesson with the names (the first family of the computed font-family of slide text that is neither embedded nor generic). The offline editor (editor.html) carries only BIZ UDPGothic and Source Sans 3; a lesson that chooses another font is built there without it, with a warning.

Token Meaning
--font-mincho Mincho family ("BIZ UDPMincho", "Hiragino Mincho ProN", "Yu Mincho", serif)
--font-cap caption font (default --font-sans)

Parts

The list of parts and their props is vendor/components/CATALOG.md (generated by node tools/catalog.mjs from the frontmatter of each README, default versions). The parts come from egatari/components: Studio carries copies of its releases in vendor/components/sha256/<hash>/, pinned in components.lock.json; npm run components:update -- vX.Y.Z brings in a release (the only networked step: it checks the tarball against the release's manifest.json and keeps every version), npm run components:check checks the copies against the lock (in npm test). New parts are proposed in egatari/components issues. The part API (DECK.define, DECK.MotionElement, static steps, static props with type: number | bool | json | string, mount, duration, cues, render) is in contract §3 and skills/motion-components.

Helpers on DECK for parts: el(tag, attrs, parent) (SVG; var() colors moved to style), mtext(parent, x, y, "*x*_0", attrs), svgMath(parent, x, y, tex, {size, anchor, color, w, h}), Plot(box, {xr, yr, xt, yt, xf, yf, xl, xl2, yl, noY, m}) with .x() .y() .d(f, a, b, n) .poly(pts) .clip, prog(t, start, dur), lerp, ease, clamp01, color(name).

Links to slides and menus. <a href="#<slide id>"> and <a href="#<slide id>:<stepKey>"> are ordinary links to a slide or a step (no script; allowed in Core documents); the runtime follows them inside the document and offers Back (contract §9). Use the ids that npm run ids assigns. m-menu lays such links out as a grid of cards (title, data-sub subtitle, data-icon badge, an optional "visited" check for slides shown in this session, cards appearing by step with per-step): m-menu README (1.0.2); example: slide 2 of examples/kokugo-kanbun. Links inside slides take --term with an underline.

Japanese language arts (kokugo)

For 国語 lessons (kanbun, classical and modern Japanese). The example is examples/kokugo-kanbun (theme kokugo).

  • Vertical writing: <div class="tate">…</div> inside .body sets the text in writing-mode: vertical-rl, filling the height of the body, columns from right to left (paragraphs are separated to the left; p.indent indents the first line). .tcy sets two-digit numbers upright
  • Vertical slides: <section class="slide layout-tate"> sets the whole slide vertically. The <h2> is one vertical column at the right edge (1166–1232px, from 48px down, top-aligned); the theme's heading rule becomes a vertical rule on the left of that column, and the column does not wrap (about 14 full-width characters; the quality checker reports a longer one). The .body (148–1138px, 48–664px; from 48px with noconcl) is in writing-mode: vertical-rl: paragraphs and lists run in columns from right to left, and a .tate block fills it. Parts, figures and tables in it (direct children other than p, ul, ol, .sub, .note, .arrow, .tate, .box, .thm, h3, blockquote) stay horizontal at the full body width, since they measure their own box; use their vertical option (m-kobun tate, m-gendai tate; m-kanbun is always vertical). The .concl is a vertical column at the left edge (48–120px; about 19 characters). The page number stays at the bottom right, below the heading column. Only the CSS changes: slide keys, captions and the readable layer of an Egatari file take the heading text as before. Example: the 現代文 slide of examples/kokugo-kanbun; tests: tools/test-layout-tate.mjs
  • Ruby: <ruby>漢<rt>かん</rt></ruby>; rt is 0.5em and the quality checker does not count its size. In vertical text the reading is on the right
  • Mincho: .mincho or <html data-font="mincho">, or a theme that sets --font-sans: var(--font-mincho) (theme kokugo)
  • m-kanbun: kanbun in vertical writing. One line per column; after a character: (送り仮名), [返り点] (レ, 一二三四五, 上中下, 甲乙丙丁, 天地人, 一レ, 上レ, …; a bare レ also works), {振り仮名}, {=かな} (written in hiragana in the 書き下し文, e.g. 不{=ず}), {-} (置き字, not read), (右|二度目) (再読文字, e.g. 未(ダ|ず)), - (竪点), 、 。. Steps: 白文 → kunten (返り点 lower left, 送り仮名 lower right) → order (characters highlighted in kundoku order, an arc where the eye jumps back; one step per line (句) since components v1.4.0, and a return to an earlier line by a 返り点 or a 再読文字 starts a step of its own; order-by="all" keeps the whole reading order as one step, with cues(k) at the start of each column from the second) → kaki (書き下し文 on the left). Rules and details: m-kanbun README (1.6.0)
  • m-kobun: classical Japanese split into words (春[名]/は[係助]/…), colored by part of speech, one sentence per step, and a modern translation (= … lines) in the last step. Clicking (or Enter on) an auxiliary verb [助動・基本形・意味・活用形] opens a card with its conjugation table and meanings, taken from a table inside the part (no network, text only). The card is a presenting control outside DECK.seek(i, k, t): it closes whenever a step is entered (also the same step again), never opens in video mode and is not printed. m-kobun README (1.0.2)
  • m-gendai: modern Japanese: __傍線__, ==二重傍線==, {語句|_A,#id} / {これ|>id} (an arrow from a demonstrative to what it refers to), paragraphs @主張#p3, @具体例#p1>p3, @対比#p2~p1 boxed by role and joined by lines. Horizontal or tate. m-gendai README (1.0.2)

Presenting

URL parameters

Parameter Meaning
?mode=talk default: later steps hidden, current step fades in
?mode=talk&preview=1 show later steps faintly: everything visible, later steps faded (--preview-opacity, default 0.45; elements marked data-preview="none" and parts that declare preview: "none" stay hidden), the current step and earlier ones at full strength with no mark on the current step (no highlighter, no .cur class), emphasis in final state
?mode=video for rendering: no tools or toolbar, reduced-motion ignored
?captions=1 start with captions on (same as s). With ?mode=video this is the only way to get captions, so a render can burn them in
?capsize=s / m / l caption size (22 / 27 / 34px); overrides the remembered size; ignored with ?mode=video
?presenter=1 presenter view (opened by p)
?view=follow / ?view=next internal: the iframes inside the presenter view
#N open at slide N (1-based); updated while navigating
#<slide id> / #<slide id>:<stepKey> open at that slide (step 0) or step (0, 1, … or concl), in its final state (contract §9)
?embed=1 in a frame: post {egatari:"position", slideKey, stepKey, t, cause} to the parent on every step change and accept {egatari:"links", follow} from it
?links=0 with ?embed=1: links to slides do not move the player; it posts {egatari:"linkRefused", slideKey, stepKey} instead

Keys

Key Action
→ ↓ Space PageDown next step
← ↑ PageUp previous step (shows it in its final state)
Shift+→ / Shift+↓, Shift+← / Shift+↑ next / previous slide (step 0)
Home / End first / last slide
digits + Enter go to that slide
f fullscreen
p presenter view (p again closes it; in the presenter view, Esc or p closes it, or only leaves fullscreen)
a autoplay from the current step (audio if any). Any navigation key stops it
l show later steps faintly (on/off)
b black screen (turns the whole window black, with a faint hint in the corner for 2 s and on mouse move; b again, Esc, a click, the arrows, Space, PageUp/PageDown or Home/End bring it back without moving the slide; the presenter view shows "Black screen", and b there toggles it on the main window; the arrows, Space and PageUp/PageDown there also only end it)
s captions on/off: the current step's script (display text) at the bottom of the slide
Shift+s caption size: S → M → L (22 / 27 / 34px), with a brief notice; the last choice is remembered (localStorage)
o overview (arrows, Home/End, Enter/Space/click to go, Esc or o to close)
x laser pointer (hides after 2 s without movement)
d pen (pressure-sensitive; ink stays per slide)
v fading pen (fades 0.7 s after you stop, over 0.5 s)
Shift+d pen color: Red → Blue → Black, with a brief notice "Pen color: Blue". The pen colors are fixed, whatever the theme: the red #FF4B00 and blue #005AFF of the Color Universal Design palette and black #000000 (on white 3.4, 5.4 and 21:1, at least 3:1 for lines; at least ΔE*ab 61 apart under simulated colour vision). tools/test-themes.mjs checks them on the ground of every bundled theme
Backspace / Shift+Backspace while the pen or fading pen is active: clear this slide / all slides
Backspace / Alt+← otherwise, after following a link to a slide: back to where the link was clicked (also the Back button at the top left and the browser's back button)
Tab / Shift+Tab focus the control-bar buttons (Enter / Space presses; Esc returns to the slide)
Esc leave the tool, close help
? key list (with dev-server keys when served by server/dev.mjs)

Toolbar (shown when the lesson opens, then on mouse move or tap)

When a lesson opens, the toolbar stays for about 5 s. The first time on a device (remembered in localStorage; shown every time when storage is not available), one line above it says how to bring it back: “Move the mouse to the bottom of the screen (or tap) to show the toolbar”, or on a touch device ((hover: none)) “Tap the screen to show the toolbar” (hidden from screen readers, which reach the toolbar with Tab); it goes when the toolbar hides. After that the toolbar comes back when the mouse moves (or, on a touch screen, when the slide is tapped; that tap does not advance the slide) and hides after 2.5 s without movement, unless the pointer is over it or it has focus. While it is hidden, a small faint button at the bottom left, Show toolbar, brings it back with a click, a tap or the keyboard (Tab reaches it; Enter moves focus to the first toolbar button; a click or tap leaves focus on the slide). A tap that wakes the hidden toolbar, or a tap on the handle, only shows it and never also presses a toolbar button. The handle and the hint are not shown in video mode, in the presenter view's inner screens, with embed=1&controls=0 (which also leaves out the toolbar), or in print. On a narrow screen (such as a phone held upright) the toolbar wraps onto two rows to stay within the screen, and the slide scales down to the screen width.

Buttons: previous, next, autoplay/stop, laser pointer, pen, fading pen (both pen buttons carry a dot in the current color; while a pen is active, Red/Blue/Black swatches appear after them; click one to pick it, the selected one is aria-pressed), clear ink (this slide, in any tool), overview, show later steps faintly, captions, Listen with read-aloud (not saved as audio) (only when the browser has speech synthesis and some step has a script but no audio; see Listen with read-aloud), presenter view, fullscreen, help, and a small “AI voice” badge when any step's audio is TTS (format spec §7.3). Tab moves into the bar (it stays visible while it has focus), Enter/Space presses the focused button and keeps focus there, Esc returns focus to the slide; a mouse click never leaves focus on a button. It sits in the letterbox below the slide when there are at least 44px, otherwise over the bottom edge and only when the mouse is near the bottom. Hidden in print and video.

The ? help ends with a Send feedback link and the version (for example v0.2.0). It opens the feedback form prefilled with the version (the player's; the Studio that made the file is added to the context when it differs), browser, OS, lesson language and mode only; nothing from the lesson is sent. Not shown in video or print, or with data-feedback="off".

Captions

s, the CC button, or ?captions=1 shows the current step's script (the display text of narration.md, readings and {mark} removed) in a bar at the bottom of the slide: --fg background, --bg text (the quality checker keeps their contrast ≥ 4.5:1). The step is split into sentence groups (about two lines; shorter at larger sizes; overlong sentences are split at commas or word boundaries) and one group is shown at a time, at most --cap-max-lines (3) lines; if a group still overflows, its last lines are shown. While presenting by hand (and in autoplay without audio) the groups advance at reading speed (ja 7 characters/s, en 15; --cap-rate or DECK.captionRate), restart at every step change, and the last group stays. During autoplay with audio, and in ?mode=video&captions=1, each group is shown for a share of the audio proportional to its length (in video without audio the whole step is shown); the size is set with Shift+s, ?capsize= or the theme's --cap-size; in video this depends only on (i, k, t). When the toolbar sits over the bottom edge, the captions move up above it while it is shown and back down when it hides. Never printed. render/render.mjs opens ?mode=video without captions; pass the parameter there to burn them in.

Listen with read-aloud

The speaker button in the toolbar, Listen with read-aloud (not saved as audio), reads the current step's script with this device's speech synthesis (the Web Speech API, format spec §5.6). It is for steps without audio: on a step with audio the button is greyed out and says that the step has audio (autoplay a plays it). While it reads, a label at the top left says Reading aloud; moving to another step, Esc, or pressing the button again stops it.

  • Only voices that run on the device are used (localService === true, matching the lesson's <html lang>; the exact region, the device's default voice and well-known natural voices come first; robotic and novelty voices such as Eddy or Bad News come last and are not used for Novice). Network voices are never used, even when the browser offers one as its default, because they send the text off the device. With two local voices for the language, Expert and Novice get different voices; with one, it reads both. Without any, nothing is read and a notice says “This device has no read-aloud voice for this language”.
  • The text is each turn's ttsParts in deck-narration (format spec §4: the spoken pieces with {x|y} readings applied, the same value that enters inputHash), joined, with the same text pre-processing as the AI voice (join-num-counter@1). Every build writes ttsParts (about 3–7 KB per example lesson); it is not shown and not used for fonts. A file built before ttsParts existed falls back to the display text, and the label then adds “Pronunciations not applied (older file)”. The reading dictionary (tts/yomi.yaml) is applied only in the editor's preview (see the editor) and the AI voice, not in the player.
  • It is not audio of the lesson: nothing is written to the file or a video, the timeline (DECK.timeline()) and the rendering at (i, k, t) do not change, a step being read lasts as long as the reading, and there is no button in ?mode=video or print.

Accessibility

Not added in ?mode=video or the presenter-view iframes, and none of it changes pixels.

  • Screen readers: a visually hidden aria-live="polite" region (#deck-live) announces "Slide n of N: title" on a slide change, the text of the newly revealed step on the next step (math read from KaTeX's glyphs), or "Step k of K" when going back or when a step reveals no text. Each slide is role="group" with aria-roledescription="slide" and an aria-label; hidden slides get aria-hidden="true" and inert, and in talk mode steps not yet shown are aria-hidden (lecture leaves them readable).
  • Keyboard: control-bar and presenter-view buttons have aria-label (Japanese or English after <html lang>), toggles have aria-pressed, and a focus ring appears only on :focus-visible. The overview is a dialog whose thumbnails are labelled buttons; focus follows the arrow keys and returns to where it was on Esc. The help (?) is a dialog that takes focus and gives it back on close. The ink layer is aria-hidden and never takes focus, so Tab and Esc keep working while drawing.
  • prefers-reduced-motion shows every step in its final state (except in video).
  • The quality checker warns about <img> without alt (alt="" marks decoration) and SVG figures without aria-label / aria-labelledby / <title>, and lists parts without a name (info).

Presenter view

Current screen (left), next step (top right), script of this and the next step (bottom right; Novice turns labelled), slide/step counter, autoplay button, tool buttons (laser pointer, pen, fading pen, clear, overview, show later steps faintly; while a pen is active, the same Red/Blue/Black swatches, synced with the main window by color index), timer with reset (⟲), clock. Keys in the presenter window: → ↓ Space PageDown / ← ↑ PageUp, a, x, d, v, Shift+d, Backspace, Esc, o, l. Ink drawn on the left screen appears on the main window. Communicates by postMessage, so it works from file://. The key row ends with a Send feedback link (same as in the ? help).

Print and PDF

Printing (tools/pdf.mjs, or the browser) shows each slide in its final state, one 1280×720 page per slide, without emphasis, ink, tools or the progress bar.

Run-time API (for scripts and tests)

DECK.seek(i, k, t, from), go(i, k, animate), next(), prev(), state() → {slide, step, max, key, mode}, timeline(), autoplay(on), stepKey(i, k), marks(i, k), morph(i), slides, mode, ready (true once the parts are mounted and the opening position drawn; a lesson started while not laid out waits for the stage to get a size, contract §3), NARRATION, AUDIO, MARKS, TIMING, VERSION (the version of the running player: player.version in deck-meta of an Egatari file; generator.version for a file from before 1.0; in a plain build the version in package.json), MADE_WITH (generator.version, or empty), FEEDBACK_URL (or null), feedbackUrl() (the prefilled form URL, or null), tools.{mode, setMode, clear, help, overview, color, readAloud} (color() is the pen color index 0–2, color(n) sets it; readAloud() says whether read-aloud is reading, readAloud(true / false) starts or stops it), ReadAloud (kit/core/read-aloud.mjs: spokenTurns, pickVoices, …), TtsNormalize, captions(on) (no argument: is it on), captionSize(z) ("s" / "m" / "l"; no argument: the current size; null in video), captionRate (characters/s), toast(msg) (brief notice), caption() (the caption text shown, or null), announce() (the live-region text), resolveFragment(f) (→ {i, k, key, id} or {error}), fragment(i, k), follow(f), back(), links() (→ {depth, follow, back}), visited(i | key) (contract §9). Events on document also include deck:captions (detail = {on}), deck:tool (detail = {mode}: "laser", "pen", "vpen" or null, when the presenting tool changes) and deck:edit (detail = {on}; only the preview of the dev server and the editor app sends it, when edit mode changes: the runtime then turns the presenting tools off, see dev server). Events on document: deck:ready, deck:pos (detail = {i, k, max, n, key, nextKey, auto, cause, back}; cause is control, link, seek or play, back the title of the slide Back returns to or null).

Dev server

node server/dev.mjs <deck.html> [--port 8730] [--theme name] (or npm run dev -- …). Details: dev-server.md.

Item Behavior
--port n default 8730; if taken, tries the next ones (up to 20). --port 0: any free port
--theme name same as the build option
listens on 127.0.0.1 only
rebuilds on slides file, narration.md, timeline.json, macros.tex, audio/manifest.json, data/, figs/, components/, the repository's kit/, vendor/components/, themes/, and your theme folders (EGATARI_THEMES, ~/.egatari/themes); the page reloads at the same slide and step
dev build no glyph subsetting (faster), data-src on editable elements, audio from /deck/audio/…, build errors shown as a page
e edit mode; double-click an h1 h2 p li td th, .big .note .sub .venue .au .thm .box .lead or a span in .concl; ⌘/Ctrl+Enter save, Esc cancel. A badge shows build warnings
n script drawer for the current step (and preview of the next)
c open comments; click to jump, "resolve" to close
Alt+click (on a Mac: ⌥ Option+click) leave a comment (comments.json)
m (edit mode; the slide controls' pen button reads “Marker” there; d also opens it) marker: draw circles or arrows (several for one comment while its box is open), then write the comment; the lines are kept as strokes (contract §13)
conflicts a save is refused if the slides or script changed since it was opened
files written deck.html (only the edited element), narration.md (only that step), comments.json, .cache/edits/ (last 30 copies)
endpoints GET / (frame page; ?presenter=1: presenter window), GET /preview (the lesson document, sandboxed; the only framable endpoint; served only with the frame page's cap and Sec-Fetch-Dest: iframe, otherwise a 403 text page), /deck/<file> (inert: CSP sandbox, non-media as text/plain attachments), /__dev/shell.js, /__dev/editor-ui.js, /__dev/presenter.js, /__dev/events (SSE), GET /__dev/info, GET /__dev/source?range=a-b, POST /__dev/edit, GET/POST /__dev/narration, GET /__dev/comments, POST /__dev/comment, POST /__dev/comment/resolve

Quality checker

Quality control in Egatari has three parts: the standards (R-fit, R-steps, … in the authoring guide), the quality check that reports findings against them, and the fix request that hands the findings to an AI. The quality checker comes as a command (npm run check, below, with every check) and in the browser (editor app, a subset).

node kit/check.mjs <deck.html> [--theme name] [--lang ja|en] [--json] builds in memory, opens headless Chrome, and prints results per slide. Exit code 0 when there is no error, 1 if there is any error, and 2 when the check did not finish because the headless browser failed to launch or crashed (the reason goes to stderr; with --json the output is {"ok": false, "status": "crashed", "counts": {…all 0}, "results": []}, so it is never mistaken for "0 errors" and never becomes a finding). --theme checks with another theme (otherwise <html data-theme>). Each finding (error or warning) says where (slide number, heading, id, step), what is wrong and how to fix it, with the id of the standard in the authoring guide where one applies (for example (R-title)), so the findings can be pasted into a fix request as they are. --lang chooses the language of the findings (default: the lesson's <html lang>, Japanese when it is ja or missing, otherwise English).

--json prints:

{"ok": false, "lang": "en", "counts": {"error": 1, "warn": 0, "info": 12},
 "results": [{"slide": 2, "id": "s-k3f9q2", "key": "s-k3f9q2", "title": "The decay law", "step": "3",
              "sev": "error", "code": "narrOrphan", "rule": "R-narr-steps", "data": {…},
              "msg": "Slide 2 “The decay law” (s-k3f9q2), step 3: narration [3] has no matching step … (R-narr-steps)"}]}

If status is crashed (exit code 2), the result is invalid: the check did not finish, so it is not a pass even though results is empty. A normal result has no status field. Scripts should treat exit code 2 and status: "crashed" as a failure; kit/core/check-result.mjs (parseCheck) does this.

Field Meaning
slide slide number, from 1; null for the whole lesson
id the slide's id, or null (a slide without id, or the whole lesson)
key the slide key (the id, or the legacy key from the heading)
title the heading text of the slide (h2 / h1, without tags)
step the step the result is about: "0", "1", … or "concl"; null when it is about the whole slide
sev error, warn or info
code the kind of result (stable; the list is kit/check-messages.mjs)
rule the id of the standard in the authoring guide (R-title, R-fit, …), or null
data the values of the result (e.g. {"px": 583, "lim": 565, "concl": true} for a body overflow; with a theme whose metrics differ from the defaults also conclTop, line, and lim for bodyRight), used by fix requests to give the amount to cut
msg the whole finding in lang: where, what and how to fix
Quality check Severity
build warnings; a script ## that matches no slide, a non-canonical step number ([01]), an invalid {#id} warn; error
slide id not matching ^[A-Za-z][A-Za-z0-9_-]{0,63}$ (slide id: …); two slides with the same key (slide key: …); a slide id equal to another id or \htmlId{…} in the lesson (slide id: …) error
the heading text of a ## heading {#id} differs from that slide's heading (the id wins) info
a slide keyed by a legacy rule: a hand-written data-key, or s<n> (no id and no heading) — slide key: legacy warn
source outside the 1.0 vocabulary (format spec §8.0): the colour classes blue, red, gray (use term, emph, muted), data-steps / data-anim (steps and motion come from data-step, data-at, draw and parts), <html data-mode="lecture"> (the participant's “Show later steps faintly”) — legacyColorClass, legacyStepAttr, legacyLectureMode warn
a link to a slide (#<id>, #<id>:<stepKey>, #N) whose slide does not exist, whose step is out of range, or :concl on a slide without a conclusion; a link #N or to a slide without an id; a relative link to a file that does not exist (contract §9) error; warn; warn
a part's data does not match its declared shape (static shape), or the part throws while drawing (compData: which part, what differs (path, expected, got), its shape and an example; standard R-components). The step count of a slide also comes from its data-step elements and .concl, so a failing part does not turn the slide's script into orphans error
console errors (the message only: stack lines and file paths, temp folders and local-server URLs are removed) error
text visible at step 0 info
title > 26 full-width units (Japanese) / > 60 characters (English) error / warn
title ends with a colon error
layout-tate: the vertical title does not fit its column error
conclusion > 35 units (Japanese) / > 70 characters (English); conclusion overflows its box (also the vertical column of layout-tate) error / warn; error
body below 565px (680px with noconcl) or right of 1236px; with layout-tate: below 680px, right of 1162px (over the heading column), left of 124px (over the conclusion column; 44px without one). These are the defaults: the limits come from the theme's metrics (Themes) error
the theme's rendered boxes differ from its metrics by more than 1px, or a theme without metrics (or the slides' own <style>) moves the body, conclusion line or vertical columns (themeMetricsDrift, once per lesson: each field with the declared and rendered value and whether the theme's or the slides' CSS moved it); unusable metrics values (themeMetricsInvalid, once per lesson); the bundled fonts did not load, so the boxes were not measured (metricsFontLoad, once per lesson; a fallback font is never measured) warn
body text < 27px, table/plot text < 24px, SVG text < 24px (skipped on slides whose heading contains 文献; ruby readings in rt are not counted; text inside an element with data-annot, a deliberately small annotation such as m-kanbun's reading-order numbers, is not counted: contract §10) error
text cut off by a box with overflow other than visible (textClipped); SVG text cut off by its viewport (viewBox) or going off the slide (svgTextOut); SVG text overflowing the <rect> around it (svgTextBox); text outside .body going off the slide or overlapping the heading, conclusion line or page number (outsideBody). Text inside an element with data-clip is not checked by these (contract §10); on a slide with bodyBottom, text in .body going off the slide is not reported again warn
on any slide (cover, section divider, layout-tate, absolutely positioned layouts): on a slide without .body, text going off the 1280×720 slide (textOffSlide; with .body this is outsideBody); a text box overlapping another text box or the slide's decorative band, such as the band at the top of the cover (textOverlap: line rectangles of the two boxes intersect by more than 2px in both directions; a KaTeX formula counts as one box, its visual box; a parent and its child, text inside the same part, and SVG text are not compared; on a slide with a body-position error, body text against the heading or conclusion line is not reported again). On a cover or section divider the finding names the allowed children and where a subtitle goes. Text inside data-clip, hidden text, KaTeX's MathML and .sr-only are not checked warn
a word in a table cell (td, th) breaks across lines (cellWordBreak): a cell with Japanese or Chinese characters and at most 6 characters (spaces not counted) rendered on 2 or more lines, or a break inside a word without Japanese or Chinese characters (words from Intl.Segmenter; browsers break Latin words only at spaces or hyphens unless overflow-wrap or word-break allows more). Long Japanese cells that wrap are not reported. Fix it with fewer columns, a shorter word or a wider column warn
です・ます in the body (Japanese) error
unrendered math or KaTeX errors error
mixed punctuation 、。 vs ,. across the lesson (Japanese) warn
a step without a script warn
template leftovers from the editor's slide list (kit/core/template-text.mjs): a script line that still starts with the placeholder mark ✎ (narrPlaceholder), or slide text still equal to a template default such as “New slide heading” or “First point” (templateText). TTS would read the placeholder and it would show in the captions and the readable layer, so TTS (tools/narration-json.mjs, tts/synth.py), --portable / --play-only and the editor's TTS and exports list them first warn
stale audio: a step's audio was made from another script (inputHash differs or is null; without inputHash, the TTS cache record audio/seg_<digest>.json is compared) — audio: stale warn
script that reads a formula (each turn by the rules of its language, see below; a Japanese sentence that only names things in Latin letters, such as “Markdown と HTML”, is not a formula) warn
turn > 200 characters (Japanese) / > 60 words (other languages) info
the script language differs from the slide language (narrLang) info
contrast of --fg --term --emph --title --accent2 vs --bg < 4.5:1; --muted < 3:1 error; warn
on each slide, text of a heading or the body against the background behind it (computed colours; not over images or gradients, not faint text) < 4.5:1, the lowest one per slide — textContrast warn
slide text in a font that is not embedded in the file (the first family of its computed font-family is neither embedded nor generic), so it looks different on each device — fontNotEmbedded, once per lesson with the names warn
a chapter divider (class="slide sec") with an <h2> and no <h1> (the title of a divider is <h1>; an <h2> is shown the same way) — secHeading warn
part drawing depends on call order error
emphasis target not found; list of emphasis per step warn; info
morph: source slide missing, no shared data-id, duplicate ids; pairs warn; info
<img> without alt; SVG figure without aria-label/<title>; part without a name warn; warn; info
timeline total and per slide info

The slides and the script may be in different languages (for example Japanese slides with English script for subtitles). The script language is "lang" in voice.json (as for the TTS), otherwise the language of the text when nearly all turns are in another writing system than the lesson (no Japanese characters in a Japanese lesson, or Japanese in most turns of another lesson), otherwise the lesson's <html lang>. The script checks then take each turn by its own language: a turn with Japanese characters by the Japanese rules, an English sentence by the English rules (only \ ^ _ and dense operators count as a formula).

Build

node kit/build.mjs <deck.html | working folder> [-o out.html] [--theme name] [--dev] [--embed-audio] [--portable] [--play-only] (or npm run build -- …). Given a folder, it builds the deck.html inside it (without one it says what it expected and exits with 1).

Option Meaning
-o file output (default <working folder>/out/<name>.html)
--theme name theme (see Themes)
--dev add data-src for the editor and skip glyph subsetting (used by the dev server)
--embed-audio embed audio as AAC 64 kbps mono (needs ffmpeg; otherwise WAV, with a warning). Without it, audio is referenced relative to the output
--portable, --play-only an Egatari file that also carries the editable source; see Egatari file

The summary line shows slides, math, parts, font files, script steps, audio steps, theme and size. Caches: subset fonts in $TMPDIR/motion-slides-fonts/ (keyed by the glyphs, the TTF file's hash and the fonttools version; uv runs fonttools==4.67.0 and brotli==1.2.0), compressed audio in $TMPDIR/motion-slides-audio/. Glyphs embedded: the slide text after decoding HTML entities and \u escapes, strings in <script type="application/json"> and the script, string literals of the parts used (without CJK in non-Japanese lessons), printable ASCII (digits, / for page numbers), Appendix, and for Japanese lessons 付録 and common Japanese punctuation. Local assets are embedded as described in Assets.

A lesson-local part (<deck>/components/<name>/index.js) is read only if its real path is inside the working folder, a library part only inside vendor/components/; a lesson-local one that resolves outside is refused with a warning and not replaced by the registry one. Inside the working folder, the output (out/…) and egatari.json are never written through a symbolic link (the file or a folder on the way); a link there makes the build fail without writing.

Build limits

The build counts while it expands and stops with build-too-large as soon as a limit is exceeded: the source (32 Mi characters), includes and embeds together (/*INCLUDE*/, {{JSON}}, {{FIG}}, data: URIs, <link rel="stylesheet">, <script src>, @import: 10 000), the size after preprocessing (64 Mi characters) and after embedding and math (256 Mi), the tags before each parse (500 000) and formulas (20 000). Every file read counts toward the include count — theme files, logos and parent themes, parts, script, audio, fonts and the runtime too — and toward one total of what was read (readChars: 256 Mi; text in bytes, base64 in characters), charged from the file size before reading, so an oversized file is refused unread; the size is an estimate, and the bytes actually read are charged too (a file that grew is refused). Sizes are always bytes (text read through an adapter that does not report bytes is charged its UTF-8 length, never its UTF-16 length). Only regular files are read: a FIFO, device or socket is refused with build-not-a-file. The whole output is checked against 256 Mi before it is joined. Audio is compressed from the bytes charged for it: the build reads the .wav once, up to its charged size, and hands ffmpeg a private copy of those bytes instead of the original path. The Egatari file export shares its build's budget for everything it reads (script, copied sources, voice.json, egatari.json, audio before compression) and checks the whole file before it builds each part: archive chunks from their projected size, the index from a lower bound before it is serialized (and against the reader's 4 MiB indexBytes), and the readable layer counted in escaped characters while it is built; base64 is encoded only after every check passed. Real lessons are far below (examples/tmlr2026-calibration: 11 KB source, 27 formulas, 3 400 tags). build(env, src, {limits: {...}}) changes them (BUILD_LIMITS in kit/core/build-core.mjs). In the editor app the build runs in a worker that is stopped after 60 s, and reads only the files the build references, at most 8 at a time, each up to its listed size (reserved before the read starts; a file that grew since the listing is refused). Outside the build, the dev server, the editor's own requests and npm run ids read deck.html (32 MiB), narration.md (16 MiB) and comments.json (4 MiB) with fixed limits (READ_LIMITS in kit/core/bounded-read-core.mjs), and npm run portable -- unpack refuses an input over the format's 100 MiB before reading it. All of these go through one bounded reader (kit/bounded-read.mjs on Node, app/bounded-read.js in the browser); tools/test-raw-reads.mjs fails on a raw read elsewhere.

Egatari file (--portable)

node kit/build.mjs <deck.html> --portable writes <working folder>/<lesson title>.ega.html (the title as the editor names its export: metadata.title, otherwise the lesson's <title>, made safe for every OS; without a title, <deck name>.ega.html) and prints its full path: one HTML file that plays like the normal build and also carries the lesson's source, so it can be unpacked into a folder (or reopened by an editor) without the original folder. It follows Egatari, the open portable lesson format (draft 1.0); the file name extension, block ids and media type come from that spec.

  • What goes in: every file the build reads from the working folder (deck.html, narration.md, macros.tex, timeline.json, data, figures, images, styles, lesson-local parts and themes), voice.json with local paths reduced to voice names, and the audio listed in audio/manifest.json for steps that exist in the script (compressed to AAC as with --embed-audio; WAV with a warning if ffmpeg is missing). Each audio clip is stored once and the runtime plays it from there.
  • What stays out: comments.json, hidden files and folders (.cache/, .git/, .env, …), out/, video/, bundle/, node_modules/, backups, *.local.json, credentials, unused audio and the TTS cache files. A file the build read but that is excluded is reported. Parts and themes from this repository are referenced by name, version and SHA-256 instead of being copied (a theme from your theme folders, EGATARI_THEMES or ~/.egatari/themes, is copied in as the lesson's themes/<name>/); a registry name is a single identifier (^[a-z][a-z0-9-]{0,63}$), and the reader refuses a file whose registry names are anything else (a path, .., uppercase).
  • Lesson id: the first export creates egatari.json in the working folder with a random docId; keep it with the lesson so later exports keep the same id. Its optional metadata object may hold license (SPDX id or URL), copyright, attribution, creators, description, accessibility, curriculum, completion, derivedFrom, authorship and audience; license, copyright and attribution are also copied to the runtime metadata, so a play-only copy keeps them. Title and language come from <title> and <html lang>.
  • Player and producer (spec §4.1, §7.1): the index and deck-meta carry player — the bundled player, {"name": "egatari-player", "version": …, "title": "Egatari Player"} (title is the name shown to people, name the stable identifier; the version is kit/player.json's, with +dev.<commit> added when the player files differ from the release recorded in kit/player.lock.json; npm run player:check) — and generator, the software that wrote the file ({"name": "egatari-studio", "version": <Studio's version>, "title": "Egatari Studio"}). contracts is {"source": 1}. Saving without rebuilding (tier A) keeps player and generator byte for byte; a rebuild writes this Studio's and warns when the file carried another player (playerChange); the editor then asks before saving (see editor, “Saving with a rebuild”).
  • File policy (spec §4.2): a <meta http-equiv="Content-Security-Policy"> right after <meta charset>, before every script, style and link: the baseline, which connects nowhere (connect-src 'none'). A play-only copy with Live adds only the Live's endpoint to connect-src, in place; destinations of embed (copied into deck-extensions in every file) go into frame-src. Readers report a file whose policy is missing, misplaced or differs (compared as parsed directives) as not conforming; it still plays.
  • Slide ids (spec §8.1): every slide in the file has an id and every script heading {#id}. Slides and headings without one (a lesson an AI just wrote, older keys by heading text, hand-written data-key or s<n>) get one at export, written back to the working folder with the keys in audio/manifest.json, timeline.json, comments.json and egatari.json (backups in .cache/edits/); the build and the quality checker still accept them.
  • Script readable without JavaScript: the file also contains the display text of every step as ordinary HTML (hidden while the lesson runs), for archiving, search and accessibility.
  • Extension lines (@x-<name>-<field> value) are collected per step into the file's index.
  • Attachments (format §3.8; draft, experimental, spec §3.8 draft): files for readers rather than for rendering, in attachments/ (subfolders allowed): the original source (TeX, Word), the typeset PDF, code, data, supplements. Their roles, titles and descriptions are in egatari.json, in the same shape as the file's index.attachments: "attachments": [{"path": "attachments/paper.pdf", "role": "pdf", "title": "Paper (PDF)", "description": "The journal version."}], role one of source, pdf, code, data, supplement, title required (a short name for the download list, one line of at most 120 characters, counted as Unicode code points as in the schema), description optional (one or two plain-text sentences, one line of at most 300 characters; shown under the title in the download list; other members are kept). Regular files under attachments/ are packed (symbolic links are not followed; hidden files, backups and credentials stay out as above); only files with a valid role and a valid title are listed in index.attachments, the others are packed with a warning; an invalid description is dropped with a warning and the entry is still listed.
    • Types per role (spec §3.8, kit/core/attach-content.mjs): pdf takes PDF; data CSV, TSV, JSON, text and xlsx; code program source (.py, .r, .jl, .c, .mjs, .ts, .sh, …), notebooks (.ipynb) and environment definitions (requirements.txt, pyproject.toml, uv.lock, environment.yml, Dockerfile, …); source TeX, Markdown, text (e.g. BibTeX) and docx; supplement PNG, JPEG, WebP, PDF and text. Never attached: executables; in any role, the extensions that common operating systems run or open as scripts, exactly the spec's list (.js, .jse, .vbs, .vbe, .wsf, .wsh, .hta, .bat, .cmd, .com, .scr, .ps1, .command, .app, .jar, .reg, .lnk; attach JavaScript source as .mjs, .cjs or .ts); WebAssembly, macro-enabled Office files and legacy Office files (.xlsm, .docm, .doc, .xls, .ppt, RTF), HTML, SVG, another .ega.html, archives and containers (ZIP, TAR, 7z, …), audio and video, and any other type. The type comes from the extension and is checked against the content (magic bytes and structure), so a renamed executable, an HTML page named .txt or a file of two types (a PDF with a ZIP appended, an image with a PDF header) is refused. Each attachment's blob carries the media type of its type (application/pdf, text/csv, text/plain for source code, …).
    • Content conditions, checked without running anything and within bounds (at most 64 MiB decompressed is read; ZIP entries and declared sizes are limited, entries that overlap or decompress past their declared size are refused): PDFs without JavaScript, embedded files or XFA; automatic actions (/OpenAction, /AA) only a destination or a GoTo inside the document (as LaTeX's hyperref writes); link annotations only GoTo or URI; no other action (Launch, SubmitForm, ImportData, GoToR, GoToE, Rendition, Named, …), also inside compressed object streams; an encrypted PDF only if it opens with the empty user password (RC4, AES-128, AES-256; it is then decrypted and checked), otherwise refused. xlsx and docx (read as ZIP in memory, never extracted): no macros (vbaProject.bin, macro-enabled types), ActiveX, embedded objects, external links or other external relationships, data connections or web queries, DDE, functions that fetch external data (WEBSERVICE, RTD, IMAGE, STOCKHISTORY, CALL, …), field codes that run or fetch (DDEAUTO, INCLUDETEXT, INCLUDEPICTURE, LINK, …, or a field name built from a nested field) and remote templates. Text must be text (no NUL bytes except UTF-16 with a BOM); JSON and notebooks must parse.
    • What happens: the build (--portable) leaves out an attachment of a type that must not be attached or that fails a condition, with a warning naming the rule; values in CSV, TSV or xlsx that a spreadsheet may read as formulas (starting with =, +, -, @ or a tab; numbers excepted) and text that looks like a credential or a device-specific path are packed with a warning. The quality checker reports what the spec forbids (MUST NOT: forbidden types, a type the role does not take, audio and video, JavaScript in a PDF, macros, files of two types) as errors and the rest (SHOULD: the other conditions, content that does not match or cannot be checked; formula injection, credentials) as warnings.
    • Limits: 10 MiB per file, 20 MiB together, 100 files (base64 makes the Egatari file about 4/3 of that); files over a limit are left out with a warning (ATTACH_LIMITS in kit/core/attachments.mjs). Attachments are not code (they do not make the file Extended), are not added to requires (a reader may ignore them) and are read within the build's byte budget.
    • egatari.json in the Egatari file lists exactly index.attachments (spec §3.3): entries that are not listed for readers are left out of the copy in the file, with a warning; the working folder's egatari.json is not changed. Unpacking writes index.attachments into egatari.json (also for a file that has none), and packing a folder takes the list from egatari.json, so unpack → pack keeps the records. A file whose egatari.json lists other attachments than its index is refused (attachments-mismatch).
    • The player of an Egatari file lists the attachments under ? (title, description, file name, role, size) and with a paperclip button in the toolbar, download only: it offers only entries with a valid title, whose extension the role takes and whose blob has the matching media type (an .html file, a .js file or a CSV listed as pdf is not offered even if the index lists it), saves each under the file name of its path (a name the operating system would not accept is not offered rather than renamed), notes on code attachments “Run code only if you trust its source”, checks the index hash and each attachment's size and SHA-256, checks the first bytes against the type (a .txt that starts as HTML is not saved), and saves the bytes as application/octet-stream through a download link, so nothing is opened or run, in the page or in a new tab. Play-only copies (--play-only) and the normal build carry no attachments (the format removes the archive layer). The quality checker also warns about missing roles or titles, unknown roles, listed files that do not exist, names that are not allowed (outside attachments/, .., reserved names, case collisions, hidden files, links) and files over the limits
  • Exported copy (exportedFrom, spec §3.1): an export from a working folder (--portable, npm run portable -- pack, the editor's Egatari file export) records {"kind": "folder", "sourceSha256": …} in the index, the source hash at export. It holds no time, path or user name, so exports stay byte-reproducible and packing an unpacked file gives the same file. Saving the Egatari file in the editor keeps the marker unchanged (also when saving without rebuilding), so a source hash that differs from it shows the copy was edited after export; saving it with Save as… as a new file (a new docId) drops the marker, and exporting from the folder again replaces it. A lesson whose source of truth is an Egatari file (saved by the editor without a folder, pasted or opened from deck.html alone) has none, and play-only files have no index. Unpacking makes the new folder the master: egatari.json does not carry the marker, and its next export writes a fresh one.
  • Audio records: each step's audio keeps the hash of the script it was made from. When the TTS cache shows that the script changed after the audio was made, that hash is written as null (unknown, so the step counts as stale); unpacking and packing keep the null. The runtime's audio entries also carry the audio media type, so a player does not need to guess it.
  • Warns above 30 MiB (split long lessons into chapters).
Option Meaning
--portable write the Egatari file (default <working folder>/<lesson title>.ega.html, at the top of the folder; -o sets the path)
--play-only write a play-only copy (default <working folder>/<lesson title>.play.ega.html): no source, only the runtime and its audio. Use it for participants when the source or extension data should not be handed out
--without-live-results with --portable: leave out attachments that hold live results (participants' questions and answers). Without it, an Egatari file that contains them prints a warning. Play-only copies never contain them (Live)
--update-themes rebuild with the current version of bundled themes that egatari.json pins to an earlier version (themes, below), and rewrite those pins; prints the colors that change

npm run portable -- unpack <file> <dir> [--force] extracts the source and audio into <dir> (and writes audio/manifest.json from the audio records, so the folder builds and packs again). It reads the file as text and never runs its scripts; it checks the index and every hash, the size limits and the path rules of the spec, refuses to write outside <dir> or through symbolic links, and refuses a non-empty <dir> unless --force. npm run portable -- pack <dir> [-o file] [--play-only] [--without-live-results] is build --portable on that folder: packing an unpacked folder gives the same file again. npm run portable -- attach <dir> <results.json> [--en] adds live results downloaded from the Live server to the working folder as a data attachment (attachments/live-<room>-<date>.json, title “ライブの結果(、)”, or in English with --en); it refuses anything that is not results of the vendored schema, files over 10 MiB, files that fail the §3.8 content checks and results already attached, and warns (but adds) when the live session's id differs from extensions.live.room in egatari.json. npm run portable -- compare <file> <dir> takes a copy exported from a working folder and that folder (same docId in egatari.json) and says, by source hash, which was edited after the export: same, copy (unpack the copy to bring the edits back), folder (export again) or both (choose which to keep; nothing is merged). It writes nothing; a file without exportedFrom or a folder of another lesson is refused.

Reading an Egatari file (unpack and the editor app) also refuses an audio/manifest.json inside the archive (it is always made from the audio records) and a settings file whose docId differs from the index, and recomputes the source hash from the files: when it differs from the index or from the runtime metadata, the reader warns that the compiled lesson and its source disagree. A file of a newer major version, or one that requires a feature the reader does not implement, can only have its source extracted (readPortable(text, {extract: true}) and the editor's Export working folder…), and only when the index and the blobs check out with the 1.x rules; it is never edited or re-exported.

Editing without rebuilding (tier A)

kit/core/tier-a.mjs (tierA(original, files, {identity?, fonts?}), no Node or browser dependency, runs nothing) saves an edit of script, audio or timing overrides into an Egatari file without rebuilding it, as the format's tier A (§7.2). files is the edited working folder (the unpacked shape, with audio/manifest.json). It rewrites only: the changed entries of deck-narration, deck-marks, deck-timing and deck-audio (other entries keep their bytes, also their formatting); the changed <div data-step-key> of the readable layer; sourceSha256 in deck-meta (and docId with identity); the changed members of the index (files, blobs, changed audio records, requires, sourceSha256, the entries of extensions made from the extension lines that changed, and docId / metadata.derivedFrom with identity, which also removes exportedFrom) and its data-sha256; the blob blocks (new content appended, unreferenced blobs dropped); and, only when the changed script needs characters that the file's @font-face rules do not cover, a new <style id="…font-supplement-<n>"> with the @fontsource split files that cover them (fonts: {faces(key), load(file)}): <n> is one greater than the largest existing supplement number (from 0), the element follows the last style element with @font-face rules, and it has one @font-face per embedded face (family, weight, style) that lacks the characters (a file that embeds only weight 400 gets only 400; a family the file does not embed gets none, with a warning). Every other byte stays. Unknown members are kept at every level. Audio records of steps whose script changed are kept as they are (their inputHash no longer matches, so they are stale); when a step's script is removed altogether, its audio record and blob are removed too, with a warning. Script for a key that matches no slide is left out of the readable layer, with a warning. It returns {ok: true, html, index, deckMeta, changes, warnings} or {ok: false, code, reason} when the edit needs a rebuild (source-changed: a file other than narration.md, timeline.json or the settings file changed), when the compiled lesson and the source disagree (source-mismatch), when timeline.json is not a JSON object (timeline), or when a region cannot be located (layout). Packing the unpacked result gives the same source files, audio records, runtime JSON blocks, readable layer and index; it differs in the fonts (a rebuild selects them again into deck-base and adds no supplement) and may order the blob blocks differently. tools/test-tier-a.mjs tests this with a region-aware diff (tools/tier-a-diff.mjs).

Attachments are archive entries, so adding, removing or renaming files under attachments/ and changing their roles and titles in egatari.json are tier A edits too: only the index (files, blobs, sourceSha256, attachments), the chunks and deck-meta.sourceSha256 change. It refuses (source-changed) when the slide source mentions the changed attachment's path (the build may have read it) and (attachments) when an added, changed or re-roled attachment breaks the limits, the name rules or the §3.8 types and conditions above; an attachment that was already in the file and did not change is kept as it is, with a warning if it does not meet them. When egatari.json lists attachments that are not in index.attachments (no role or title), the saved copy lists exactly index.attachments, and the result carries that text as config so that the editor writes it to its workspace too.

TTS

uv run --project tts python tts/synth.py <deckdir> [options]. Details: video.md.

Option Meaning
--voices expert=a,novice=b voices (over voice.json)
--ref-dir DIR reference voice folder (over IRODORI_REF_DIR; default tts/voices/)
--engine irodori|voxcpm2 over voice.json engine and the lesson language
--yomi FILE reading dictionary (over MOTION_YOMI; default tts/yomi.yaml). Japanese only
--only key,… only these steps (slideKey:step)
--dry-run no TTS: silence of estimated length (7.5 characters/s Japanese, 2.5 words/s English); separate cache
--force ignore the audio cache
--replace-recorded also replace recorded and imported audio (manifest kind recorded / imported, written by the editor app's Audio panel) with TTS; without it they are kept
voice.json field Meaning
expert, novice a name (<ref dir>/<name>.wav) or a path (contains / or ends in .wav; relative to the lesson). Defaults bob, alice. The reference voice is never packed into the Egatari file (.ega.html): keep it outside the working folder
lang language (default: lang of the lesson's HTML — the file you pass, <deckdir>/deck.html, or the only *.html in the folder — else ja)
engine irodori (default for Japanese) or voxcpm2 (default otherwise)

Writes audio/seg_<digest>.wav, audio/seg_<digest>.json and audio/manifest.json (steps removed from the script are dropped from the manifest; their wav files stay). Each manifest entry also carries kind: "tts" and inputHash, the hash of the step's script (format spec §5.4), so the editor app and the quality checker can tell when the script changed afterwards. Recorded and imported takes are kept (see --replace-recorded). The editor app can also record, import or regenerate one step at a time in the browser (editor); its post-processing uses the same numbers as tts/core.py. Warns if dry-run audio remains. When the server is not localhost, voice paths are not checked locally. Rebuild afterwards.

Video export

node render/render.mjs <built.html> --out <dir> [options] (opens the lesson with ?mode=video).

Option Meaning
--out dir required
--fps n default 30
--scale x default 1.5 (1920×1080)
--only key,… only these steps; the rest stay as they are
--full also full.mp4, and with two or more chapters chapter_NN.mp4 and chapters.json (needs all clips)
--force ignore .render-cache.json
--captions burn the script captions into the video (?captions=1); part of the cache key

Outputs: clip_NNNN.mp4, clip_NNNN.png, timeline.out.json (source, fps, scale, width, height, duration, clipDuration, segments[] with index, clip, still, key, slideKey, stepKey, i, k, title, chapter, start, dur, anim, lead, audioDur, audioOffset, frames, clipDur, clipStart, start_ms, end_ms, transcript, text), .render-cache.json. Clips beyond the current number of steps are deleted.

Tools

Command Purpose
npm run new -- <folder> [--lang en|ja] [--theme name] [--title "…"] new working folder (deck.html + narration.md) from a clean template; refuses a non-empty folder
npm run ids -- <working folder | deck.html> [--dry-run] give every slide without an id a new id (s- + 6 random base32 characters), rewrite narration.md headings to ## heading {#id}, rename the keys in audio/manifest.json, timeline.json, comments.json and data-morph="…"; audio is not re-synthesized. Backups go to .cache/edits/, line endings are kept, a second run changes nothing. Refuses a lesson with duplicate keys, id collisions or invalid ids
npm run portable -- unpack <file> <dir> [--force], npm run portable -- pack <dir> [-o file] [--play-only] [--without-live-results], npm run portable -- attach <dir> <results.json>, npm run portable -- compare <file> <dir> Egatari file ↔ folder; see Egatari file
npm run player:check [-- --strict], npm run player:bump -- patch|minor|major the bundled player (Egatari Player): compare its files (PLAYER_FILES in kit/core/player.mjs) with the release in kit/player.lock.json (information only; --strict fails when they differ, for the release candidate); bump writes the new version to kit/player.json and the lock and adds changes/player-<version>.md. See RELEASING.md
npm run build:app build the editor app: the hosted editor in dist/ (for GitHub Pages; dist/index.html) and the Offline editor dist/editor.html. See editor.md
node tools/test-app.mjs tests the editor app (OPFS folder, edit, script, comments, external changes, export, presenter)
node tools/get-fonts.mjs download the BIZ UDPGothic and BIZ UDPMincho TTFs into fonts/ (enables glyph subsetting with uv)
node tools/test-kokugo-example.mjs the kanbun of examples/kokugo-kanbun reads without warnings with the library's m-kanbun (no browser; the parts' own tests are in egatari/components)
node tools/theme-metrics.mjs measure <theme> [--lang ja|en], node tools/theme-metrics.mjs check <theme> render a theme (a folder or a name, as for --theme) and print its metrics block measured from the rendered boxes, to paste into theme.json (whole pixels, each edge rounded outward to the more lenient limit by less than 1px; exits with 2 without measuring when the bundled fonts do not load); check compares the declaration (or the defaults) with the rendered boxes and exits with 1 when they differ
node tools/test-theme-metrics.mjs [workdir] theme metrics: validation and inheritance, the theme hash, every bundled theme renders as declared (strict), the limits and themeMetricsDrift in the checker and the editor's quick check
node tools/test-layout-tate.mjs [workdir] layout-tate slides in the default and kokugo themes: heading column, body, conclusion column and page number placed without overlap, seek determinism, quality-check standards, slide keys and readable layer unchanged
node tools/test-themes.mjs [workdir] [--shots <dir>] every bundled theme: a sample lesson builds and passes the quality check; role-colour contrast, colour-vision simulation and the pen colours (see Themes); the look shapes in tokens.css match kit/core/theme-shapes.mjs; prints the numbers, and with --shots a contact sheet per theme
node tools/test-fonts.mjs [workdir] fonts: only the chosen fonts are embedded (from theme.json fonts or named in CSS), Source Sans 3 italic in English lessons and for Latin text in <em>/<i> of Japanese ones, and fontNotEmbedded for a font that is not in the file
node tools/pdf.mjs <built.html> <out.pdf> PDF, one page per slide
node tools/shot.mjs <built.html> <out.png> [i] [k] [mode] PNG of slide i (0-based), step k, final state
npm run sheet -- <working folder | deck.html | built.html | .ega.html> [out.png] [--mode talk] [--cols 4] [--width 640] [--tiles] [--steps] contact sheet: one PNG with every slide at its final step (1-based number, and the key if short; default tile width 640 px; output <deck>/out/sheet.png); --tiles also writes each slide's PNG to <out>-tiles/. --steps instead lays out every step of each slide that has a figure or part, side by side, one row per slide (default tile width 320 px; output <deck>/out/steps.png) to check a figure's constraints step by step. Prints the path and the slide count
npm run comment-shots -- <working folder | deck.html> [id …] [--all] for each open comment in comments.json (or the listed ids, or all with --all), PNGs of its step's final state with its pin (and ink strokes) drawn: <deck>/.cache/comment-shots/<id>.png (480×300 of the slide around the pin at 2×, at least the bounding box of its strokes plus 48 px, the whole slide without a pin), <id>-full.png and index.json. A comment whose step is gone is reported, not shot
node tools/catalog.mjs regenerate vendor/components/CATALOG.md
npm run components:update -- vX.Y.Z [--from <folder>] bring in a release of egatari/components into vendor/components/ and components.lock.json (checks sha256 against manifest.json; never replaces a version)
npm run components:check vendor/components matches components.lock.json (no network)
node tools/narration-json.mjs <narration.md> [--voice json] parsed script as JSON (used by synth.py)
node tools/smoke.mjs <built.html> <outdir> timeline and a few screenshots
node tools/test-components.mjs [name …] [--shots dir] build and test part demos
node tools/test-tools.mjs <built.html> [outdir] overview, pens, laser pointer, print, presenter ink
node tools/test-controls.mjs <built.html> [outdir] toolbar, help, presenter buttons
node tools/test-autoplay.mjs <built.html> autoplay and its presenter-view button
node tools/test-links.mjs [dir] links to slides: fragments at load, clicks in each mode, Back (button, keys, browser history), autoplay kept, embedding (position with cause: "link", linkRefused), presenter view, m-menu, quality checker errors
node tools/test-dev-server.mjs [workdir] dev server on a copy of examples/minimal
node tools/flake.mjs <built.html> <i> <k> <outdir> investigate a false "depends on call order" report

Environment variables

Variable Used by Meaning Default
CHROME_PATH check, pdf, shot, render, tests browser executable Google Chrome, then Microsoft Edge, then Playwright's Chromium
IRODORI_URL TTS OpenAI-compatible speech server http://localhost:8882
IRODORI_REF_DIR TTS reference voice folder (--ref-dir wins) tts/voices/
MOTION_YOMI TTS reading dictionary (--yomi wins) tts/yomi.yaml
EGATARI_THEMES build, check, dev server, Egatari file your theme folders, searched after the working folder and before the bundled themes/ (Themes); separated by : (; on Windows) none; ~/.egatari/themes is searched after them when it exists