Version 1.4.0. It reads format 0.20 (SPEC.md in the Booklet repository). A note marked with any 0.x version, earlier or later, opens as it is, and its marker alone never produces a notice. The view says something only when the note holds a part it does not read; that notice is described under What it does not draw yet. It is not desktop-only: every release has been tried by the author on an iPhone 17, and each build is checked under Obsidian's own mobile emulation and at a phone's width.
One screen at a time. A booklet opens on home: a card for each module and each activity that is in no module, as numbered rows. A module has a screen of its own, with its opening and its activities as cards (tab by tab when the module has tabs), and an activity has one too: its opening, a tab bar, and one page at a time. A booklet that offers one activity opens into it; one with a single module of several activities opens on that module's screen; start: home keeps home. Activities marked hidden are not drawn and do not count. Which screen, tab and page is showing is saved in Obsidian's own workspace file with the pane, never in the note, so it comes back after a restart and Obsidian's Back and Forward arrows walk through it. The note's own bytes are never changed by moving about.
For a keyboard or a screen reader: a tab bar is a tablist named in plain words (the title without its Markdown marks), and the chosen tab controls the panel under it (on an activity's screen, the page body, named by its tab and described as "Page 2 of 3"; it keeps the page group's own label "Page 2 of 3" as well, which a screen reader gives up for the tab's name). After a press that moves to another screen, focus goes to that screen's title, and after one that turns the page, to the page, so a reader starts there; Obsidian's own Back and Forward, and a tab chosen with the keys, leave focus where they put it. A - - - line straight under a line of text is a page break, as Markdown reads it (a run of hyphens with no spaces there underlines the line as a heading).
Cards: a title, a picture, a blurb and a status line. The picture is the line's icon: (any of Booklet's 161 icon names, drawn by Obsidian's own Lucide set) or a leading emoji, else the card's number. The blurb is the activity's first paragraph. The status line is the file's own (a > line under the activity line, or status: off for none), else worked out: "2 questions", "1 of 2 answered", "Done", entries kept, and "N reads" for the folded notes a person reads.
Moving about: a card opens its screen; the trail at the top of every screen below home names where you are and each step above it is a link (the booklet's title leads it as the link home, except where the step after it says the same words, as a module of one activity with no words of its own does, being called by the booklet's title; then the title is left out and the activity stays, and the pager's Back to the overview goes home); the pager at the foot of an activity has Back, "Page 2 of 3" and Next, and Next on the last page goes on to the next activity of its module in the file's order, also between activities that are in no module. Choosing another tab is not a step in Back and Forward; a page, an activity or a screen is. Each tab keeps the page last seen there while the pane is open. The tab bar takes the arrow keys, Home and End. A tab chosen while a page is turning ends on that tab, and a double press on Next turns one page.
tab: and a module's notice: a module's tabs hold its activities as the activities' tab: settings say (an unknown one joins the first tab and is listed in the opening notice); a page break (***, or --- after a blank line) makes a new section of a page; an activity does not sit under another (format 0.17), so parent: on an activity line is listed among the note's problems as a setting the kind does not take and ignored, and the activity is an ordinary one (a tree is made with page lines: Pages, below); a module's notice (licence, copyright, source, version, from a [!notice] callout, or from the front matter of a file with one module) is a closed "About this module" disclosure, with a source linked only when it is a web address.
Links: [[#Heading]] and [words](#slug) inside a note open the first place in that module whose heading, activity title, tab name, question or callout title reads those words, as a step in Back and Forward (Ctrl or Cmd opens a new pane); words nothing holds are plain text. A link to a module ([[week-2.booklet]]) opens it if the booklet holds it and otherwise reads "not in this booklet", and never creates a note. A link from another note, [[Check-in.booklet#Today]], opens the screen, tab and page that hold the words, looking through the whole file for the first match.
Openings: the booklet's opening text (under the booklet's title on home) and each module's (under the module's title), rendered by Obsidian. A heading that repeats the title is not drawn twice.
Activities: an activity's prose (rendered by Obsidian, so links, images, callouts, hints and solutions look as they do in Reading view), and its questions. Finalize, its note and the kept entries of an activity that repeats are drawn under a page only on a tab that holds a question (or when the opening above every tab does).
A note with no activity line and no module line is one activity, named by the front matter title (SPEC section 3): its prose, its tab lines, its page lines and its questions are that activity's, so a one-page worksheet needs no structure. It has no id, so it cannot repeat, and its answers are kept under the plain records line. A booklet of one activity opens straight into it. A note with nothing in its body draws "This note has nothing to draw."
entries:folded and description: (format 0.17). entries:folded on the line of a repeat activity starts its list of kept entries folded, closed until the reader opens it. The reader's own opening and shutting of the list wins for as long as the pane is open, and nothing is written for the mark or for the choice. Any other value of entries: is listed among the note's problems and ignored. description: in the front matter is kept as written and shown nowhere: the view has no list of booklets, search result or link preview of its own to put a description in (Obsidian shows what it shows of the file itself), so it does not use the first paragraph as one either.
The eight common question kinds, stored the way the format and its reference renderer store them:
| Kind |
How it is answered |
What is stored |
choice |
one button per option; press again to clear |
the option's position, from 1 |
multi |
buttons that toggle |
a list of positions, in the order picked |
scale |
one button per anchor |
the anchor's number |
matrix |
one row for each item, one button per anchor in it (a grid where the pane is wide enough, else each item as a single-choice question under the matrix's words); press the picked anchor again to clear an item |
a list with an anchor's number for each item, null for one not answered |
text |
a text field (long: a larger box) |
the text as typed |
lines |
one field per line, a new one as you type |
a list of strings, one per field |
number |
a number field (min:, max:, step:) |
a number |
date |
a date field, with Clear |
YYYY-MM-DD |
A lines answer lists every field as the reference renderer does, so a blank line, and the empty field at the end, are in it as ""; a kept entry leaves them out. A choice or multi with open takes options of your own, stored as their words; your earlier ones are offered again (below, Earlier words). Words typed in its Add your own field are added as Enter or Add would add them when you leave the field, open another note in the pane, close the pane or switch it to Markdown, and before Finalize (SPEC section 5); on a choice they replace the pick. White space adds nothing. An option's reply (the indented lines under it) shows when it is picked.
Right and wrong (SPEC section 5): where an option is marked [x], a choice marks the pick right or wrong when it is made, and shows the answer when the pick is wrong, with a line under the options ("Right.", "Not quite. The answer is ...") that a screen reader hears. A multi has a Check button that marks the picks as they are then: each right pick right, each wrong pick wrong, and the right options missed as the answer. Any change of the picks after it takes the marks away, and putting a pick back does not bring them back (they come back when Check is pressed again, as in the reference renderer); a multi shows its replies only after Check. A mark is a word and an icon as well as a colour. Marks are never written into the note, and never clear or change what was picked. A question with no [x] option marks nothing.
Shared lists (menu:): a choice or multi whose options come from a menu callout draws that menu's options, found as the format says: in the question's own module first, then in the data section outside every module. The answer is a position in the menu, from 1, as for options of its own.
Two widgets (widget, with its booklet widget block found through the ![[#^id]] line), stored as the reference renderer stores them:
| Engine |
How it is answered |
What is stored |
grid-select |
cells under four edge labels; press items, add words |
item ids, and x|cell|words |
svg-regions |
places as buttons; a place's words; a summary |
[{"id": place, "s": [word ids, own words]}] |
The grid (grid-select, the feelings grid) draws each cell with its name in the cell's colour, its items to press and a field for words of your own. The answer keeps the order you picked (SPEC section 8): words of your own typed in a cell go at the end of the list, and taking one out leaves the others where they were, as the reference renderer keeps it. The body widget (svg-regions) is drawn without its figure: the places listed in chips are buttons, pressing one shows its words in their senses groups and a field for words of your own, and a summary under it lists each place marked, with its words and a button to clear it. A word you type that is one of the place's words (in any case) is stored as that word's id. With readonly, a grid shows its words and the body widget names its places (from regions), with nothing to press. With describe, the cells' and places' notes are listed. A kept entry shows a grid's answer as its words, and a body widget's as each place with its words.
A question asked for each pick of another (each:<id>, SPEC section 5). A question of any kind or a widget with each: names a choice, a multi or a widget in the same activity, its source, and is asked once for each pick in the source's answer: the question's title and help once, then a copy of the question (its options, field, scale or matrix, with its marks, open, min:, max:, step: and earlier:) under each pick's words (an option's words, an item's or a place's label, or your own words; for a grid's x|cell|word, the word). Nothing is drawn while nothing is picked. The answer is a list of pairs, [{"pick": <the pick as the source stores it>, "a": <the answer of the question's kind>}], one for each pick with an answer. Taking a pick back keeps its pair, not drawn, and picking it again brings it back; a kept entry keeps it too and shows one line for each pick its source holds, "
· <pick>". A draft whose only answers are pairs for picks taken back counts as empty. Your own options under one pick are offered under every pick, and in later entries. An <code>each:</code> that names nothing in its activity, names the question itself, names a question of another kind or chains (names a question that is itself asked for each pick) is listed among the note's problems and ignored.</p>
</li>
<li><p><strong>Earlier words</strong>: some of your own words come back as buttons to press (SPEC section 5, "Words the reader wrote before"). An option of your own (<code>open</code>) is offered again after the question's options, and under every pick of a question asked for each pick; a word you wrote for a place of the body widget is offered under every place, after the widget's words, in a group named <em>Earlier words</em>; and a <code>lines</code> question whose line sets <code>earlier:<n></code> offers its earlier lines under its fields, in a group named <em>Earlier words</em>. A grid cell's words and a <code>text</code> answer are not offered again. The words come from the activity's kept entries, newest first, and from the answer being written (the body widget's other places, the other picks' copies); never from another draft. In a <code>repeat daily</code> activity today's kept entry is not a source, since the draft will replace it, and under <code>each:</code> only the pairs whose pick the source holds in the same entry are. A body widget's own word is one that is not one of its word ids and does not start with one of its group ids and a colon. At most 12 are offered at each place, or the number <code>earlier:<n></code> gives (digits only; <code>earlier:0</code> offers none), counted after leaving out the words listed there and the words already there (an own option or a line compared in any case, white space at either end aside; a body word as the format compares a widget's words). A press stores the words exactly as typing them there would: at the end of the answer or the place's words, or in the empty line at the end of the lines, with a new empty line after it. It never changes what you have typed, Enter never takes an offered word, nothing is written until you press one, and the entries they came from are not touched. A long one is shortened to two lines on screen, and a press takes its full words.</p>
</li>
<li><p><strong>Activities that repeat</strong> (<code>repeat</code>): what you type is kept as the activity's draft (a <code>booklet draft <activity></code> block), so it is still there when you come back. <strong>Finalize</strong> keeps the draft as a dated entry (in a <code>booklet entries <activity></code> block, with a <code>ts</code> such as <code>2026-10-27T18:30:00Z</code>) and empties the fields. The kept entries are listed under the activity, newest first, each with its date and its answers in words from the note's own lists. The list is a fold: its heading, "Kept entries (4)" ("Kept days" for a daily activity), is a button that opens and shuts it with a press, Enter or Space. It starts open, the choice is remembered for each activity while the pane is open, and it is never written into the note.</p>
</li>
<li><p><strong><code>repeat daily</code></strong>: one kept entry a day. Once today's is kept, the fields show it; change an answer and Finalize again to replace it. "Today" is the day on the device's own calendar when the view was drawn (SPEC section 8), so a check-in left open past midnight keeps writing to the day it shows: Finalize then keeps the entry at that day's last second, replacing that day's entry. The view is drawn again, and today moves on, when it comes back into use on a later day (the window or the app shown again, or its pane made active) while no field is being typed in.</p>
</li>
<li><p><strong>Queries</strong> (<code>booklet query</code> in an activity's prose, SPEC section 6): kept entries drawn as <code>cards</code> (the default: one dated card per entry, its questions as lines), a <code>table</code> (one column per field), a <code>list</code> (one line per row: badge, label, the other fields, value, and the note beneath) or <code>tiles</code> (a big value over a label; with no value anywhere, a strip of pills). <code>from:</code> names an activity that repeats, or one question (which is then the value); <code>fields:</code>, <code>limit:</code>, <code>empty:</code>, <code>group:</code> and the roles (<code>label</code>, <code>value</code>, <code>note</code>, <code>badge</code>, <code>tone</code>, <code>link</code>, <code>icon</code>) are read as the format says. Kept entries only, newest first, never a draft; an answer of the wrong shape is left out of its row, and a question asked for each pick is one field per pick, "<title> · <pick>" (named alone in <code>from:</code>, it is one row per pick instead). A tone is also said in words to a screen reader; a <code>link</code> that is a web address or a note's path makes the row's label lead there, and any other target is plain text; an <code>icon</code> is drawn when Obsidian has an icon of that name.</p>
</li>
<li><p><strong>Dashboards</strong> (SPEC sections 6 and 7, 0.9.0):</p>
<ul>
<li><strong>A data block as a source.</strong> <code>from:</code> may name a <code>booklet data</code> block (its <code>^id</code>), and every view draws its rows: the block's <code>fields</code> are the headings, a list of values is several pills, <code>group:</code> works as it does over kept entries. A data block is the file's own: it is read, and the only things ever written into a data file in the vault are the one value of an in-place edit you kept (see <em>Editing a field in place</em>) and a row you added or removed (see <em>Adding and removing rows</em>); an inline block in the note is changed by the same three acts, between its own fences and nowhere else. One that is not JSON, or does not hold rows, is listed among the note's problems, and a query that names it is listed too (it names something that is nothing in this module) and is not drawn. Rows that nest and open are described under <em>Rows that open and data files</em>, below.</li>
<li><strong>Charts.</strong> <code>as: bars</code> draws one bar for each row, from zero, its length the <code>value</code>, its colour the row's <code>tone</code> (Obsidian's green, orange and red, said in words too); <code>as: line</code> draws a line for each field <code>fields:</code> names (Obsidian's named colours, told apart by dash and marker as well), a gap where a row has no number, and a readout line under it that a tap, a hover or the arrow keys, Home and End move along the points. A kept-entry chart runs from the oldest to the newest day and <code>limit</code> takes the newest N. A scale answer is plotted by its number. Every chart has the same rows as a table under a closed "Show the numbers", and one that cannot be drawn (the field is not a number, no field is named), has one reading, or whose bars are all 0, says so in a sentence in its place. A chart is drawn as plain SVG, named by hidden text and <code>aria-labelledby</code>.</li>
<li><strong>Sort and filter</strong>, the reader's own. From nine rows a view has a closed "Sort and filter" button: a search over the fields the view shows, a "Sort by" menu with a direction button, a count ("3 of 12 shown") and Clear; a table's headings sort too (ascending, descending, the original order). A row with no value sorts last whichever way. A line keeps its order and has the search alone. What the reader sets is kept while the pane is open, on this device, and is never saved in the pane's state or written to the note. A nested view adds <em>Open all</em> and <em>Close all</em> beside Clear.</li>
<li><strong>Delete a kept entry.</strong> Each card of the Kept entries list under Finalize has a Delete button that asks "Delete this entry?" in place ("Yes, delete" or "No") and writes nothing until Yes. The entry is named by its place among the activity's kept entries and by what it read as when its card was drawn, so a pane that is a step behind the file (an entry deleted elsewhere, even one with the same <code>ts</code>) removes nothing and says so, and the card is left with its Delete button. Delete, its question, Yes and No are named with the entry's date for a screen reader, and Escape closes the question. The entry is taken out as one item of its list and every other byte of the note is as it was; a block that loses its last entry stays <code>{"items": []}</code>. A query's cards do not offer it.</li>
</ul>
</li>
<li><p><strong>A span of days and summaries</strong> (SPEC section 6, "A span of days, and summaries"): <code>limit: 7 days</code> keeps the entries of today and the six dates before it, on the device's calendar, today being the day the view was drawn; an entry whose <code>ts</code> will not parse, or that is later than now, is in no span. <code>summary: on</code> gives a row for the entries' dates ("Days logged"), then a row per question in the activity's order, with <code>count</code>, <code>days</code>, <code>average</code>, <code>lowest</code>, <code>highest</code>, <code>total</code> and <code>most</code> as the kind has them and the default <code>value</code> and <code>note</code> ("Good", "Rough 1 · Mixed 1 · Good 3"); a grid is summarised like a <code>multi</code>, and a question asked for each pick gives a row per pick. A query of one question with <code>summary: on</code> gives its distribution, listed options, anchors or items in list order with zeros. With no <code>as:</code> a summary is a table. The rows are worked out when the view is drawn and again when the records change, so an entry kept shows at once; nothing is written.</p>
</li>
<li><p><strong>Groups, and questions inside callouts</strong> (SPEC section 6): a <code>group</code> is a titled box on plain paper that holds prose and questions, written with one <code>></code> less for what it holds; a <code>[!group]-</code> is folded and opens on its title (Enter or Space too), and one that holds an answer is drawn open. A question can also stand inside any other callout (a tip, a note), which goes beyond SPEC section 6 and the reference renderer (they take a group's questions only, and the linter warns of one in a tip); it is drawn in Obsidian's own callout box with its title, icon and content, and its answer is kept in the activity's records like any other. A line that makes structure (a module, an activity, a tab, a row, data) inside a callout is ignored and listed among the note's problems. A fold the reader opens or shuts is remembered while the view is open, never in the note. A group or a callout that holds a question may stand in a row's cell: it is a box inside the cell, with its question.</p>
</li>
<li><p><strong>Steps</strong> (SPEC section 12, "one at a time"): a page with two or more groups offers <em>One at a time</em> and <em>All at once</em>. All at once is how it is drawn first. One at a time draws what comes before the first group (the lead) whole, then one group a step, with "Step 2 of 4" and a row of dots (done where every question in the step has an answer); Back and Next move a step before they move a page, and Next on the last step goes on as it does on the last page. A step is not a step in Obsidian's Back and Forward, and the saved place does not change. Every step is in the page, hidden when it is not on show, so no answer is lost; the page is drawn at the first step with a question not yet answered. A step draws its group open even when the note wrote it <code>[!group]-</code> (SPEC section 12); the reader can fold it, and that is remembered while the view is open like any fold. The choice is kept on the device (the vault's local storage, per note), never in the note, and goes with the note when it is renamed or moved.</p>
</li>
<li><p><strong>Rows</strong> (SPEC section 3): the cells of a <code>[!row]</code> ... <code>[!row|end]</code> are side by side where there is room and stacked on a phone; each heading at the shallowest level starts a cell, and with no heading each block is a cell. A question stands in the cell it was written in, and a group or a callout that holds one is a box inside its cell. Only in an activity's pages: a row left open at a page break, a tab or the end ends there.</p>
</li>
<li><p><strong>Reading kinds and sources</strong> (SPEC section 6): <code>diff</code>, <code>law</code> and <code>opinion</code> callouts are drawn in the reference renderer's colours, pictures and borders (cyan with a dashed border and the git-compare picture, the theme's own text colour with a double border and the scale, pink with a dotted border and the message-circle) on Obsidian's callout variables. A footnote's mark (<code>[^id]</code>) is a numbered button; pressing it opens its source under the paragraph (title, detail, page, the quotation, and a "Verified" date), or a plain reference where the definition does not fit the citation pattern (<code>*Title*, detail, p. N: "quotation" Verified YYYY-MM-DD.</code>); one is open at a time, and a second press, the Close button or Escape shuts it. Marks are numbered by first use on each screen; a mark with no note shows <code>[?id]</code>; marks in a title, a label or the booklet's opening are left out, and marks in code (a fenced block at any indent, an indented block, a code span of any length) or in maths are left alone. A source opens under the paragraph its mark is in; from a list item it opens inside the item after its words, and from a table cell in a row of its own across the table. A definition is read where the file puts it (a module's own notes first, then the data section's) and is never drawn as prose.</p>
</li>
<li><p><strong>Pages</strong> (SPEC section 3, "Pages: a tree inside a tab"). A page line, <code>> [!page|id parent:x icon:name] Title</code>, starts a named page in a tab; <code>parent:</code> puts it under another page of the same tab, and a tab's pages are a tree. A tab of pages gets a "Pages" button on the left of the page area, closed to begin with, that opens its tree with a search (see the choices below), the pages under the one on show as cards, and a "Mentioned on" list of the pages that link to it; the trail names the pages above; Back goes up to the parent, Next names the next page, and "2 of 3" counts the sections of a page. A page is a step in Obsidian's Back and Forward, a tab is not, and which page is showing is never written to the note. A note with no page line reads, draws and saves as it did before pages. The reference renderer draws less than this (a trail, Back, Up and Next, and cards for the pages under a page; no menu, no search, no "Mentioned on"): the format lets a renderer draw them, and the plugin does. What the plugin chose where the format is silent, and where it goes beyond it, is listed under <em>Pages</em> near the end of this file.</p>
</li>
</ul>
<h2 id="audio-video-and-listen">Audio, video and <code>listen</code></h2>
<p>Format 0.18 adds audio and video, a question kind called <code>listen</code>, a <code>media-from</code> declaration and a Continue button. This section says what the plugin does with each, what stays on your device, what a host can see, and what the guard does and does not cover. A made-up course that uses all of it is in <code>test/page-notes/Listen demo/</code>.</p>
<p><strong>What plays.</strong> Audio and video are written with the image syntax, and the extension of the address says which they are: <code></code>, <code>![alt][id]</code> with a definition line, and Obsidian's <code>![[a.mp3]]</code>. Audio is <code>mp3</code>, <code>m4a</code>, <code>ogg</code>, <code>opus</code>, <code>wav</code> and <code>flac</code>; video is <code>mp4</code>, <code>webm</code>, <code>mov</code>, <code>mkv</code>, <code>ogv</code> and <code>3gp</code>.</p>
<ul>
<li><strong>A file in your vault, or on your own machine, plays at once, in Obsidian's own player.</strong> "Local" means a file in the vault, or an address that leads to your own machine and nowhere else: the host <code>localhost</code>, an address in <code>127.0.0.0/8</code> such as <code>127.0.0.1</code>, or <code>[::1]</code>, over <code>http</code> or <code>https</code>, with any port. The plugin does not ask permission for what is already on your own device, and does not replace Obsidian's player. Only those spellings count: <code>http://2130706433/</code>, <code>http://127.1/</code>, <code>[::ffff:127.0.0.1]</code>, <code>localhost.</code> with a trailing dot, an address with a user name in front of the host, a LAN address (<code>192.168.x.x</code>, <code>10.x.x.x</code>) a name under <code>.localhost</code> (<code>media.localhost</code>) and a name that merely holds the word (<code>localhost.example.org</code>) are remote and are held back. A frame (<code><iframe></code>, <code><object></code>, <code><embed></code>) on your own machine is a page, not a file to play, and is held back as before. The plugin stores no copy of the file; for a <code>listen</code>, how far you got (a position, second by second) is kept on this device, as the <em><code>listen</code> question</em> below says.</li>
<li><strong>Any other address is remote and is held back.</strong> That includes every address that starts with a scheme and does not lead to your own machine. Nothing remote is requested when a note opens or a page is drawn. It is requested only when you press Play on that piece of media, and only from a host you have allowed; on a first press from a host not yet allowed, the card asks and your answer decides.</li>
<li><strong>No permission is ever read from a note.</strong> A booklet can say where its media lives (<code>media-from</code>) and nothing else: a note someone sends you can change what you are asked, never what is allowed.</li>
</ul>
<p><strong>The allow list.</strong> An <em>Allow list</em> button at the top of the booklet view opens a card. You allow a site, not a file, and you choose how long the allow lasts:</p>
<table>
<thead>
<tr>
<th>Scope</th>
<th>What it means</th>
<th>Where it is kept</th>
</tr>
</thead>
<tbody><tr>
<td>Once</td>
<td>this piece of media only, for now</td>
<td>nowhere</td>
</tr>
<tr>
<td>This note</td>
<td>every piece of media from the site, in this note</td>
<td>the plugin's own data, under the note's path</td>
</tr>
<tr>
<td>This vault</td>
<td>every piece of media from the site, in any note</td>
<td>the plugin's own data, and shown in its settings</td>
</tr>
</tbody></table>
<p>None of these is ever written into a booklet: not into its front matter, not into its records. A site is written the way you would say it, <code>youtube.com</code>, with no scheme and no path. It means <code>https://*.youtube.com/*</code>: that site and every subdomain of it, over https only. The card says that https is assumed, and the write-in blank takes a site and its top-level domain and nothing more. The card's buttons and lists name the scopes the same way throughout: <em>Allow once</em>, <em>Allow for this note</em>, <em>Allow for this vault</em> (and <em>Add for this note</em> and <em>Add for this vault</em> in the write-in).</p>
<ul>
<li><strong>The card lists</strong> the vault's sites and this note's sites, each with a checkbox. Unticking one takes it off the list at once.</li>
<li><strong>The card also opens by itself, as a request</strong>, in two cases. When you open a booklet whose <code>media-from</code> names sites that are not yet on the vault list, it asks about those. When you press media from a site that is not allowed, it asks about that site: allow once, for this note, for this vault, or Not now. Not now is where the focus starts. One line, shared by the choices, says what the site will see ("media.example.org will see when you play and from where."), and each choice then says how long its allow lasts.</li>
<li><strong><code>media-from</code> is a courtesy.</strong> It lets a booklet ask up front. A site already on your vault list loads in a booklet that never declared it, and a booklet that declares a site you have not allowed loads nothing from it.</li>
<li><strong>Escape and Tab in the card.</strong> While the card is open it takes Escape for itself: the app hands a key to the topmost handler only, so one Escape closes the card when it is on top and closes Obsidian's Settings when Settings is (the card does not pull focus back to Settings, and one Escape no longer closes both, on Obsidian 1.11.7 where Settings is a window inside the main one). This holds with focus on a control behind the card or on nothing. Tab and Shift+Tab stay inside the card.</li>
<li><strong>Settings, Booklet, shows the vault list</strong> (a page of one section, so it has no heading of its own) and has a button, <em>Add suggested services</em>, that puts a short list of well-known video and audio sites on it (YouTube, Vimeo and a few audio services, named in the button's description). The list starts empty: nothing is allowed until you choose. What the card writes shows in the settings page, and what the settings page writes shows in the card.</li>
</ul>
<p><strong>Players for services.</strong> A link to a video or audio service the plugin knows (YouTube and Vimeo among them) becomes a slot with a Play button once its site is allowed. The press loads the service's own embedded player in a frame, with focus on it and a spoken line saying so; leaving the page takes it down. A site that is not allowed, and a service the plugin does not know, stay a link that names the site. Allowing <code>youtube.com</code> also covers <code>youtu.be</code>, <code>youtube-nocookie.com</code> and <code>releases.obsidian.md</code>, because the YouTube player is Obsidian's own frame, which is served from <code>releases.obsidian.md</code>; the card says so. A <code>listen</code> that holds a service link has only a tick to set by hand: the plugin cannot read where a framed player is.</p>
<p><strong>The <code>listen</code> question.</strong> A <code>listen</code> holds one piece of media and remembers how far you got. It shows the prompt, the media, a <em>Done</em> tick that you can set and clear yourself, and a line "Left off at 12:34" when you have stopped part of the way. The answer is kept in the note like any other, as <code>{ "pos": 754, "done": true }</code>.</p>
<ul>
<li><strong>When it writes.</strong> While the media plays, the position is kept on your device only, in whole seconds. It is written into the note when you pause, when the media ends, when you leave the page and when Obsidian quits. It never moves playback that is under way: a change to your answers that arrives from another device (Sync) while you listen shows in the Done tick at once, but the "Left off" line does not show the incoming place while the element plays, and playback is never moved, and a change to the booklet's design is held back until you stop. After a hard kill of the app the last few seconds of your place may be lost, because the device saves them late.</li>
<li><strong>When it counts as done.</strong> At the end, or when you pause or stop within <code>left:</code> seconds of the end (120 if the line says nothing) and the position is past half of the media's length. With an unknown length, only the end counts. The plugin never clears a <code>done</code>; only you do, with the tick. When playback ends, <code>pos</code> is taken out of the answer, so it reads <code>{ "done": true }</code>.</li>
<li><strong>Counting.</strong> A <code>listen</code> is answered when <code>done</code> is true, and an activity of listens says "3 of 4 done". A <code>pos</code> with no <code>done</code> is not an answer. In a <code>repeat</code> activity a <code>listen</code> is drawn without a tick and listed in the opening notice.</li>
<li><strong>Playback continues when you turn to another page, switch to another note or close the note, and your place is written when you stop.</strong> The player (Obsidian's own, with its own controls) moves into a small Now playing bar at the bottom of the window with Back to the page and Close buttons. This was measured on Obsidian for desktop (1.14.5). A framed player (YouTube, Vimeo, Spotify, SoundCloud) cannot be moved without reloading, so it stops with its page. Locking a phone or leaving the app is not promised: it has not been measured on a phone, and one forum report says Android cut audio after a few minutes with the screen off. Use <em>Open in browser</em> on a hosted file for lock-screen controls.</li>
<li><strong>Continue</strong> appears on home and on a module's screen and takes you to the first unfinished <code>listen</code> or unanswered question after your place. Your place is the furthest page you have reached on this device, otherwise the last <code>listen</code> or question answered in the booklet's order. A question you passed over is not offered again, so Continue cannot hold you on it. It works out the rest from the note's records, so it works on a phone after listening on a desktop.</li>
</ul>
<p><strong>Links that hide their host.</strong> An external link whose words are not its address (<code>[Sign in](https://...)</code>) shows the real host after it, outside the link, so the link's words are unchanged.</p>
<p><strong>What the guard covers.</strong> For the text of a booklet, the plugin guarantees that nothing remote is requested before a press, that only an allowed <code>https:</code> host is used, that an address with a user name in it is never used, and that an allow names only the host as written. Frames and raw media tags in a booklet are drawn as links that name the host and are never loaded.</p>
<p><strong>What the guard does not cover.</strong></p>
<ul>
<li>Remote pictures and the links you click. Those are Obsidian's, as in Reading view.</li>
<li>Embedded notes (<code>![[Other note]]</code>) and blocks drawn by other plugins (Dataview, Meta Bind and the like can draw media or frames). Obsidian draws them, outside the plugin.</li>
<li>A redirect. The allow names the host in the address as written. If that server answers with a redirect to another host, your device follows it, and nothing in the plugin can stop that hop. A booklet should write the address that serves the file directly.</li>
<li>A local file that Obsidian reads (its length, for instance) before you press Play.</li>
</ul>
<p><strong>Privacy.</strong> A host you allow can see that your device asked for the file, when, and from what address, which tells it when you play and roughly how far you listen (a player asks for the file in pieces). The plugin sends it nothing else. <code>pos</code> and <code>done</code> are in the note, so they travel with a booklet you share back to a teacher or a clinician; whoever hands out a booklet that holds a <code>listen</code> should say so. The device keeps two small things in the vault's local storage and never in a note: the position of a <code>listen</code> that is playing and not yet written, and the furthest page reached in a booklet.</p>
<p><strong>Known limits.</strong></p>
<ul>
<li>Embedded notes are Obsidian's, as above.</li>
<li>Whether audio keeps playing with the screen locked or the app in the background has not been measured. The lock screen has no Booklet controls.</li>
<li>There are no captions yet: a caption file beside a video is the next format's. A transcript is an ordinary folded callout after the media, and the plugin draws it as it is.</li>
<li>A framed service may refuse to play inside the app; the link beside the player opens it in the browser.</li>
<li>Two devices writing the same <code>pos</code> at the same moment have not been tried with any sync; use one device at a time.</li>
<li>Android has not been tried.</li>
</ul>
<h2 id="the-copy-callout">The <code>copy</code> callout</h2>
<p>Format 0.19 adds one callout, <code>copy</code>: <code>> [!copy|id of:a,b,c] Button label</code>. The plugin draws it as one button, an Obsidian button like the others, with the label as its words. Pressing it puts one piece of text on your clipboard: the lines written under the callout (the author's lead-in, for example "Please mark my answer"), then each word of <code>of:</code> in the order written. A word is one of three things:</p>
<ul>
<li>a question of the same activity: <code>Question:</code> and its title (just <code>Question:</code> when it has none; the help lines under the title are not copied), then <code>My answer:</code> and what you typed or picked (a choice or a multi gives the option's words; a <code>lines</code> answer, every line as you typed it; an answer in the note that is not of its kind's shape, as the JSON the note holds);</li>
<li><code>id/solution</code> or <code>id/hint</code>: <code>Model answer:</code> or <code>Hint:</code> and the text of the folded solution or hint straight after question <code>id</code> (just the label when that callout has no words);</li>
<li>the id of a fenced block marked <code>^id</code>: the id, a colon and the block's text.</li>
</ul>
<p>When you have typed or picked nothing, the question and the model answer are still copied and the answer reads <code>My answer: (no answer typed)</code>. Words you typed that are not written yet are copied too. A matrix, a widget, a <code>listen</code> and a question asked for each pick cannot be copied, and a word that names nothing, or a question of another activity, is left out and listed among the note's problems; a copy line with nothing left is not drawn, and neither is one whose <code>of:</code> has an empty place (<code>a,,b</code>, <code>a,b,</code>), which is listed too. Pieces are one blank line apart, and the text has no blank line or space at its start or end.</p>
<p>It copies and does nothing else: it writes nothing to the note or its records, and sends nothing anywhere. After a press a line beside the button says "Copied" (a status region, so a screen reader announces it) and empties after a few seconds; if a word was left out it says which. If the clipboard refuses, the line says so and the text is shown in a read-only field, selected, to copy by hand. The text is put on the clipboard inside the press, which is what the phone app needs. Pressed on a desktop, this was tried in the real app on 1.14.5 and 1.11.7 (check 60 of <code>e2e-tests/</code>); it has not been tried on an iPhone by this release's builder, and Android has not been tried.</p>
<h2 id="rows-that-open-and-data-files">Rows that open and data files</h2>
<p>Format 0.19 lets a data row hold other rows and lets a data block live in a file beside the note. This plugin reads both (dashboards, slice 1).</p>
<p><strong>Rows that open</strong> (SPEC section 6, "Rows that open"). A <code>table</code>, a <code>list</code> or a <code>cards</code> query over a data block nests its rows in two ways that make one tree: a flat row whose <code>parent:</code> field (the default is <code>parent</code>) holds another row's <code>id</code>, and a data row whose field holds a list of rows, or one row. A row that has children, or fields its line does not show, is a native <code>details</code> with a <code>summary</code> for its line: a keyboard opens it with Enter or Space and a screen reader hears it as a disclosure, named by the words on its line (a row with none is named "Row"). It is shut until you open it, and what it opens to is drawn then, not before: first its other fields, each under its label as plain text with its line breaks kept (every field that plays no role, is not on the line, is not <code>id</code>, the <code>parent</code> field or the <code>group</code> field, and does not hold child rows, decided row by row at every depth), then its children, in groups headed by the field they came from (children found by <code>parent:</code> have no heading).</p>
<ul>
<li><strong>Table.</strong> It stays a table: one body, every row a row of the same columns under real column headings, a child one step in. The disclosure is in the first column that is not the badge. The rows a row opens to are rows after it, taken away again when it shuts.</li>
<li><strong>List.</strong> A row that opens is a <code>details</code> in the list; its open part and its child list are inside it. At most 200 levels are drawn inside one another; a chain deeper than that continues in the 200th level's own list.</li>
<li><strong>Cards.</strong> A card that opens is a card that folds: its title, badges, value, note and the lines <code>fields:</code> names stay in sight when it is shut, as on a card that does not open, and its open part and child cards are inside.</li>
<li><strong>What is kept.</strong> The rows you open are kept for as long as the pane is open, on this device only, and never written to the note or saved in the pane's state. They survive a sort, a search and every redraw, including the redraw when a data file is read again. A search keeps each match with its parents and opens those while it is on; that opening is not remembered. <em>Open all</em> opens every row that opens (not only the ones a search keeps) and <em>Close all</em> shuts them. <code>limit:</code> counts top rows, and <code>group:</code> is not drawn on rows that nest.</li>
<li><strong>Print.</strong> Printing prints everything: before a print every closed row is opened and its open part drawn at once, and after it exactly those rows fold back. The rows you opened are not touched, and a print that never sends <code>afterprint</code> does not swallow your next click.</li>
</ul>
<p><strong>Data files</strong> (SPEC section 7, "Rows from a file"). A data block whose fence line names a file, <code>booklet data dashboard.json inbox.requests</code>, takes its rows from that JSON file at that key path (dots, keys of letters, digits, <code>_</code> and <code>-</code>; none means the whole file), and the fence is empty or holds only <code>fields</code>. The file name is a plain relative name of a <code>.json</code> file in the note's own folder or below it (letters, digits, <code>_</code>, <code>.</code>, <code>/</code> and <code>-</code>, at most 200 characters, no part that is empty or starts with a dot, so no <code>..</code>, no leading <code>/</code>, no <code>//</code>, no backslash and no scheme). Any other name, and any other key path, is listed among the note's problems and is never read. The file is read through the vault, and only that: nothing leaves your device. It is read once for each read, however many blocks name it; each block takes its own key path from it and keeps its own fence <code>fields</code> over the file's. A file over 5 MB is not read. A UTF-8 byte order mark at the start of the file is ignored. A key path walks object members, never a list.</p>
<p>When the file cannot be read the first time, the query draws one quiet box in the view's place that says why ("Data not shown: the file dashboard.json was not found.", "is not JSON", "has nothing at “inbox.requests”", "holds no list of rows", "is too big (the limit is 5 MB)", "could not be read", or a name "is not a name this plugin reads"), and no control. A fence that holds <code>rows</code> as well as naming a file is listed among the note's problems, and the file's rows are the ones drawn. The file is read again when the vault reports a change to a <code>.json</code> file and when the pane comes back into view (the window or the app shown again, the pane made active): only a file whose size or time changed is read again, a change that lands while the file is being read for the first time is picked up when that read ends, and a read that finds the same text as before draws nothing; otherwise the rows are drawn over what you were looking at, with your open rows, the page's scroll, a wide table's scroll and the focused control kept. A file that cannot be read again (half written, not JSON, gone) leaves the rows already on screen as they are and is tried again at the next look; the quiet box is for a file that has never been read well. Obsidian does not list a <code>.json</code> file unless it is set to detect all file extensions, so a change made outside Obsidian is found when the pane comes back into view, not the moment it is made.</p>
<p><strong>A question asked for each row</strong> (SPEC section 5, "A question asked for each row"). A question of any kind with <code>each:</code> naming a <code>booklet data</code> block (in the activity's module, or in the booklet's shared data) is asked of the rows of that block, at any depth. It is asked only of the rows that carry a field with the question's id (of every row that has an <code>id</code> when none does), and a row with no <code>id</code> has nothing to name it by, so it is asked nothing. It is drawn in the row's open part under the question's own words, or, when a query's <code>fields:</code> names the question's id, in that column (a table column headed by the question's title, a list row's extra, a card line), and only there. An answer is the same pair as for a question asked for each pick, <code>{"pick": <the row's id>, "a": <the answer>}</code>. When the field holds a value, the question starts from it (a choice from the option whose words match, ignoring case and the spaces at either end) and an answer replaces it, so the pair also says <code>"was": <the value when you began></code>; when the field holds a list, or the row has no such field, the question starts empty, the list's items, then the answers kept so far (newest first, each as its time and its words), are shown as history under it, and an answer is added with no <code>was</code>. Typing in it is the activity's draft, as everywhere. <strong>Finalize</strong> in a row (under the row's questions in its open part, and only in a row that has questions asked of it there) keeps that row's draft pairs of all the questions as one new entry, and leaves other rows' answers in the draft. A question placed in a column has no Finalize of its own: the activity's Finalize keeps its draft with the rest.</p>
<p><strong>Editing a field in place</strong> (SPEC section 5, "Editing a field in place"). A field whose label in <code>fields</code> is an object with <code>edit</code> (<code>text</code>, <code>long</code>, <code>number</code>, <code>date</code> or <code>choice</code> with <code>options</code>) is drawn as its value, on a button. Press it (or Enter or Space on it) and the value is a box of that kind: a field, a text area, a number field, a date field or a menu of the options. Enter, or leaving the box, keeps <strong>one act</strong>, <code>{"pick": <the row's id>, "a": <the new value>, "was": <the value when you began>}</code>, under the field's name, in a <strong>new kept entry</strong> of the activity that holds the query; Esc keeps nothing, and so does leaving a box with the words it began with. There is no Finalize for it. A number is kept as a number, and an emptied number box is kept as an emptied value (<code>null</code>). Text in a number box that is not a number (<code>3e</code>, which the browser reports as unreadable) is never kept as an emptied value: the box stays open with the text in it and says in words that it is not a number, and Esc leaves the value as it was. If the screen is left while the text is still unreadable, nothing is kept and a notice says so. In a <code>long</code> box Enter keeps and Shift+Enter makes a new line. The activity must <code>repeat</code> and have an id, and must not be <code>repeat daily</code>: in any other the field is drawn as its value and nothing more (a daily activity keeps one entry a day, made by its own Finalize, so an act, which is an entry of its own, has no place in it; there a question asked for each row is a draft like any other, kept by the activity's Finalize, with no <code>was</code> and no row Finalize). A field is also its value only when it has no <code>edit</code> mark, when it is a <code>choice</code> with no options, when the row has no <code>id</code>, when an earlier row has the same <code>id</code>, and when the row's value is a list or the row does not carry the field: a list is drawn as the other views draw one (its words, several pills in a table) and is for appending, not for editing in place. The records writer adds the one entry (after the activity's last entry, or as a new <code>booklet entries</code> block) and changes no other byte of the note; an entry at the same second as another is moved to the next free one. The entry is written first; then, if the rows come from a data file in the vault, the plugin writes the edit into that file too (next paragraph). The Kept entries list under the activity's Finalize shows such an act as the field's name and, for each pair, the row's id, the new value and what it was ("title: p-orchard: Old orchard (was Orchard)"), not as a question this view does not show.</p>
<p><strong>The plugin writes the data file for an in-place edit.</strong> A data file in the vault is the plugin's to host (the Booklet repository's <code>docs/hosting.md</code>, "Applying a reader's acts"), so after an edit is kept (the entry in the note comes first, and is never lost) the plugin applies it once to the file the data block names. At that moment it reads the file again and applies the edit to that fresh text through the app's own read-and-write step for a file (<code>adapter.process</code>, available since Obsidian 1.7.2, so in every version this plugin supports). That step reads the file, lets the plugin change the text and saves it, as one step inside Obsidian, so a change made outside a moment ago is seen and not overwritten. A change made by another program in that very same moment, between the read and the save, is not guarded against: Obsidian offers no lock on a file for other programs. It sets the field of the row whose <code>id</code> is the pick, <strong>only if the data there still holds <code>was</code></strong> (compared as the display states compare, a choice ignoring case and spaces at the ends), and it replaces <strong>only that value's own text</strong>: a text, long, date or choice value as a JSON string, a number as a JSON number, an emptied number as <code>null</code>. Every other byte of the file stays as it was (spacing, key order, other rows, line endings, a leading byte order mark). A nested row is found by its <code>id</code> at any depth. The field named <code>id</code> is never written, even if the data marks it <code>edit</code>: it is what every act names the row by, and changing it would orphan them. If the data holds something else now, the row is gone or its <code>id</code> is on more than one row (the file is then left alone), the row does not carry the field, or the file is not valid JSON, nothing is written; the entry stays kept, a notice says so in words ("Booklet: change kept, but not written to orchard-dashboard.json, because the data holds a different value now."); if the file already holds the value, the notice says that and not that it was written, and the cell shows the value now with "You sent: …". The plugin never writes the file for: a row's Finalize or any answer to a question asked for each row (those have no <code>was</code>), a file outside the note's own folder or below (it is never read), or a file the vault cannot write (read-only, missing): those are kept as entries and the cell says " · sent, waiting". Rows written in the note itself (an inline <code>booklet data</code> block) are changed by the same function, on the text between the block's fences: the note changes there and nowhere else, and the notice says "written to the data block". A field marked <code>edit: text</code> (or <code>long</code>, <code>date</code>, <code>choice</code>) is written as a JSON string, so one that held a number or <code>true</code> or <code>false</code> holds a string after an edit. An edit is applied once, when it is kept; nothing is applied again when the note is opened later, and there is no setting to turn it off. A file written this way is drawn again from the text just written, in every block that names it.</p>
<p>After the data is read again the field shows what SPEC section 5 says: the value now, with nothing marked, when it equals what you sent; what you sent, with the words " · sent, waiting" after it, when it still equals <code>was</code>; the value now, with "You sent: …" beneath, when it is anything else (a refused or out-of-date act looks like this). The marks are words, not colour: the value's button is described by them (<code>aria-describedby</code>), and a kept change is announced by a notice ("Booklet: change kept and written to orchard-dashboard.json.", or "Booklet: change kept." where there is nothing to write). Words typed in a box and not yet kept are the pane's until the box is left, and survive the rows being drawn again (a data file read again): nothing you typed is lost. A write that is refused puts the box back with your words in it. A row taken out of the data keeps its pairs in the note, and they are not drawn; if a box was open on one of its fields at that moment, the reader's words are kept as an act at once, as leaving the box would have kept them.</p>
<p><strong>The <code>readonly</code> key</strong> (SPEC section 6) belongs to the query: bare, it locks every field and question the view draws; <code>readonly: stage, notes</code> locks those. A locked field is its value, with no button; a locked question is drawn as its answer in plain words, with no control, and is not drawn at all when it has no answer (its title and an empty control are not repeated on every card). Two views of the same rows can differ. A bare <code>readonly</code> locks adding and removing as well (see <em>Adding and removing rows</em>). Whether a field is locked or not, a data block, in the note or in a file, is changed only by what a reader kept: an in-place edit, an added row or a removed row, as described here.</p>
<p><strong>Print and a screen reader.</strong> A box is a real labelled control: a field, text area or menu named by the field's label and the row's label ("Stage, Orchard"), and a button named "Edit Stage, Orchard: growing"; a question is named by its title and the row's. They are reached by Tab and have the theme's focus ring. Printing shows values, not boxes: the sheet hides the controls and prints each question's words and each field's value (a box left open prints as its words in plain text), and the print rule of <em>Rows that open</em> (every closed row is opened and folds back) still holds.</p>
<p><strong>Opening a row whose first column is editable.</strong> Because pressing an editable value edits it, a row whose first column holds an editable field (the usual title) opens from its marker (the triangle), from the empty end of its line, or from the keyboard with Enter or Space on the line itself (Tab reaches the line before it reaches the value). On a phone, touch the triangle at the left of the line or the blank space at the right end of it; touching the value edits it.</p>
<p><strong>Adding and removing rows</strong> (SPEC section 5, "Adding and removing rows"; the contract is <code>design/dashboards-as-booklets/rows-contract.md</code> in the booklet-hub). A query that says <code>add</code> (a bare line) draws an <strong>Add a row</strong> button after its rows; one that says <code>delete</code> draws a <strong>Remove</strong> button on each row (a column of its own in a table). A row with one field that takes children also draws <strong>Add under this row</strong> in its open part: of its fields that are a list of rows (a list whose items are all objects), the one that has rows, if exactly one does (an empty list beside it is ignored, so <code>"jobs": [{…}]</code> with <code>"tags": []</code> takes the child under jobs); else, when none has rows, the one empty list (<code>"jobs": []</code> takes its first child). Both are drawn only in an activity that repeats and is not daily, over a table, a list or cards, and a bare <code>readonly</code> leaves them out; a query that says neither draws neither. Neither is drawn, and no field is edited in place, over a data block the plugin cannot write: one inside a callout, or one in the note that is not in plain fences. The plugin does not hide them with a setting: they are there because the query asked.</p>
<ul>
<li><strong>The form.</strong> Add opens a small form with a box for each field the data marks <code>edit</code> and the query does not lock, in the order the data block declares its fields (whatever the query shows), each of its kind (a text field, a text area, a number field, a date field, a menu of the options). Enter in a one-line field, or Add, keeps the row; Cancel, or Esc anywhere in the form, closes it and keeps nothing; the words typed survive the rows being drawn again, and if the entry cannot be kept they are given back. A number box with letters in it keeps nothing and says so.</li>
<li><strong>The id.</strong> The new row's <code>id</code> comes from <code>idFormat</code> on the data block (next to <code>fields</code>; the block wins over a file it names), a string of text with parts in braces: <code>{n}</code> (the highest number found in an id that fits the format, plus one; <code>{n:3}</code> pads to three digits), <code>{uuid}</code>, <code>{time}</code> (UTC, <code>20261108104200</code>), <code>{date}</code>, <code>{epoch}</code>, <code>{field:NAME}</code> (the slug of what was typed in that field: accents removed, lower case, anything else a hyphen), <code>{parent}</code> (the id of the row it goes under, empty at the top) and <code>{{</code>/<code>}}</code> for the braces themselves. <code>{time}</code> and <code>{epoch}</code> take the next free second, up to 60, if the id is taken; any other clash refuses and says to add a part that tells rows apart. Refusals say why: an empty box ("Give it a name first."), a box with nothing an id can use ("'日本語' has no letters or digits an id can use. Type an id, or use another part."), a format that makes nothing ("That id format makes an empty id here."). A format that cannot be read draws one quiet line in place of the Add button: two number parts, a number part next to text or digits (<code>{parent}{n}</code>, <code>{n}{time}</code>, <code>plot1{n}</code>; put a constant that is not a digit, such as a hyphen, between them), or a <code>{field:NAME}</code> the form has no box for ("The id format uses Name, which this table does not ask for."). With no <code>idFormat</code> the form asks for an Id, which is trimmed, must be new, on one line, and match the whole of <code>idPattern</code> (a regular expression with the <code>u</code> flag, on the same block) if there is one. A bare <code>{n}</code> writes a number, in the entry's <code>pick</code> and in the data; anything else writes a string. An id is compared as text, so an existing number 5 and a string "5" clash, and a number that was removed can be made again. A box left empty is left out of the row (a form with every box empty still adds a row with only its id); a number box is kept as a number and a choice as its option's words.</li>
<li><strong>Remove asks first.</strong> "Remove this row?" or, for a row with rows under it, "Remove this row and the 3 rows under it?", with Remove and Cancel; Esc cancels, and the question is announced politely. A row with a removal under way offers no second one. A row still waiting to be added offers Remove too: it keeps a removal of the row as the entry made it, nothing is written (the data never held it), the row goes, and its number is free again (this is how to dismiss a row that was added and later taken out of the data by someone else).</li>
<li><strong>An act names its block, and ids are text.</strong> Each pair carries <code>"in": "<the data block's id>"</code> (the id after <code>^</code>), for example <code>{"$add":[{"in":"beds","pick":"j-5","under":"p-1","row":{"title":"Mulch"}}],"ts":"…"}</code>. A row waiting to be added or removed is drawn only in the views of that block, <code>{n}</code> and clashes count only within that block's rows and waiting adds, and only that block's data is changed (its file and key path, or its inline fence), so an activity may hold several data blocks. An entry whose pair has no <code>in</code>, or an <code>in</code> that names no data block of the note, is kept byte for byte as written and is neither drawn, counted nor applied (the spec's linter reports it; an entry written before this was the rule, in a beta build, is one of those, since no old shape is kept before 1.0). Ids are text for every act: an in-place edit's pick, a question asked for each row, and an add's or a removal's <code>pick</code> and <code>under</code> all match a row's <code>id</code> as text, so 5 and "5" are one id; the pick is stored as the id is written.</li>
<li><strong>What is kept, and then written.</strong> Pressing Add or Remove keeps <strong>one entry</strong> (<code>$add</code> or <code>$delete</code>, one pair, like an edit's) in the activity's records, exactly as it is written; then the plugin changes the data <strong>once</strong>: the new row goes at the end of the block's rows, or of the one list of rows under the parent, in the layout of the row before it (its indent, its spacing after a colon, its line ending), with <code>id</code> as its first key; a removed row goes with one comma (the one after it, or the one before it for the last row) and its own line. An add whose id is already there with the same values, and a removal of a row that is already gone, change nothing; an add whose id another row has taken since is refused ("A row with that id exists."). A removal is checked against the row as the reader saw it (every key and value, keys in any order): a row changed since is not removed. Everything else in the file, or in the note outside the block's fences, is the bytes it was. An inline block is changed between its fences only.</li>
<li><strong>While the data has not followed.</strong> A row added shows at the end of its parent's rows with " · sent, waiting"; a row asked to be removed stays with " · removing, waiting"; a removal the data did not follow because the row changed shows "You asked to remove this; it changed since." and offers Remove again. These are words, not colour. A success says nothing beyond the redraw. A refusal keeps the entry and says why in a notice: "That row is gone.", "More than one row has that id.", "Cannot tell where the new row goes under this one.", "Changed since you drew it.", "That file is not valid UTF-8, so it was not changed." (nothing is written into text that holds a byte that was not UTF-8, for an edit as well), that a write would make the file too big for the plugin to read (5 MB), "This data cannot be changed from here.", or that the file or note cannot be written.</li>
<li><strong>Looks.</strong> Both are styled with the repo's tokens. Add and Remove are small in every look; the add form's buttons and the Remove question's answers are the larger ones. In the Paper look those are its pills, the form its card, and the confirm its amber warning; in Obsidian's own look they are Obsidian's buttons and the confirm a darkened red with white words. Print shows none of the controls.</li>
</ul>
<p><strong>Known limits of adding and removing.</strong> An add or a remove the data refused is kept and is not tried again: it shows as waiting (a removal of a row that changed, as "changed since") every time the note is opened, because the plugin applies an act once, when it is kept; Remove on the waiting row dismisses it. A parent with two fields that are lists with rows, or with no list with rows and two or more empty lists, cannot take a child, and a row held as a field's one object (not in a list) cannot be removed. A number outside JavaScript's exact range (beyond 9007199254740991) in a removed row is read as the nearest number the language has before it is compared and recorded: the file's own text of the row goes with the row, but the removal's record of it (<code>was</code>) holds the rounded number, and two rows that differ only past that precision look the same to the check. A row nested thousands of levels deep cannot be compared and is refused ("nested too deeply to change"). On a phone-width screen the Remove column of a table stays at the right edge of what shows while the table scrolls, and the question wraps; a table row that does not open has no open part to move Remove into. The web reader (<code>booklet.html</code>) does not support adding or removing rows.</p>
<p><strong>Known limits of the acts.</strong> An edit is written into the data file after its entry is kept, so for a moment the cell can say " · sent, waiting" before the value lands, and the rows are drawn twice (once when the entry is kept, once when the file is written). A second quick edit of the same field, before the first has landed in the data, is refused as out of date (it began from the old value): it is kept, not written, and the cell shows the value now with "You sent: …". The apply runs inside the note's own queue of writes, so it waits for the writes before it. The file's size and time are noted just after a write; an outside program that wrote in the gap between the write and that note can be missed until the next change. A query over the activity's own kept entries (<code>from:</code> the activity and a question) shows "Nothing kept here yet." for entries made of acts on rows, whose answers are pairs for rows and not an answer for the question; the Kept entries list under Finalize does show them. An answer that starts from a field's value starts only a <code>text</code>, <code>date</code>, <code>number</code>, <code>choice</code> or <code>multi</code> question; a <code>scale</code>, <code>lines</code> or <code>matrix</code> question starts empty although the field holds a value. A data field named <code>ts</code> cannot be edited (an entry's time is called <code>ts</code>; the writer refuses it and a notice says so). Text that cannot be read in a number box is not kept, and the text itself is not kept either: the browser gives the plugin no access to it.</p>
<p><strong>Not drawn.</strong> A <code>listen</code> question is not asked for each row (a line says so). A widget asked for each row is drawn as the question's own widget, one per row.</p>
<h2 id="what-it-does-not-draw-yet">What it does not draw yet</h2>
<p>Each of these is left in the note exactly as written; where the view draws something in its place, it says so:</p>
<ul>
<li>a widget's figure (the body widget's drawing): the widget is drawn without it, and says so if the data has one</li>
<li>widget engines other than <code>grid-select</code> and <code>svg-regions</code></li>
<li>footnote marks in a few rare places: a page-break line inside a <code>$$</code> block is still a page break, so a mark after it is not numbered (the numbers read the block as one) but is drawn on the next page as a mark with no number; a lone <code>$$</code> line with no second <code>$$</code> makes everything after it in that part maths; an unclosed fence runs to the end of its part; and the two private-use characters the plugin swaps marks for (U+E000 and U+E001) in a note's own text can be drawn as a mark when the same paragraph holds a real one</li>
<li>footnotes in print (endnotes), a footnote preview on hover, and the reference renderer's side panel for a source: the plugin opens a source under its paragraph</li>
<li>a link that opens a booklet at a page (page trees have none: a link goes by words)</li>
</ul>
<p><strong>The notice at the top of a note.</strong> A note that holds something this view does not read opens with one notice first among its problems (a closed "N things in this note could not be read" at the top of the view). The note's own marker never makes a notice: a note marked with another 0.x version, earlier or later, that holds nothing the view does not read opens with nothing said. The notice says which version of the plugin is reading and which format it reads ("This is Booklet plugin", the plugin's own version number, "which reads format 0.20."), and then lists each part it does not read, one line each: the line's number in the file (the front matter counts), what is written there (cut at 80 characters) and why. These are the parts it lists: a kind of line that is none of the format's nor Obsidian's own callouts (drawn as "Not yet supported" when it has words after its bar, as an ordinary callout when it has none); a setting a line's kind does not take (<code>mx:5</code> for <code>max:5</code>, <code>open</code> on a <code>scale</code>), which is ignored as the format says; a <code>start:</code> in the front matter that is not <code>home</code>, an <code>icon:</code> that is not a name in Booklet's icon set, a <code>status:</code> that is not <code>off</code> and a <code>tab:</code> that names no tab of its module, each ignored as the format says; a key a query does not have; a <code>media-from</code> value that is not an <code>https:</code> origin (a path, a wildcard, <code>http:</code>), which is ignored while the other origins still count; a <code>left:</code> on a <code>listen</code> that is not whole seconds; a <code>listen</code> in a <code>repeat</code> activity ("kept as written, drawn without a tick"); a second embed under a <code>listen</code> (only the first is followed); a <code>listen</code> whose lines hold no embed and no link; and a <code>```booklet</code> block of a type this view does not read, or one that has no type, which is drawn as "Not yet supported" where it stands in an activity. A theme block (<code>booklet theme</code>), which the format defines, is one of those: themes are not drawn yet. A raw <code><iframe></code>, <code><frame></code>, <code><object></code>, <code><embed></code>, <code><audio></code> or <code><video></code> tag is listed among the note's problems rather than as unread, because a later plugin would not read it either ("drawn as a link in this view; nothing loads"); the format does not allow them. What is listed stays in the file exactly as written, the rest of the note draws, and answers are still taken and written. <code>each:</code> and <code>earlier:</code>, which the plugin draws, are not listed. Any other value the view cannot use on a name it knows (<code>limit: week</code>, <code>earlier:abc</code>) and a line that looks like a Booklet line but cannot be read are not listed there: they are among the other problems, with their line.</p>
<p>When something listed might be read by a later version of the plugin, or the note's marker is later than 0.20 and something is listed, the notice adds how to update: "To update: Settings, Community plugins, Check for updates." A name that is plainly a mistake gets no such line, because an update would not change it. The rule: a setting or a query key is a mistake when the format has that name somewhere (<code>hidden</code> on a page line) or it is one or two edits from one (<code>mx</code> for <code>max</code>, <code>lmit</code> for <code>limit</code>; one edit is allowed when the shorter of the two names, the unknown one and the format's, has four letters or fewer, else two); an unknown kind of line or block is never taken for a mistake. A <code>start:</code>, <code>status:</code> or <code>tab:</code> value is always a mistake, and an <code>icon:</code> is one when it is one or two edits from a name in the icon set (the set may grow, so any other name may be a later one). A made-up note for each case, with the notice's words, is in <code>test/notice.test.mjs</code>.</p>
<p>Also not done yet: editing a kept entry (deleting one is done), data sections, and languages other than English for the view's own words. A link to a block of the same note (<code>#^id</code>) does nothing.</p>
<p>A note that Obsidian opens before its metadata cache holds its front matter (one created and opened in one go, or re-indexed after a sync) shows as plain Markdown until <em>Open as booklet</em> is pressed: the plugin knows a note is a booklet only from that cache, and it does not switch a pane on every change of it, since that would flip an ordinary note while someone types a <code>booklet:</code> key into it. This is how 0.6.0 behaved, and it is unchanged.</p>
<h2 id="what-it-writes-and-what-it-never-does">What it writes, and what it never does</h2>
<ul>
<li><strong>It sends nothing of its own, and has no telemetry or account.</strong> It makes no network request until you allow a host and press Play on its media; then your device asks that host for the file, and the host sees that you did (see <em>Audio, video and <code>listen</code></em>). Its one group of settings is the list of hosts media may load from. It uses only Obsidian's API, no Node or Electron API. The <code>copy</code> callout's button puts text on your clipboard when you press it and sends it nowhere. Obsidian may load remote images or other content linked by the note, as it normally does.</li>
<li><strong>It changes only what you changed.</strong> An answer, a draft or an entry is written into the records region, and inside it only into the one block that holds it, and inside that block only the one answer or entry. Every other byte of the note, every other record block, and every answer or entry you did not touch stay exactly as they were written: no respacing, no re-sorting, no trimming. A note with no records gets a records region added after its text, separated from it by one blank line.</li>
<li>If the note is also open as Markdown, the change goes into that editor, so nothing typed there is lost; otherwise it is written with Obsidian's <code>Vault.process</code>. While the note is open as Markdown, the view reads the note from that editor too, which holds the newest text: the file as last saved can be older, and taking it would take back an answer just given.</li>
<li><strong>An answer of the wrong shape is kept as written</strong> (SPEC section 8). An answer whose shape is not its question's kind, such as pairs under a plain <code>multi</code>, a list under a <code>scale</code> or a number under a <code>text</code>, typed by hand or written by another renderer, is never rewritten: its question is drawn read-only with a notice saying what the note holds, no press or Finalize changes it, and a kept entry shows it as written. It can be changed in Markdown. What each kind keeps is under <em>Choices made</em> below.</li>
<li><strong>A field you are in is never overwritten.</strong> This holds for a widget's fields for words of your own too. When the note changes while you are typing in the view, or have a field selected (words typed in its Markdown pane, a change from Sync or from another plugin), the field keeps what you typed, the caret and the selection. It shows what the note holds once you leave it, and if the change was to the same question, what you typed is written over it, as the newer change. If the change was to the note's text, the view is drawn again once you leave the field and no press is under way. Words typed in <em>Add your own</em> and not added yet are kept too, and added when you leave the field.</li>
<li><strong>Words typed and not added yet survive closing and reloading.</strong> Before Obsidian quits or its window closes, the view writes what was typed and adds the words typed in <em>Add your own</em> and a widget's fields, as leaving the field would. A reload (<em>Reload app without saving</em>, or Obsidian reloading itself) gives a plugin no time to write, so those words are also kept on the device as they are typed, in the vault's own local storage (never in the note, and never sent anywhere), and added when the note is next drawn; then they are taken off the device. Until then they are only on that device.</li>
<li><strong>Typed words reach the note within 0.7 seconds.</strong> Words typed in a text field are written 0.7 seconds after the first key not yet written, however long the typing goes on. Closing the pane, opening another note in it, closing the window and quitting write at once. A crash or a forced stop loses at most the last 0.7 seconds; so does the <em>Reload app</em> command.</li>
<li><strong>Line endings and the byte-order mark.</strong> With only the Booklet view open, a note's line endings (LF or CRLF), its byte-order mark and its final newline are kept. If the note is also open in a Markdown pane, answers go through that pane's editor, and Obsidian's editor saves the note with LF endings and without a byte-order mark, as it does for any note it edits.</li>
<li><strong>A record it cannot read is kept and never written into.</strong> A record that is not valid JSON, has no list of entries, or is unclosed is listed at the top of the view with its line and is kept exactly as written. A record written twice is read as one list, later values winning, and both copies stay in the file.</li>
<li><strong>The note can change under the view.</strong> When the note changes on disk while it is open (Sync, another editor, another plugin), the view follows and keeps what is being typed. Renaming or moving the note keeps the view on it; deleting it closes the view; turning the plugin off leaves the note openable as Markdown.</li>
<li><strong>A large note stays quick.</strong> A daily tracker of 3,000 kept days (414 KB) opens its home screen in about 0.1 seconds and draws a table of all the days in about half a second; a press is in the note in about 0.1 seconds.</li>
</ul>
<h2 id="what-stays-put">What stays put</h2>
<p>These names are kept, so that a hotkey, a saved workspace or a style snippet written today goes on working. This is the plugin's promise from 1.0.0: its own surface, which is the names in this list, and never losing what a reader wrote. It is not a promise that the format stays as it is: the format is still a 0.x version, and the plugin reads the one version named at the top and opens a note marked with another as it is. Anything not named in this list can change from one release to the next.</p>
<ul>
<li><strong>The plugin's id is <code>booklet</code>.</strong> It names the folder <code>.obsidian/plugins/booklet/</code> and is the prefix of the command ids.</li>
<li><strong>The command id is <code>toggle-view</code></strong> ("Toggle between the note view and Markdown"; Obsidian shows its full id as <code>booklet:toggle-view</code>). A hotkey bound to it keeps working. New commands may be added; none is renamed or removed.</li>
<li><strong>The view type is <code>booklet</code>.</strong> Obsidian's saved layout holds it, so a workspace with a booklet open restores it.</li>
<li><strong>The saved pane is read tolerantly.</strong> Obsidian saves, with the pane, the note's path and the place the reader was at: <code>{ version, screen, tab, page, part }</code>, with <code>version</code> 2 (<code>page</code> is a named page's id, left out for a place with no named page). A field this release does not know is ignored; a missing or mistyped field, a state of any other version (an older release's included), or a state that is not an object at all (nothing, <code>null</code>, a string, a number, a list) opens the home screen, or the nearest place the note has. It is never an error and never loses the note. The place is saved by Obsidian and never written to a note.</li>
<li><strong>A setting changes how the view looks, or which hosts media may load from, and never what is written to a note.</strong></li>
<li><strong>The plugin's settings keys are <code>allowedMediaHosts</code>, <code>noteAllows</code> and <code>look</code> (<code>paper-light</code>, <code>paper-dark</code> or <code>obsidian</code>; with no <code>look</code>, the plugin uses Paper and petrol in the variant that matches Obsidian's light or dark).</strong> They are kept in the plugin's own <code>data.json</code> (<code>.obsidian/plugins/booklet/data.json</code>), which has this shape, and Obsidian Sync may or may not carry it to another device (unmeasured): <code>{ "allowedMediaHosts": ["youtube.com"], "noteAllows": { "Courses/Tea.booklet.md": ["media.example.org"] } }</code>. Each entry is a host as written (<code>youtube.com</code>), lower-case, with a port only when it is not the usual one (<code>host:8443</code>), meaning <code>https://*.youtube.com/*</code>.</li>
<li><strong>The device keeps four keys in the vault's local storage (Obsidian's own store for the vault, never synced):</strong> <code>booklet-listen:<path></code> holds the position of a <code>listen</code> that is playing and has not been written to the note yet; <code>booklet-place:<path></code> holds the furthest page reached in that booklet; <code>booklet-held:<path></code> holds the words typed in a field and not yet added, until they are added; and <code>booklet-steps:<path></code> holds whether the steps of a note's parts show one at a time. None is ever in a note, and none is sent anywhere.</li>
<li><strong>Two custom CSS properties are stable</strong>, by name and by meaning (their default values may change with the theme): <code>--booklet-ch-bar</code> (the colour of a bar in a chart, the theme's accent by default) and <code>--booklet-mx-text</code> (the width of the words column of a matrix, <code>12rem</code> by default). Five more are listed under "Styling" below. The plugin's other <code>--booklet-*</code> properties are internal and may change from one release to the next, as its class names may (next).</li>
<li><strong>Class names are not an API.</strong> The <code>booklet-…</code> classes may change from one release to the next; a snippet that styles them does so knowing that.</li>
</ul>
<h2 id="styling">Styling</h2>
<p>The view has three looks, set in the plugin's settings ("Booklet look") and, for one view at a time, from the view's own menu ("Use Obsidian's look in this view" and "Use the Booklet look in this view"):</p>
<ul>
<li><strong>Paper and petrol, light</strong> and <strong>Paper and petrol, dark.</strong> The plugin's own colours, fonts (Lexend for headings, Atkinson Hyperlegible for text, both carried inside the plugin; nothing is fetched), corners, shadows and pictures. They are on by default, and until the reader chooses one, the plugin uses the variant that matches Obsidian's light or dark. They style the Booklet view only, never anything else in Obsidian. Their animations run only when the device has not asked for reduced motion.</li>
<li><strong>Obsidian.</strong> The look the plugin had before 1.1.0: it follows the reader's Obsidian theme, light or dark, because it is drawn from Obsidian's own CSS variables.</li>
</ul>
<p>In every look, a question is a sheet, and a reading block (a callout) is tinted with its own colour and edged in it. The contrast of the Paper looks is measured in <code>docs/themes-contrast.md</code>.</p>
<p>Five custom properties are stable, by name and by meaning, with the Obsidian variable each one defaults to. They are set once, on <code>.booklet-view</code>, and a CSS snippet can change any of them. They join the two named in "What stays put".</p>
<ul>
<li><code>--booklet-card-bg</code>: the fill of a sheet, <code>var(--background-secondary)</code>.</li>
<li><code>--booklet-card-border</code>: the hairline round a sheet, <code>var(--background-modifier-border)</code>.</li>
<li><code>--booklet-card-radius</code>: the corner radius of a sheet and a block, <code>var(--radius-l)</code>.</li>
<li><code>--booklet-card-pad</code>: the room inside a sheet, <code>var(--size-4-4)</code>.</li>
<li><code>--booklet-block-edge</code>: the width of the coloured edge of a reading block and of an answered sheet, <code>var(--size-2-3)</code>.</li>
</ul>
<p>Class names are not an API: a snippet that styles the <code>booklet-…</code> classes does so knowing that they may change.</p>
<h2 id="compared-with-reading-view">Compared with Reading view</h2>
<p><strong>What the plugin does and does not do.</strong> A note drawn in the view can do what any Obsidian note can (links, embeds, images, blocks drawn by community plugins). The plugin sets no limit on what a note may show and filters nothing out, except where the list below says it changes something.</p>
<p><strong>What the view draws or changes itself, instead of Obsidian, and why:</strong></p>
<ul>
<li>Footnote marks: numbered buttons that open a source under the paragraph. Obsidian renders the note one piece at a time, so its own footnotes cannot be numbered across the screen; the view numbers the marks by first use and shows a mark with no note as <code>[?id]</code>.</li>
<li>Heading links and links to a module: the view moves between its own screens, so it opens the place itself. A heading link whose words no place holds is drawn as plain text, and a link to a module the booklet does not hold as a "not in this booklet" chip.</li>
<li>Definition lines (<code>[name]: address</code>): they are taken out of the prose and given to the module they stand in, or to the data section they stand under, so they are never drawn as text. A line inside a code block or a comment is not a definition and stays as written.</li>
<li>Questions and the controls: the view writes the answers into the note's records.</li>
<li>Task checkboxes in prose: switched off, so a press never edits the design part of the note.</li>
<li>Remote audio and video: not drawn as a player. A slot names the host and waits for the allow and a press, where Reading view would start fetching on its own. A link to a video or audio service the plugin knows becomes a Play button once its host is allowed.</li>
<li>Frames and raw media tags (<code><iframe></code>, <code><audio></code>, <code><video></code> and the like): drawn as a link that names the host and never loaded. Reading view draws them.</li>
<li>Links whose words are not their address: the real host is shown after the link, which Reading view does not do.</li>
</ul>
<p><strong>What works as in Reading view:</strong></p>
<ul>
<li>Embeds of other notes (<code>![[Other note]]</code>, and of one heading of it).</li>
<li>Mermaid diagrams, after Obsidian's one-time Allow for the vault.</li>
<li>Audio and video in your vault (<code>![[a.mp3]]</code>, <code></code>, <code>![alt][id]</code>), in Obsidian's own player, from a note in the vault root or in a folder.</li>
<li>Maths, tables, code blocks and callouts.</li>
<li>Images, written inline or as definition lines (<code>![Alt][name]</code>, the definition in the module or in a data section): a name or a path written as Obsidian reads it resolves by Obsidian's link rules (a bare <code>pic.png</code> finds <code>attachments/pic.png</code>, and a name shared by two files gives the file Reading view gives), tested from a note in the vault root. Each piece of prose is drawn with the definitions of its own module first, then those of the module's data section, then the shared data section's; never another module's.</li>
<li>Links to a note, <code>[[Other note]]</code>, with or without <code>#Heading</code> or <code>#^block</code>: a click opens the note, Ctrl or Cmd in a new pane, and the pointer over one shows the hover preview, as the reader's Page preview settings say.</li>
<li>Selecting and copying text.</li>
</ul>
<p><strong>Other community plugins.</strong> Dataview (inline queries, <code>TABLE</code>, <code>LIST</code>), Tasks (query blocks), Admonition (<code>ad-</code> blocks, folds) and Meta Bind (inline fields, toggles, buttons that write to the note's front matter) were tried, and their blocks draw in the view as they do in Reading view. Tasks' enriched task lists written in prose stay plain checkboxes: the view draws pieces of the note, not the file, so a plugin that asks for the note's line numbers gets none. A Tasks checkbox inside a Tasks query result stays live, as in any note, and edits the line it belongs to, which can be in the booklet's design part. Other plugins were not tried.</p>
<p><strong>Known limits:</strong></p>
<ul>
<li><code>#tags</code> in prose are plain text, not tag links.</li>
<li>Find in page (Ctrl or Cmd+F) is not available in the view; use the Markdown view (the command <em>Toggle between the note view and Markdown</em>).</li>
<li>Export to PDF is not offered for the view. The print rules (every closed row opened, values in place of boxes) run on the window's print events and are tested by sending those events and by reading the print style sheet; a printed page has not been looked at on paper or in a PDF.</li>
<li>An embedded note has no icon that opens it.</li>
<li>A definition whose address is on the line after <code>[name]:</code>, or one indented four spaces under a list item, is not taken out of the prose; it works only for the piece of prose it stands in. Write it on one line, not indented.</li>
<li>After pressing Allow for Mermaid, the diagram appears when the note is drawn again, for example when it is opened again.</li>
<li>An image whose file is missing is a broken image with its alt text, where Reading view says the file could not be found.</li>
<li>Media: embedded notes are Obsidian's, background audio is unmeasured, and there are no captions yet. The full list is under <em>Audio, video and <code>listen</code></em>.</li>
</ul>
<h2 id="which-obsidian">Which Obsidian</h2>
<p>Booklet needs Obsidian 1.11.7 or later (<code>minAppVersion</code>, since 0.13.0; 0.12.0 declared 1.8.10). 1.11.7 is the oldest release on which the real-Obsidian suite (<code>e2e-tests/plugin-check.js</code>, 59 checks) passed completely; the measurement, made on the 0.12.0 build, is in <code>docs/floor-0130.md</code>. Nothing below 1.11.7 is promised.</p>
<p><strong>What was run.</strong> The suite passed completely on 1.11.7 and on 1.14.5 in the floor measurement above. For the 1.0.0 pass (<code>docs/accessibility-100.md</code>) it was run in full on both: 58 of 59 passed on each. The one failure, check 27 (a wiki of 13 pages), still expected the closed "Pages in this tab" disclosure that the Pages button replaced. Its two expectations were then updated.</p>
<p>After the 1.0.0 fixes (lanes E1 and E2, and the <code>e2e-tests/</code> updates of lane F) the whole suite was run again on 2026-11-03. On 1.14.5 it passes 59 of 59. On 1.11.7 it passes 58 of 59: check 10 (no uncaught page error) failed on the two <code>illegal access</code> errors that closing a pop-out window throws in check 40, the intermittent error described below, which is not shown to be the plugin's; check 40 alone then passed three times of three on 1.11.7. Two checks needed their expectations changed first, both in <code>e2e-tests/</code>: check 57 wrote its hostile inputs at <code>http://127.0.0.1:<port></code>, which is local since ruling C-079 and so no longer held back (the same spellings at a made-up host are still tested), and check 20 (a link from an ordinary note) had a race in the harness, opening a note before the vault had noticed it, which failed 2 of the 5 runs seen before the harness waited for the file and 0 of 6 after.</p>
<p><strong>Older releases, on the 0.12.0 build.</strong> On 1.8.10 the suite passes 58 of 59 (closing a pop-out window sometimes throws an uncaught <code>illegal access</code> there, cause not found). The one failure is the phone layout: the Now playing bar covers Obsidian's bottom bar, because Obsidian before 1.11.7 has no <code>--navbar-height</code> for the bar to stand above (1.11.4 and 1.11.5 have it but lack the floating-bar class and offset), and the settings page's write-in was 40 px high there (a rule now makes it 44 px). 1.10.6, 1.11.4 and 1.11.5 were run in part, and the releases between 1.8.10 and 1.11.7 that are not named here were not run. Earlier releases were run and fail: on 1.5.8 and 1.7.7 an image written as a definition line (<code>![Alt][name]</code>) is not given an address Obsidian can load (and 17 of Booklet's icon names draw nothing on 1.5.8), and on 1.8.3 and 1.8.7 the check that edits a note in a Markdown pane found an ordered list lower in the note renumbered by Obsidian's editor (a matrix's <code>0.</code> <code>1.</code> <code>2.</code> became <code>4.</code> <code>5.</code> <code>6.</code>). The releases run were 1.5.8, 1.7.7, 1.8.3, 1.8.7, 1.8.10, 1.10.6, 1.11.4, 1.11.5, 1.11.7 and 1.14.5; 1.5.3 was downloaded and not run, and 1.8.4 to 1.8.6, 1.8.8 and 1.8.9 were not run. The plugin uses <code>app.loadLocalStorage</code>, which Obsidian has had since 1.8.7.</p>
<p><strong>Not tested.</strong></p>
<ul>
<li><strong>Android.</strong> It has not been tried.</li>
<li><strong>A real phone.</strong> The suite runs in desktop Obsidian in a virtual display, with Obsidian's own phone emulation for the phone layout. The author has tried every release by hand on an iPhone 17, which is not a measured pass: no layout figures were taken on the phone itself.</li>
<li><strong>Speech.</strong> No screen reader has been listened to by the test harness; see <em>Accessibility</em> below.</li>
<li><strong>Obsidian releases other than 1.11.7 and 1.14.5.</strong> Those two are the releases the 1.0.0 pass ran on.</li>
<li><strong>Themes other than Obsidian's default light and dark.</strong> In the Obsidian look a community theme can change every colour figure in <em>Accessibility</em>. The two Paper looks have their own colours, measured in <code>docs/themes-contrast.md</code>; that page covers the Paper looks only, and the figures in <em>Accessibility</em> were measured on the Obsidian look before the Paper looks existed.
It is not desktop-only: every release has been tried by the author on an iPhone 17, and every build is checked under Obsidian's own mobile emulation and at a phone's width. <strong>Android has not been tried.</strong></li>
</ul>
<h2 id="accessibility">Accessibility</h2>
<p>The plugin draws its screens with Obsidian's own buttons, fields, lists and theme, and for 1.0.0 they were measured. This section says what was measured and what was not; the full report, with every figure, is <code>docs/accessibility-100.md</code>.</p>
<p><strong>What was measured.</strong> On 2026-11-03, in real Obsidian 1.14.5 and 1.11.7 (the desktop app under a virtual display), a script (<code>e2e-tests/access-pass-100.js</code>) pressed the real Tab key through every screen the plugin draws (home, the tab bar, a page, every kind of question, the dashboards, the Allow card, the Now playing bar, the Pages panel and drawer, and the plugin's part of Obsidian's settings), and recorded each stop. It read the accessibility tree Chrome gives a screen reader (names, roles and states), ran axe-core 4.10.2, computed the colour contrast of every distinct style in Obsidian's default light and dark themes, and looked at 200% text at a phone's width (390 by 844) in Obsidian's phone emulation. Each version gave 83 recorded facts; 81 passed and 2 failed on each. On 1.14.5 both failures are in Obsidian's own Settings rows, which the plugin does not draw. On 1.11.7 one is that and the other was the Allow card over Settings: the card opened but focus was pulled back to Settings, and one Escape closed both. The maintainer ruled that this be fixed in 1.0.0, and it is: the card now takes a key scope of its own, and <code>e2e-tests/allow-settings-100.js</code> passes 21 of 21 on 1.11.7 and on 1.14.5 (<code>docs/accessibility-100.md</code>, finding 13).</p>
<p><strong>What was found.</strong> Every screen the plugin draws had a visible focus indicator on every stop, no focus lost and no trap other than the Allow card's own (Tab cycles inside it, as a dialog's should); the exception is Obsidian's own Settings rows, where Tab sticks and some stops show no ring. No control lacked a name or a role. Six problems were fixed in this pass (the Allow card's Escape and Tab with focus outside it, a missing focus ring on the kept-entries fold, Escape on the Pages button, placeholder text contrast, trail links read with a slash in their names, and Pages links under 24 px tall), and three more afterwards: the Allow card over Settings on 1.11.7 (above), the focus ring (below), and 200% text on a 320 px phone, where the Allow card's last line was cut at its scroll area's edge and a folded callout's arrow lay 11 px past the window (the card now has more room to scroll in, with a rule over its Close footer, and a callout title in the plugin's view may wrap; measured on 1.14.5 only, by <code>e2e-tests/phone-200-100.js</code>). At 200% text on a phone width of 390 px nothing overlapped, was cut off or scrolled sideways, except that the data tables are wider than the screen and scroll inside their own box, and that at 100% on a short page the Pages drawer can run under Obsidian's floating bottom bar (its list scrolls inside itself). On 1.11.7 the text scale could not be set exactly, and the figure there is about 187%.</p>
<p><strong>The focus ring</strong> was the theme's pale focus border (1.74 to 1.88 to 1 on the page and cards in the light theme, 1.98 to 2.29 in the dark). It is now the text colour on the plugin's own controls: 12.42 to 1 or more in the light theme and 8.02 to 1 or more in the dark, measured by real Tab presses (<code>e2e-tests/focus-ring-100.js</code>). Not covered: Obsidian's own ring on its own Settings rows (1.80 and 2.08 to 1 on 1.11.7), which the plugin does not draw, and the ring on the browser's native audio player, which was not measured.</p>
<p><strong>Contrast that follows the user's theme.</strong> Some colours the plugin draws are Obsidian's own theme colours, and for 1.0.0 the plugin follows them (the maintainer's ruling, recorded in section E of <code>docs/choices-100.md</code>: keep Obsidian's theme colours for now). They fall under the 4.5 to 1 target for text, or the 3 to 1 for the edge of a control, in the default theme:</p>
<ul>
<li><strong>White text on Obsidian's accent colour</strong> (Continue, Next, the selected choice, the active tab, number badges): 3.43 to 1 in the light theme and 4.26 to 1 in the dark.</li>
<li><strong>Accent-coloured text</strong>, which includes the active page in the Pages panel and links: 4.26 to 1 on white in the light theme (the dark theme passes at 6.19).</li>
<li><strong>The edge of a text field:</strong> 1.01 to 1.28 to 1 in the light theme and 1.24 to 2.29 in the dark. A field's edge is hard to find for a low-vision reader.</li>
<li><strong>Bar and line colours in the light theme</strong> (2.41 to 2.95 to 1). The numbers are always in a table under the chart, so colour is not the only carrier.</li>
</ul>
<p>The default light and dark themes are the only ones measured.</p>
<p><strong>What was not measured.</strong> Speech: the harness cannot hear, so nothing here says what VoiceOver, TalkBack or any other screen reader says. The accessibility tree is what a screen reader is given, not what it speaks. The maintainer tests speech by hand, on a Mac and an iPhone, with the script <code>docs/screen-reader-script-100.md</code>; its results are not in this README. Also not measured: a real phone, Android, Obsidian releases other than the two above, themes other than the default, hover and pressed states, high-contrast mode, reduced motion and voice control.</p>
<h2 id="other-plugins-that-rewrite-notes">Other plugins that rewrite notes</h2>
<p>Booklet was run in a real Obsidian 1.14.5 beside the Linter plugin 1.35.0 (a formatter that rewrites a note on save or on a command). With Linter's default settings, in which every rule is off, nothing in a note changed. With all of its 67 rules on, five test notes were rewritten; no <code>booklet</code> fence and no records line was lost, and Booklet still read every note. Leave these rules off for notes Booklet opens:</p>
<ul>
<li><strong><code>ordered-list-style</code></strong> renumbers a list, and a scale's anchors are a list whose numbers are the stored answers (<code>0.</code> <code>1.</code> <code>2.</code>). After it ran, the scale's anchors began at <code>1.</code>, and an answer of <code>2</code> showed against a different label than the one pressed. The note still passed the linter. This rule can change what a stored answer means.</li>
<li><strong><code>yaml-title</code></strong>, <strong><code>file-name-heading</code></strong> and <strong><code>capitalize-headings</code></strong> rewrite the note's <code>title:</code>, add a heading and re-case headings, so the name Booklet shows changes.</li>
<li><strong><code>empty-line-around-blockquotes</code></strong> and <strong><code>remove-space-around-characters</code></strong> change the records line (a blank line before it, spaces round its dash). Booklet reads the result.</li>
</ul>
<p>This was run on made-up notes and on Linter 1.35.0 only; no other formatter or Linter version was run.</p>
<h2 id="try-it-by-hand">Try it by hand</h2>
<ol>
<li>Copy the <code>test-vault</code> folder of this repository somewhere of your own, and open that folder as a vault in Obsidian (<em>Open another vault</em>, then <em>Open folder as vault</em>).</li>
<li>Obsidian asks whether you trust the author of the vault: choose <em>Trust author and enable plugins</em>, or turn on community plugins in <em>Settings → Community plugins</em>. Booklet is already in the vault's plugin folder and switched on.</li>
<li>If the vault also has the earlier "Booklet (spike)" plugin, turn that one off first: both open booklet notes, and only one should.</li>
<li>Open <code>Check-in</code> (the two widgets and a shared list), <code>Daily tracker</code>, <code>Every kind</code>, <code>Kept records</code> or <code>Evening check-in</code>. All their content is made up. The header button switches views (<em>Show Markdown</em> on the booklet view, <em>Open as booklet</em> on the Markdown view), and so does the command <em>Booklet: Toggle between the note view and Markdown</em>.</li>
</ol>
<p>The folder <code>test/page-notes/Pictures demo</code> is a made-up note with its small picture files, for trying pictures, links to notes and text selection (0.10.0): copy the whole folder into a vault that has the plugin and open <code>Pictures demo.booklet.md</code>.</p>
<p>The folder <code>test/page-notes/Listen demo</code> is a made-up three-unit tea course for trying audio, video, <code>listen</code>, the allow card and Continue (0.12.0): a local tone for each unit (made again, byte for byte, by <code>make-tones.mjs</code>), one hosted episode on an address that never answers (<code>media.tea-course.invalid</code>), a video link and a quiz in each unit. Copy the whole folder into a vault that has the plugin and open <code>Tea course.booklet.md</code>. <code>Tea course with problems.booklet.md</code> beside it holds mistakes on purpose, for the opening notice and the look-alike link rule, and is not part of the corpus test.</p>
<p>To use it in a vault of your own, install it from the Community plugins directory or with BRAT (below), or copy <code>main.js</code>, <code>manifest.json</code> and <code>styles.css</code> from <code>test-vault/.obsidian/plugins/booklet/</code> into <code><your vault>/.obsidian/plugins/booklet/</code>, then turn it on under <em>Community plugins</em>.</p>
<h2 id="install-a-test-build-with-brat">Install a test build with BRAT</h2>
<p>Booklet is in Obsidian's Community plugins directory: <a href="https://community.obsidian.md/plugins/booklet">community.obsidian.md/plugins/booklet</a>, or <em>Settings → Community plugins → Browse</em> and search for Booklet. BRAT is the way to try a build before it is released. Its test builds are GitHub pre-releases of this repository (tags like <code>1.0.2-beta.1</code>; see Releasing), and its real releases are plain <code>x.y.z</code> releases. BRAT (Beta Reviewers Auto-update Tool, a plugin in the directory) installs both and updates them.</p>
<ol>
<li>In <em>Settings → Community plugins</em>, browse for <strong>BRAT</strong>, install it and turn it on.</li>
<li>In BRAT, add a beta plugin and give it the repository <code>benthepsychologist/booklet-obsidian</code>. BRAT installs the newest release or pre-release by version number; turn Booklet on under <em>Community plugins</em> if BRAT has not.</li>
<li>To get a later build, check for updates in BRAT.</li>
</ol>
<p>If the earlier "Booklet (spike)" plugin is in the vault, turn it off first: both open booklet notes.</p>
<h2 id="build-and-test">Build and test</h2>
<pre><code>npm install
npm run build # type-checks, builds main.js, copies main.js, manifest.json and styles.css into test-vault/.obsidian/plugins/booklet/
npm test # bundles src/core and src/view.ts with esbuild, then runs node --test over test/*.test.mjs
npm run lint # the Obsidian directory's linter (eslint-plugin-obsidianmd) over the repository root (src/, manifest.json, package.json, LICENSE); see docs/lint-101.md for what it finds and what is left
</code></pre>
<p><code>npm test</code> runs the reader, the records writer, drafts and kept entries, the JSON edits, and a check that the plugin uses no HTML sinks, no inline styles and no Node or Electron module. It runs the two widget engines of the reference renderer (<code>booklet.html</code>, taken from the Booklet repository as it is) step by step beside the plugin's, and checks that both store the same answer after every step. It also runs the view itself over a jsdom document, with the few parts of Obsidian's API it uses stubbed (<code>test/obsidian-stub.mjs</code>), to check that a change to the note arriving while a field is being edited leaves that field and what was typed in it alone. The stub also holds an in-memory <code>data.json</code>, a settings page and its <code>Setting</code> rows, a renderer that draws pictures and media, and (in <code>test/helpers.mjs</code>) a fake media element, so the allow list, the settings and <code>listen</code> can be tested without Obsidian; its top comment names each. Its corpus test goes over every booklet example in the Booklet repository, every kit starter, every test-vault note and the listen demo's clean note: it answers every question of the eight kinds, every widget and every question on a shared list, finalizes every repeating activity (and finishes a daily one twice in a day), clears every answer, and after each step checks that nothing outside the records region changed, that every record it did not need to change is byte for byte the same, and that <code>lint-booklet.py</code> passes on the written file. The corpus is found through these environment variables; a folder that is not there is a skipped test, with a message, not a failure:</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>What it points at</th>
</tr>
</thead>
<tbody><tr>
<td><code>BOOKLET_REPO</code></td>
<td>a checkout of the Booklet repository (default <code>/workspace/booklet</code>)</td>
</tr>
<tr>
<td><code>BOOKLET_EXAMPLES</code></td>
<td>its examples folder (default <code>$BOOKLET_REPO/examples</code>)</td>
</tr>
<tr>
<td><code>BOOKLET_LINTER</code></td>
<td>its <code>lint-booklet.py</code> (default <code>$BOOKLET_REPO/lint-booklet.py</code>); needs <code>python3</code></td>
</tr>
<tr>
<td><code>BOOKLET_STARTERS</code></td>
<td>a folder of kit starter booklets</td>
</tr>
</tbody></table>
<p><code>npm test</code> also runs marking (a <code>choice</code> and a <code>multi</code>, right and wrong, the replies, Check, nothing written), the matrix (reader, press, clear, grid and stacked, wrong shape, entries, queries) and <code>each:</code> of every kind (copies, pairs, marks on each copy, wrong shape, the bytes written), and the wrong-shape guard over a corpus of answers of the wrong shape for every kind and widget (in answers, drafts and kept entries), <code>each:</code> from the setting's mistakes to the pairs on disk, the empty-draft rule, kept entries and the reader's field as copies come and go, and earlier words: <code>earlier:</code>'s values, each place's sources, limit, order and matching, today's entry, pairs whose pick was taken back, a stale word id, and the bytes a press stores. It checks that the settings the plugin reports a kind does not take are the ones the linter reports, on every corpus file. Every test that lints a test-vault note expects no error from the reference linter on any of them (a linter older than the format the plugin reads may warn that a note's marker is another version, which is a warning and not an error). The check-in note's question for each feeling picked, its <code>blue</code> and <code>purple</code> cells and its earlier thoughts (<code>earlier:12</code>) came with format 0.16, and a 0.15 linter reports them.</p>
<p><code>npm test</code> also runs the 0.7.0 reading side (<code>test/reading.test.mjs</code>): a callout that holds Booklet lines (a group inside a group, a line that makes structure ignored and listed, a question in a tip, the <code>></code> taken off what it holds), a thematic break inside a group, a row's cells and an unclosed row, the steps of a page, a question inside a group answered and what is written, a folded group, an answer of the wrong shape inside a group, steps shown one at a time (Back and Next, no step in Obsidian's history, the first step not done, the finish rule), the footnotes (definitions, the citation pattern, scopes, numbering, the source under its paragraph, Escape, a mark with no note, a mark in code), and the reading kinds' styles; each has a test that fails without it, and drawing the reading note writes nothing.</p>
<p><code>npm test</code> also runs what the cold review of the 0.7.0 draft found (<code>test/review.test.mjs</code>): a row around a group or around a tip that holds a question, a step's group drawn open, ids that no two panes share, a live region written only when it changes, the marks a <code>multi</code>'s <em>Check</em> puts up taken away by a change of the picks, the one rule for a footnote's mark in code and maths, a source opened from a list item or a table cell, a reader's fold of a callout, Back landing on the last step of the page before, a matrix cleared of every answer, words typed in two copies of a question asked for each pick, focus left alone after a refused move, and the steps choice following a renamed note. The second review's round adds a list item's later paragraphs, a heading-like line inside maths in a row, a very long line of backticks, and a press saying its result again. Of the 27 tests in <code>test/review.test.mjs</code>, 17 fail without the fix they pin; the 10 whose titles start <code>[pin]</code> pin behaviour that was already right (found by breaking the code by hand and seeing no test notice), so there is no fix for them to fail without.</p>
<p><code>npm test</code> also runs the 0.6.0 leftovers (<code>test/leftovers.test.mjs</code>): the numeric columns of a long table read once for each column, the tabs' panels and their names, focus after a press that moves, tab names without Markdown marks, a press's default stopped, <code>- - -</code> under text as a break (in pages and in an opening), and a code fence's closing rules; each fails without its fix. The tests wait for a state they name (<code>until</code>, and <code>quiet</code> for a view with nothing under way) and no longer sleep a guessed time, apart from the view's own typing pause and one wait that something does not happen.</p>
<p><code>npm test</code> also runs the 0.9.0 dashboards: data blocks as a source and every view over them (<code>test/data.test.mjs</code>, <code>test/data-view.test.mjs</code>), rows with their time and a scale's number, and tiles of an each-pick (<code>test/picks.test.mjs</code>), removal of one kept entry byte for byte and two entries that share a <code>ts</code> (<code>test/remove.test.mjs</code>, <code>test/remove-ui.test.mjs</code>), the charts and their states in words (<code>test/charts.test.mjs</code>), the reader's sort and filter (<code>test/view-state.test.mjs</code>, <code>test/controls.test.mjs</code>), and the made-up dashboard note in <code>test/page-notes/</code> (<code>test/demo-note.test.mjs</code>). The legacy golden file (<code>test/legacy-golden.json</code>) differs from release 0.8.0's in two places only, the Delete button on a kept entry's card.</p>
<p><code>npm test</code> also runs queries: what the reader takes from a query block and reports (a key a query does not have, <code>limit: week</code>, <code>summary: yes</code>, a span or a summary where no entries are kept, <code>group:</code> on a summary, and a query that cannot be drawn), kept entries as rows, a span of days on a faked clock and in more than one time zone (across midnight, and across a change of clocks), and the summary rules (counting, ties, the reader's own options, zeros, rounding, locale, <code>empty:</code>), the check-in note's roll-up, the four views drawn in the view, and that drawing writes nothing. Each of the 0.5.0 fixes has a test that fails without it: words typed and not added kept through a reload and added before a quit, a comma label typed whole stored as its item, today taken when the view is drawn, and answers of the wrong shape counted once among the note's problems.</p>
<p><code>npm test</code> also runs the notice for what the plugin does not read (<code>test/notice.test.mjs</code>): a made-up note for each case (a marker of another 0.x version with nothing unread and with one unread line, a kind of line, a setting that is a mistake and one that might be new, a query key, a block, several at once, a file with Windows line endings), checking the notice's words, the line numbers it names, that answers are still taken and that every unread line is byte for byte as written after a save; and the names that stay put (<code>test/frozen-names.test.mjs</code>): the plugin's id, its command id and its view type, run on the release build with Obsidian stood in for, and a table of thirty-six saved pane states (nothing, a string, a list, a place of another release, a part in text, a page that is gone) that must open the home screen or the nearest place and never throw. The notice and the names are also checked in the real app (checks 46 and 47 of <code>e2e-tests/</code>).</p>
<p><code>npm test</code> also runs the <code>copy</code> callout (<code>test/copy-callout.test.mjs</code>): the line as read (its id, label, words and lead-in, with no id, in a group, with no <code>of:</code>, outside an activity), the folded hint and solution that belong to a question and the ones that do not, the assembled text for a typed answer, an empty one, a choice, a multi, a scale, a number, a <code>lines</code> answer and a wrong shape, the order, a word written twice, a word that names nothing or cannot be copied, fenced blocks in a module's fence and data section and the shared one, and the view: the button and its status region, the text on a faked clipboard, typed words copied before they are written, a refused clipboard and the field it shows, and that the note on disk is not touched.</p>
<p><code>npm test</code> also runs rows that open and data files (format 0.19). <code>test/rows-open-core.test.mjs</code> is the core's part: a data row that holds a list of rows, or one row, has children, <code>parent:</code> nests flat rows, both make one tree by one rule (a chain 4000 rows deep, a repeated id, a field named like a member every object has), what a row opens to, the fence that names a file and its key path, what a file holds at a key path, the <code>readonly</code> key (read, kept, ignored) and an <code>each:</code> that names a data block. <code>test/rows-open-view.test.mjs</code> draws them over a jsdom document: a table, a list and cards that nest, each row a native <code>details</code> drawn when it opens; a card that opens showing its <code>fields:</code> lines on its closed face; the rows you open kept through a sort, a search and a redraw, and never written to the note; Open all and Close all; a print that opens every closed row and folds back exactly those; a data block that names a file, read through the vault beside the note (the loading words, then the rows, or the quiet box that says why not, a file over 5 MB never read, a name outside the note's folder never read), read again when the vault or the pane says so with the open rows, scroll and focus kept; a file that cannot be read again leaving the rows on screen, a byte order mark ignored, a read with the same text drawing nothing, a change during the first read picked up, and a query's box held for printing let go when the screen is drawn again; the orchard example, a dashboard of 462 rows at every depth, and the names a screen reader gets. The example is <code>test/dashboards/orchard-dashboard.booklet.md</code> with its <code>.json</code> file; every word is made up. The same is checked in the real app by check 61 of <code>e2e-tests/</code>.</p>
<p><code>npm test</code> also runs acts on the rows of a data block (format 0.19, the second slice of the dashboards work). <code>test/rows-ask-core.test.mjs</code> is the core's part: which fields are edited and in what kind of box, rows with no id or a repeated id, <code>readonly</code> bare and by name, which rows a question is asked of, a question placed in a column, what a question starts from, the three display states, the newest act on a row, and the writer (one new entry and no other byte changed, another entry's and pair's own keys kept, an activity that does not repeat refused, a row's Finalize leaving the others in the draft). <code>test/rows-edit-view.test.mjs</code> draws the orchard example over a jsdom document: a value pressed into a box and kept by Enter as one act with <code>was</code>, Esc and unchanged words keeping nothing, leaving the box keeping it, each kind of box, the field read-only where the activity does not repeat, the display states after the data is read again, a refused write giving the words back, words surviving a redraw, <code>readonly</code> per view, a question in a column and in a row's open part, a row's Finalize, the names a screen reader gets, and the print rules in the style sheet. The same is checked in the real app by check 62 of <code>e2e-tests/</code>.</p>
<p><code>npm test</code> also runs adding and removing rows (the third slice of the dashboards work). <code>test/row-id.test.mjs</code> is the id's part (the format read part by part, the slug, <code>{n}</code> counted past the ids and the adds waiting, a clash, the typed id). <code>test/rows-apply.test.mjs</code> and <code>test/rows-fence.test.mjs</code> are the writes (an added row in the layout of the one before, at depth, under a parent, a removed row with its comma, a stale <code>was</code>, CRLF, a byte order mark, trailing blank lines, and the block in the note changed between its fences only). <code>test/rows-act-core.test.mjs</code> is the query keys, the two entry keys kept byte for byte, a daily activity leaving them alone, and what is drawn while the data has not followed. <code>test/rows-add-view.test.mjs</code> draws the controls over a jsdom document, in the note and in a vault file, and <code>test/rows-add-style.test.mjs</code> reads the styles. The same is checked in the real app by check 63 of <code>e2e-tests/</code>.</p>
<p><code>e2e-tests/</code> drives the real Obsidian desktop app over the DevTools protocol and checks the plugin there, one numbered check for each feature; see <code>e2e-tests/README.md</code>.</p>
<h2 id="what-ci-runs">What CI runs</h2>
<p><code>.github/workflows/ci.yml</code> runs on every pull request to <code>main</code> and on every push to <code>main</code>, on a plain runner with no secrets, no dependency cache and read-only permissions (the hardening <code>release.yml</code> has). It runs <code>npm ci</code>, then <code>npm test</code> (the unit tests and the safety test; the corpus tests skip, with a message, because there is no Booklet repository or kit starters on the runner), then a check that the committed <code>test-vault/.obsidian/plugins/booklet/</code> copies equal a fresh <code>node esbuild.config.mjs production</code> build (<code>git diff --exit-code</code> over that folder). A pull request that changes <code>src/</code> or <code>styles.css</code> without rebuilding the vault copies fails it: run <code>npm run build</code> and commit the result.</p>
<p>CI does not run the real-Obsidian suite (<code>e2e-tests/</code>): it needs the Obsidian binary and a display and takes about 9 minutes, so it stays a local gate that a builder runs before a pull request is opened. Whether to run it on a runner is left open. Branch protection (requiring the check before a merge) is a switch on GitHub, not in this repository.</p>
<h2 id="releasing">Releasing</h2>
<p>Every build is a GitHub release made by <code>.github/workflows/release.yml</code> when a version tag is pushed: <code>main.js</code>, <code>manifest.json</code> and <code>styles.css</code> attached, with a build-provenance attestation, the shape BRAT installs from and Obsidian's directory reads. A plain <code>x.y.z</code> tag makes a real release (marked latest, and it must be on <code>main</code>); a tag <code>x.y.z-<suffix></code> such as <code>1.0.2-beta.1</code> makes a pre-release, for trying a build in BRAT before the real one (it may be on any branch). Nothing is submitted to the directory from here. <code>docs/release-flow.md</code> is the maintainer's step list for both (which files carry the version, what <code>release-check.mjs</code> checks, what to look at on the release page).</p>
<ol>
<li><strong>Set the version</strong> on the branch that carries the change: <code>npm version 1.0.1</code>. npm writes it into <code>package.json</code>, and <code>version-bump.mjs</code> writes it into <code>manifest.json</code> and adds it to <code>versions.json</code> with the manifest's <code>minAppVersion</code>; <code>.npmrc</code> stops npm from committing or tagging. The version in the three files is always a plain <code>x.y.z</code>: Obsidian's directory takes no other kind. A pre-release is a tag with a suffix on top of that version (<code>1.0.1-beta.1</code>); the suffix is in no file.</li>
<li><strong>Test and merge:</strong> <code>npm test</code> and the real-Obsidian check (<code>e2e-tests/</code>), then merge to <code>main</code>.</li>
<li><strong>Tag <code>main</code> and push the tag:</strong><pre><code>git checkout main && git pull --ff-only
git tag -a 1.0.1 -m 1.0.1
git push origin 1.0.1
</code></pre>
</li>
<li><strong>The workflow</strong> refuses a tag that is not a plain <code>x.y.z</code> or <code>x.y.z-<suffix></code>, whose <code>x.y.z</code> is not the version in <code>package.json</code>, <code>manifest.json</code> and <code>versions.json</code> (<code>release-check.mjs</code>), or, for a plain <code>x.y.z</code>, that is not on <code>main</code>. Then it builds, runs <code>npm test</code> (the corpus tests skip there, with no Booklet repository on the runner), attests the three files and publishes them: a plain <code>x.y.z</code> as a real release, <code>x.y.z-<suffix></code> as a pre-release. <code>gh attestation verify main.js --repo benthepsychologist/booklet-obsidian</code> checks a downloaded <code>main.js</code> against its attestation.</li>
</ol>
<p>Booklet is listed, so the directory takes every new release straight from GitHub, and a release is a release to everyone.</p>
<h2 id="choices-made-where-the-format-is-silent">Choices made where the format is silent</h2>
<p><strong>The one list of every choice the plugin made where the format is silent is <code>docs/choices-100.md</code>:</strong> 168 rows (and, in its section F, four more for adding and removing rows), each with its source, the reason and the place in the code, sorted into the ones that change what is written to a note, the rest by theme, and the ones that were already the maintainer's own words; section E of it records the maintainer's rulings of 2026-11-03. The lines proposed to the format spec are in <code>docs/spec-proposals-100.md</code>. The choices of 0.12.0 (media, the allow list, <code>listen</code>, Continue) are also one row each in <code>docs/choices-0120.md</code>, a dated record. The rest of this section, the bullets below, is older and is kept for now; where a ruling changed a choice, the one list says so.</p>
<h3 id="pages">Pages</h3>
<p>SPEC 0.18 section 3 defines the page line and the tree (and <code>CHANGES.md</code> says which of its rules came from this plugin's 0.8.0). Read against it, the plugin differs in three kinds of way, listed here: it follows the spec where it once chose otherwise, it leads the spec where the format defines nothing, and it makes choices where the format is silent. Everything in section 3 that is not named below is read as the spec says. A <code>"0.16"</code> note reads page lines too, and a wiki written with <code>parent:</code> on activities is no longer a tree (see <em>What it draws</em>).</p>
<p><strong>Where the spec decides and the plugin follows it</strong> (each of these once was a choice of the plugin's; each is tested)</p>
<ul>
<li>A page line takes <code>icon:</code> and <code>parent:</code> only. On every kind that takes an id, a first word that holds <code>:</code> is a setting and the line has no written id, so <code>> [!page|parent:map] Title</code> has no written id and <code>> [!activity|parent:x] T</code> has none and is listed in the opening notice as ignored ("an activity line takes no <code>parent:</code> setting").</li>
<li>A written page id that is not letters, digits and dashes is said, and the id is made from the title (accents dropped, lowercase, each run of other characters one dash, at most 60 characters); a title that leaves nothing gives <code>page-N</code>, N being the page's place among the tab's page lines.</li>
<li>A fold marker after the bracket is ignored and said. A page line in the booklet's opening, in a module's opening, after a module's end line, in an activity's opening above its first tab line, inside an open row or inside a group is ignored and said; in a data section it is said and ignored, and in the records region it is said and kept as written. A page line inside another callout's body (a <code>tip</code> that holds a question, say) is that body's text, not a page. A file with no activity line is one activity, and its page lines are its pages.</li>
<li>The words before a tab's first page line are the tab's first page, named by the tab (by the activity when there is no tab line), with no id and no emoji split off. They are empty when they hold only blank lines, comments and a lone break; then the first named page is the first. A tab with no page line is one page.</li>
<li>Pages with no <code>parent:</code> stand side by side in file order. A missing parent, a page that is its own parent and a cycle put the page at the top, and each is listed among the note's problems. Of two pages with one id the first keeps it and every other page, in file order, takes the next free number (<code>g</code>, <code>g</code>, <code>g-2</code> are <code>g</code>, <code>g-3</code>, <code>g-2</code>); each renumbering is said. A <code>parent:</code> names the first page that wrote that id. A page id is its own: it may be an activity's or a question's id. A chain of pages each under the one before is read without a depth limit (a chain of twenty thousand reads in a few seconds).</li>
<li>The pages under the one on show are drawn as cards ("Pages under this one"): the title, a picture (<code>icon:</code>, else a leading emoji, else its number), the page's blurb and no status line. All of them are drawn, with no "Show N more" for a long row.</li>
<li>The reading order is depth first. An activity's blurb is the first paragraph of the first thing under its line that is not a page line; a page's blurb is its own first paragraph, and the <code>></code> lines directly under a page line are that paragraph. A page's title is a place a link by words opens (on its first section), and a link to a heading opens the page and the section that hold it.</li>
<li>Back on a later section goes to the section before; on a page's first section it goes up ("Up: The feelings grid"); on a top page it goes to the top page before it, called plain "Back"; from the tab's first page it leaves the activity. Next goes to the next section, then to the next page in reading order ("Next: More than one at once"), then from the tab's last page to the next activity. Back never goes to another tab. The counter is "2 of 3" in a tab that holds a page line and "Page 2 of 3" in a tab with none, and a page of one section has none. A page's title heads every section of it.</li>
</ul>
<p><strong>Where the plugin leads the spec</strong> (the format defines no syntax for these and lets a renderer draw them or not; the reference renderer draws none of them)</p>
<ul>
<li>The "Pages" button is drawn on the left of the page area for a tab that holds a named page and two or more pages. It opens a panel (a <code>nav</code> landmark named "Pages in this tab") holding a search box and the tab's tree, every level open, the page on show marked with <code>aria-current</code> and scrolled into view. On a wide pane (about 40 rem and over) the open panel is a column beside the page; on a narrower pane and on a phone it is a drawer over the page with a Close button, and choosing a page closes the drawer. The panel starts closed, scrolls inside itself, and is drawn with Obsidian's own buttons, search box, lists and variables. Opening moves focus to the search box (to the panel itself on a phone, so no keyboard rises); Escape clears the search words first, then closes the panel and returns focus to the button. Whether it is open and the words in its search box are the pane's, kept in memory while the pane is open and never saved or written.</li>
<li>The search keeps the pages whose title or words hold every word typed (case and marks aside; the words are the page's prose and its questions' prompts), the ones with the words in their title first, then the rest in reading order. Enter opens the first, Escape clears. Nothing typed is written to the note.</li>
<li>"Mentioned on" lists the pages whose words hold a heading link that opens the page on show, from anywhere in the link's module (a bare activity's scope is the bare activities), found by the search a link is followed by. A page does not mention itself; a source is listed once; a link counts only where a reader sees it as a link, so not in a code fence, inline code, an HTML comment, a <code>%%</code> comment, a <code><pre></code> block, a <code>$$</code> block or an indented code block (four spaces or a tab after a blank line, outside a list), nor a block link or an embed; a link in an activity's opening is the activity's; a page in another tab or activity is named with its tab or activity first. It is drawn for a tab that holds a named page, after the cards.</li>
</ul>
<p><strong>Choices where the format is silent</strong></p>
<ul>
<li><p>The cards under a page, and "Mentioned on", are drawn after the last section of the page, whatever step is on show; a tab's first page made of the words before its first page line has no cards.</p>
</li>
<li><p>The trail on a page under another goes from the booklet down: the booklet's title as the link home (left out when the activity's crumb says the same words, as it does for a module of one activity with no words of its own, which is called by the booklet's title: the trail then starts at the activity, once, and does not read "Garden wiki / Garden wiki"), the activity (now a link), the tab when the activity has a tab bar (a link to its first page), each page above (links), then the page. A top page's trail ends at the activity. The pager and the trail name a page without its leading emoji, as an activity's title is. A page's title is drawn as written at the head of the page.</p>
</li>
<li><p>The saved place is <code>{ version, screen, tab, page, part }</code>, kept with the pane in Obsidian's workspace file and never in the note. <code>version</code> is always 2, and a place of any other version opens the start screen. <code>page</code> is a named page's id and <code>part</code> counts within it; a place with no page key (a tab's first unnamed page, and every note with no page line) has <code>part</code> counting every section of the tab. (The format says which page is showing is never stored; it means in the file.)</p>
</li>
<li><p>Nothing inside a closed HTML comment, Obsidian <code>%%</code> comment, <code><pre></code> block or <code>$$</code> block is read as structure: a Booklet line at the start of a line there (<code>> [!page</code>, <code>> [!tab</code>, <code>> [!activity</code>, <code>> [!module</code>, a question, a row, a notice and the rest), a footnote definition and a heading (as a link target) all stay text, so a commented-out page, tab, activity or question does not appear, and none is listed among the note's problems. A region hides lines only when its closer (<code>--></code>, <code></pre></code>, the next <code>$$</code>) is found later in the body, outside fenced code and before the records section (<code>--></code>, <code></pre></code>, the next <code>$$</code>, and for <code>%%</code> the next <code>%%</code>); a <code><!--</code>, <code>%%</code>, <code><pre></code> or <code>$$</code> with no such closer hides nothing (so a stray <code>50%% off</code> is harmless), and neither does one inside inline code or a fenced code block. A <code>%%</code> comment works inline (<code>text %%aside%% more</code>) or over lines, and in a callout line, as <code><!--</code> does; a run of percent signs is read left to right in pairs, so <code>%%%</code> is a <code>%%</code> and a plain <code>%</code>. A line in a fenced code block is not structure either, as before, and text in one is never a closer. Known behaviours the owner ruled to leave ("leave $$ as it is. it's an obsidian problem" and "leave the records oddity and note as a known gap"): two <code>$$</code> far apart in prose pair, and what lies between them is hidden, as Obsidian does (the same holds for two <code>%%</code> in one paragraph); and a <code>> [!records</code> line written inside a comment still starts the records section, because that section is found by a scan of the raw file, so the lines under it are not read as structure. Two things are left as they were: the records section (<code>%%</code> and <code>> [!records</code>) is the plugin's own data section and is read from the raw file, and a closer is never looked for in it, so a closer written inside a saved answer can neither hide the records nor close a mark written in the body (a stray <code><!--</code> in the body stays harmless after the reader types <code>--></code> into an answer); and a page break (<code>---</code>) is still a break wherever it stands, the open item the spec lists under "<code>$$</code> block". The format is silent about all of it.</p>
</li>
<li><p>A second page that repeats an id already ending in a number takes the number after it (<code>g-2</code> twice are <code>g-2</code> and <code>g-3</code>).</p>
</li>
<li><p>Menu and focus: pressing a page in the side panel leaves it open (a drawer closes) and puts focus on the new page's title, which is a heading; so do Next, Back and a card that lead to another page of the same screen (a move to another screen puts focus on that screen's title); the first drawing of a note, and a step through Obsidian's own Back and Forward, do not. A deep tree scrolls inside its panel. A screen has one <code>aria-current="page"</code>: the menu's, with the trail ending in plain words (a tab with no menu keeps the trail's mark). A page group's label is the page's name with its count ("The two axes, 1 of 2"); a tab with no page line keeps "Page 2 of 3". Enter in a search box with no words does nothing.</p>
</li>
<li><p>Steps and pages: a page with two or more groups can be taken one at a time (the format's "one at a time", section 12), and that works on a named page the same way. Back and Next move a step first, then a section, then a page: on a step after the first Back goes to the step before; on the first step it goes where it went without steps (the section before, up, the top page before, or out of the activity), and a move to another section or page opens on its first step, except Back to the section before in the same page, which opens on that section's last step. The step the reader is on is the view's, never a step in Obsidian's Back and Forward and never saved.</p>
</li>
<li><p>Another section or page of a tab is a step in Obsidian's Back and Forward; choosing another tab never is.</p>
</li>
<li><p>Not done: a link that opens a booklet at a page (the format defines none: a link goes by words), and printing a booklet (Export to PDF is not offered for the view; what the row views do when the window prints is described under <em>Rows that open and data files</em>).</p>
</li>
<li><p>Every 0.x marker reads page lines, and another marker makes no notice of its own. A setting a page line does not take (<code>> [!page|x hidden]</code>) is listed in the notice like any other, without the line about updating: it is the page line's to fix.</p>
</li>
</ul>
<h3 id="records-entries-and-widgets">Records, entries and widgets</h3>
<ul>
<li><p>A kept entry's <code>ts</code> is written to the second (<code>2026-10-27T18:30:00Z</code>), as in the format's own example, and first in the entry; two entries never share a <code>ts</code>.</p>
</li>
<li><p>A new block is written in the reference renderer's layout (answers indented by one space, a draft on one line, entries one to a line). An existing block keeps its own layout, and a change follows it.</p>
</li>
<li><p>An entry keeps only the answers that hold something, and a <code>lines</code> answer without its blank lines.</p>
</li>
<li><p>A draft emptied of every answer is removed; an answers block emptied of every answer stays, as <code>{}</code>.</p>
</li>
<li><p>A new draft goes after the activity's entries block; the first entries block takes the draft block's place; an entry is added after the last entry of the activity's last entries block.</p>
</li>
<li><p><code>repeat daily</code> replaces the first entry kept on the same day where it stands.</p>
</li>
<li><p>A widget's colour names are Obsidian's own colour variables, so they follow the theme in light and dark: <code>warm</code> is <code>--color-red</code> (the colour of <code>--text-error</code>; warm is the format's colour for the tone bad), <code>green</code> is <code>--color-green</code> (<code>--text-success</code>, the tone good), <code>amber</code> is <code>--color-orange</code> (<code>--text-warning</code>, the tone warn), <code>teal</code> is <code>--color-cyan</code>, and <code>slate</code> is <code>--text-muted</code> on a faint wash of the text colour (Obsidian has no blue-grey). A cell is a pale wash of its colour with a band of the full colour along its top; its words stay in the theme's text colour.</p>
</li>
<li><p>Words of your own in a widget are added on <em>Add</em>, on Enter, on leaving the field, or on leaving the note (opening another note in the pane, closing it, or Finalize), one or several with commas between them; the reference renderer uses a tag field that adds them on a comma, on Enter and on leaving the field, so nothing typed is dropped. When the field is left by a press on another button, the words are added first and then the press, in the reference renderer's order; the button under the press is kept until its click, as that renderer keeps a cell's buttons and the places' summary, so the press lands. A press on the very button the words name ("tired" typed in a cell, then its <em>Tired</em> pressed; "cool" typed for a place, then <em>Cool</em> pressed) presses it once, as the reference renderer holds that press, rather than pressing it and taking it back. The <em>Add your own</em> field of a choice or multi question adds its words the same way, as format 0.16 and the reference renderer do, and a press on the very option its words name ("plum" typed, then <em>Plum</em> pressed) presses that option once in the same way. Words typed in either kind of field and not added yet are added before Obsidian quits or its window closes; after a reload they are added when the note is next drawn (above, <em>What it writes</em>). Words typed with commas between them are split at the commas, except where they are, whole, a listed item's or sense word's label that holds a comma ("Calm, settled"), or such a word already in the field: that is one word, stored as the item.</p>
</li>
<li><p><strong>What each kind keeps</strong> (the wrong-shape guard), from SPEC sections 5 and 7. An answer that holds nothing (no answer, <code>null</code>, <code>""</code>, <code>[]</code>, <code>{}</code>) is no answer, never of the wrong shape.</p>
<table>
<thead>
<tr>
<th>Kind</th>
<th>Kept as its shape</th>
</tr>
</thead>
<tbody><tr>
<td><code>text</code>, <code>date</code></td>
<td>a string (a date written another way is shown as kept)</td>
</tr>
<tr>
<td><code>lines</code></td>
<td>a list of strings</td>
</tr>
<tr>
<td><code>number</code></td>
<td>a finite number</td>
</tr>
<tr>
<td><code>scale</code></td>
<td>a whole number from 0</td>
</tr>
<tr>
<td><code>choice</code></td>
<td>a whole number from 1, or a string</td>
</tr>
<tr>
<td><code>multi</code></td>
<td>a list of whole numbers from 1 and strings</td>
</tr>
<tr>
<td><code>matrix</code></td>
<td>a list of whole numbers from 0, or <code>null</code>, one for each item</td>
</tr>
<tr>
<td>any kind with <code>each:</code></td>
<td>a list of pairs: <code>pick</code> a number or a string, <code>a</code> an answer of the kind's shape (or none)</td>
</tr>
<tr>
<td><code>grid-select</code></td>
<td>a list of strings</td>
</tr>
<tr>
<td><code>svg-regions</code></td>
<td>a list of <code>{"id": string, "s": [strings]}</code></td>
</tr>
</tbody></table>
<p>Other keys in a pair or a place are kept and allowed. A widget of an engine the plugin does not draw is not checked. Finalize keeps an answer of the wrong shape as written, a <code>lines</code> one with its blank lines too.</p>
</li>
<li><p><strong>Marking</strong>: where the reference renderer (<code>booklet.html</code>) and SPEC section 5 agree this is what the plugin does. A <code>choice</code> with several <code>[x]</code> options takes the first as the answer, as the renderer does; a <code>multi</code> takes all of them. An option of your own (<code>open</code>) is never marked, and an option of a shared list (<code>menu:</code>) has no <code>[x]</code>. A mark applies to each copy of a question asked for each pick on its own.</p>
</li>
<li><p><strong>A matrix</strong>: the reference renderer draws a grid of radio buttons, which cannot clear an item, and draws separate questions only where the pane is narrow; the plugin draws buttons in both, so an item is cleared by pressing its anchor again in both (SPEC allows it). A matrix whose items are all cleared keeps no answer, where the renderer keeps <code>[null, null]</code>. A matrix with no items, or no anchors, is listed among the note's problems and is still drawn as a question. A query of one matrix question with <code>summary: on</code> gives its distribution for each item, "<item> · <anchor>", and a summary of one gives a row for each item, "<title> · <item>", as a scale does. A kept entry shows it as "<item>: <anchor's words>" for each item, with a dash for one not answered. SPEC is silent on these.</p>
</li>
<li><p><strong><code>each:</code> of every kind</strong>: the reference renderer does not draw <code>each:</code> at all; the plugin follows SPEC. A pair whose <code>a</code> holds nothing (<code>null</code>, <code>""</code>, <code>[]</code>) is no answer, as the renderer's check has it, not a wrong shape.</p>
</li>
<li><p><strong><code>each:</code></strong>: the copies are drawn in the order the source's answer holds the picks. While the view is open, a new pick's copy is added at the end and nothing already on screen moves, even when the source's answer changes order (a press or a typed word never reorders it, but a hand edit can); a drawing of the view (opening the note, or a change to its text) takes the answer's order again. That order is the view's, like the open place, and is not stored. A copy whose pick is taken back while you are in its field stays until you leave it. A pick's own options are offered under every pick: first from the other picks' pairs now, in the source's order, then from earlier entries, newest first, each entry's pairs whose pick its source holds; words listed as an option, words already in that pick's answer and repeats are left out before the limit is counted (the same for a plain <code>open</code> question). If a hand edit gives a pick two pairs, the first is shown and changed, and the others are kept as written. A <code>repeat daily</code> draft holding only pairs for picks taken back shows today's entry, with the draft's pairs over the entry's, pick by pick.</p>
</li>
<li><p><strong>Questions inside a callout other than a group</strong> go beyond SPEC section 6 and the reference renderer, which draw a group's questions only (the linter warns of a question in a tip). They are drawn because the plugin's brief asks for it; removing them is a few lines in the reader.</p>
</li>
<li><p><strong>Steps default to all at once</strong>, as the reference renderer does, with the switch on the page; a step view is the reader's choice on the device. A step is what a group is; a page with fewer than two groups has none, and prose between two groups goes with the group after it.</p>
</li>
<li><p><strong>A source opens under its paragraph</strong>, not in the reference renderer's side panel or bottom sheet: Obsidian's panes are narrow and a panel in the flow keeps the page as it is. No hover preview and no endnotes in print.</p>
</li>
<li><p><strong>The law kind is drawn in the theme's own text colour</strong> (Obsidian has no slate), at a soft ground; the other two use the theme's cyan and pink.</p>
</li>
<li><p><strong>A row is drawn only in an activity's pages</strong>; in an opening it is left to Obsidian. A row left open at a page break ends there without a problem listed; one left open at a tab, activity, module or the end of the file is listed.</p>
</li>
<li><p><strong>Own options held on the device have a key of their own</strong>: the words typed in a copy's Add your own field (a question asked for each pick) are held at <code><key>#pick:<pick></code>, so no pick can make a key that another field uses. Words an earlier version held under <code><key>#own:<pick></code> are no longer looked for.</p>
</li>
<li><p><strong><code>blue</code> and <code>purple</code></strong> are drawn from <code>--color-blue</code> and <code>--color-purple</code>, as the other colour names are; like <code>slate</code> and <code>teal</code>, they carry no tone.</p>
</li>
<li><p>The booklet's title is drawn above its opening, and a module's title above the module's opening.</p>
</li>
<li><p>The place open in a body widget is the view's, kept across drawings of the view, not part of the answer.</p>
</li>
<li><p><strong>Dashboards (0.9.0), where SPEC or the format's proposal is silent:</strong></p>
<ul>
<li><strong>Every view of a question asked for each pick named alone in <code>from:</code></strong> (a table, a list, cards, tiles, bars and a line) has one row for each pick, in the source's order (the label is the pick's words, the value the answer from the newest entry that answered it), where the reference renderer's table, list and cards give one row for each entry with a field for each pick, and its tiles, bars and line draw a line saying they do not draw it yet. The plugin leads the reference renderer here, as SPEC section 6 asks ("one row per pick and value").</li>
<li><strong>Removal is by position, time and appearance.</strong> An entry is named by its place among the activity's kept entries in the file, and both its <code>ts</code> and how it read when its card was drawn (its whole entry, as JSON) are checked against it; an entry that is no longer there, or reads differently now (the pane's copy was older than the file), is not removed and the reader is told. It is never found by <code>ts</code> alone, because two entries of one block may share one. Two entries that read exactly alike (the same <code>ts</code> and the same answers) cannot be told apart, so a stale card of one may remove the other; they are the same entry to the reader. An entry read from items in several blocks (the same <code>ts</code> in each) goes from all of them.</li>
<li><strong>A data block that cannot be read</strong> is listed among the note's problems and is not linked, so a query that names it is listed too, as naming something that is nothing in this module, and is not drawn. A data block whose id is also an activity's, a question's or another block's is listed among the note's problems (SPEC section 7 does not allow it). Which one a query reads is unchanged: the activity, then the question, then the first block. A block in the shared data section is read by every module, so the activity or question of that id wins only in its own module, and the problem names that module.</li>
<li><strong>A query that cannot be worked out or drawn</strong> (it threw) says "This query could not be drawn: <the reason>" in its own place, and the rest of the screen draws.</li>
<li><strong>A chart's wording</strong> ("Cannot draw as bars: Mood is not a number.", "One reading so far.", "Every value is 0, so no bar is drawn.") and the sort and filter's ("Sort and filter", "Filter rows", "Sort by", "Original order", "12 rows", "3 of 12 shown", "Nothing matches.") are the plugin's own, in English.</li>
<li><strong>A line's series colours</strong> are Obsidian's named colours (blue, orange, purple, cyan, pink, green), each with its own dash and marker, and a bar is the accent colour; none is the booklet's own colour. A theme that redefines a named colour recolours the chart.</li>
<li><strong>A search</strong> reads the fields the view shows (a role's field, the other fields it draws, the date of a kept entry), any case, as "contains"; a match in a nested list is kept with its parents, which stand open while the search is on.</li>
</ul>
</li>
<li><p><strong>Queries and summaries:</strong></p>
<ul>
<li>A summary's <code>fields:</code> picks its rows and keeps them in the activity's order (the dates row first), as "one row per question, in the activity's order" reads; without a summary, <code>fields:</code> also gives the order.</li>
<li>A summary drawn as a table has a column for each statistic any of its rows has, then the note; the <code>value</code> field repeats one of the statistics, so it is no column of its own. Drawn as a list, tiles or cards, a summary row shows its label, value and note (and badge, tone, link or icon if a key points one at a field), and its other statistics only in a table, since on a summary <code>fields:</code> names rows, not the extras.</li>
<li>The reader's own options in a note and in a distribution come after the listed ones, in the order first seen going over the entries newest first. An own option whose words are a listed option's (case and white space at either end aside) counts as that option; a grid's own words are counted by their words, whichever cell they were typed in.</li>
<li>A question asked for each pick: its fields, and its summary rows, are in the source's list order (its options, items or places), then the reader's own picks as first seen. A summary gives a row for each pick its entries hold, with a count of 0 and no value or note when no place was given for it.</li>
<li>Numbers are drawn in English (the view's language): the average to one decimal, half away from zero, and a whole number with none; other statistics as they are, to at most six decimals.</li>
<li>A <code>link</code> that is a heading (<code>#Heading</code>) is plain text: the view has no headings to go to.</li>
</ul>
</li>
</ul>
<h2 id="licence">Licence</h2>
<p>Apache-2.0. See <code>LICENSE</code>.</p>
<p><strong>Third-party code.</strong> One library is bundled into <code>main.js</code>: <a href="https://github.com/pjeby/monkey-around">monkey-around</a> 3.0.0 by PJ Eby, declared ISC in its package. It is used in <code>src/switch.ts</code> to open a Booklet note in its own view. Its package carries no separate licence file or copyright line to reproduce here.</p>