Write a book
A book is pages of prose with live elements in them: buttons, charts, lists and slots your app fills. Write a page as data, show it in your app, then bind the elements that need to do something.
Write a page
Keep a book in a package, so it ships with the app that shows it.
Make a folder trail for an app package, with this manifest as
trail/package.kdl:
name "@alice/trail-guide"
version "0.1.0"
api_version "*"
kind "app"
A page is a .dhml file: Markdown prose, with elements in kdl
fences. Pages live under chapters/ in a package. Save this one as
trail/chapters/ridge.dhml:
# The ridge trail
The ridge trail climbs 640 m over 12 km. Start early: the
last bus leaves the top at five.
## Before you go
```kdl
column gap=6 {
for var="thing" in="gear" {
label "- {{ thing }}"
}
row gap=8 align_items="center" {
button id="pack" "Packed one"
label id="packed" "{{ packed }} of {{ #gear }} packed"
}
}
```
## The climb
```kdl
chart id="climb" height=180 {
line color="#7fbbb3" width=3 {
point x=0 y=420
point x=3 y=610
point x=6 y=880
point x=9 y=1010
point x=12 y=1060
}
}
```
- Each element is a KDL node, named for the widget it makes:
label,button,row,chart. DHML elements lists them all. {{ }}holds a Lua expression, read from your app's globals each time the page renders.forrepeats what it holds once per item of a list, andifshows what it holds when its condition is true.- An
idnames an element, so your app can reach it.
Show it in your app
Save this as trail/init.lua. It sets the globals the page reads,
loads the page, mounts it, then binds the button:
gear = { "Two liters of water", "A warm jacket", "A paper map" }
packed = 0
local dir = datumhue.package:dir()
local page = dir:open("chapters/ridge.dhml"):book()
page:mount({ height = "100%", padding = 16 })
page:element("pack"):on_click(function()
packed = math.min(packed + 1, #gear)
page:refresh("packed")
end)
local climb = page:element("climb")
climb:update({
margin = { left = 48, right = 16, top = 8, bottom = 28 },
})
climb:axis({ scale = climb.x_scale, side = "bottom" })
climb:axis({ scale = climb.y_scale, side = "left", grid = true })
In this example: datumhue.package Package:dir Dir:open File:book Book:mount Book:element Book:refresh PlotArea:axis
Run it with datumhue run trail. The page shows its heading, the gear
list, the button and the climb; each click on Packed one counts up.
File:bookloads one page as a book.Book:mountshows it as a page that scrolls.Book:elementgives the handle the element made: a button'sUiElement, a chart'sPlotArea. Here the button gets a click handler, and the chart gets axes.Book:refreshrenders an element again, so its{{ }}read the globals as they are now.
Check it
Before you share the package, check it:
$ datumhue pkg check trail
It prints a line that starts with OK when the manifest and the
package's files are in order, and names each problem when they are
not.
Make it a book of chapters
A book.kdl at the package's root names the book and lists its
chapters, in reading order. Write the second page,
trail/chapters/lake.dhml, as you wrote the first:
title "Trail guide"
chapter "The ridge trail" file="chapters/ridge.dhml"
chapter "The lake loop" file="chapters/lake.dhml"
Load the whole book with Dir:book instead of File:book. A book of
several chapters mounts with a list of chapters beside the page and a
row to step between them. Book:go moves to a chapter, or to an
element's id on the page:
local book = datumhue.package:dir():book()
book:mount({ height = "100%" })
book:go(2)
Add content_type "book" to package.kdl to say the package is a
book. pkg check then also reads book.kdl, and checks every page it
lists against the elements DatumHue knows.
Images
A page shows an image as Markdown writes one: !, its alt text in
brackets, then its path in the book's folder in parentheses. A screen
reader names the image by its alt text. Leave the brackets empty for an
image that only decorates, such as a divider, and assistive technology
passes over it:


Images need a book loaded with Dir:book: File:book raises on a page
with one. Bytes:book shows a named image as its alt text in italics,
[image: The lake at dawn, seen from the ridge], and leaves out an
image with empty brackets.
Scripts and listings
A lua fence in a page is a script: it runs once, in your app's
globals, when the book loads. Add source after the fence's language
to show its code instead of running it, or echo to show it and run it
too. A kdl fence takes the same words:
```lua
gear = { "Water", "A hat" }
```
```kdl echo
button "Try me"
```
The font DatumHue draws text in joins some signs into one shape, such
as >= into one sign. For a page that teaches code, mount the book
with ligatures = false. Every character of the book's text then shows
as typed: prose, listings, the text of elements, the chapter list and
navigation alike. A chart's labels still join them.
local book = datumhue.package:dir():book()
book:mount({ height = "100%", ligatures = false })
A book without scripts is data: pkg new
with --kind book makes one as an asset pack, which any app opens with
Dir:book. A book with scripts runs code, so its package must be an
app; pkg check says so when it is not.
See also
- DHML elements, for every element and its properties.
- Query and chart data, for the marks a
chartelement holds. - Open, pick and save files,
for the
DirandFilehandles a book loads from.
Reference used on this page: datumhue.package Package:dir Dir:open File:book Book:mount Book:element Book:refresh PlotArea:axis Dir:book Book:go Bytes:book