Skip to main content
Recipe number 163

Cookbook · Chapter 12 · Textbooks

A harmony textbook with music examples in ABC

A chapter on cadences whose examples are ABC fences in the Markdown, engraved at the column width, one of them running on over two pages.

On this page
Output
Canvas · PDF
Postext
Tested with Postext 1.54.0
Needs ≥ 1.54.0 · postext-pdf ≥ 1.54.0
Licence
Updated 22 Oct 2026
Code MIT · Text CC BY 4.0

pp. 2–3 of 4

  • Trim 180 × 250 mm
  • 1 column
  • Libre Caslon Text 10.5/14.5
  • Libre Caslon Display
  • Libre Franklin
  • 4 pages
  • Level
  • Postext 1.54.0
  • Laid out in 15 ms
  • 152 lines of code

In short

A chapter of a music theory textbook about how phrases end. The music examples are written as short lines of text in the chapter, and Postext draws them as staves and notes; the longest one continues on the next page like a paragraph.

What you'll build

Chapter 5 of a keyboard harmony course, 180 × 250 mm in Libre Caslon, on the five cadences: perfect and imperfect authentic, half, plagal and deceptive. Each cadence gets two short paragraphs and an example in three voices with its Roman numerals under the staff, written in ABC right in the Markdown. The chapter ends with Greensleeves in A minor, a numeral over every bar, and that melody is too long for the room left on page 3: it sets three systems there and the last two at the head of page 4, broken between systems as a paragraph breaks between lines. Staff size, music font and spacing are set once in the configuration, so the five examples and the melody share one look.

This recipe answers

  • How do I put music examples in the text of a textbook, written as notation?

The short answer

script.js · lines 36–51in full code
// Each ```abc fence in the chapter is engraved at the width of the text column and cut
// into systems. The style is set once here; the Markdown only holds the notation.
const scoreStyle = {
  staffSize: mm(5.5), // a textbook's rastral: examples read at arm's length on a desk
  musicFont: 'Bravura', // the traditional engraved look, heavy enough beside Caslon
  textFont: 'Libre Caslon Text', // the Roman numerals of `w:` lines, in the text face
  spacing: 1.6, // air between the chords: an example reads as a progression, not a cluster
  barNumbers: 'system',
  systemGap: mm(4),
  marginTop: pt(LEAD / 2),
  marginBottom: pt(LEAD / 2),
  color: col('ink'),
  // A long example runs on into the next page between two systems, never leaving one
  // system alone at the foot of a page or at the head of the next.
  minSystemsAtBreak: 2,
};

One score style for every example in the text

Ingredients

Type
Libre Caslon Text, Libre Caslon Display, Libre Franklin (SIL OFL 1.1)
Assets
None: every picture is drawn in code

Method

#1 · Write the example where the text needs it

A fence tagged abc holds the notation, and the text goes on after it. L:1/2 makes each bare chord a half note, [Bdg] stacks three notes in one voice, and a w: line puts one word under each chord, here the Roman numerals. The alt attribute is what a screen reader and the tagged PDF say instead of the picture.

```abc {alt="Perfect authentic cadence in G major: I, IV, V, I"}
X:1
M:4/4
L:1/2
K:G
[Bdg] [ceg] | [Adf] [Bdg] |]
w: I IV V I
```

#2 · Give every score the book's look

The short answer is the whole design of the music: a 5.5 mm staff, as harmony books use for examples read at the keyboard, Bravura beside the Caslon text, and the numerals in the text face instead of the engraver's Times. spacing: 1.6 spreads four half-note chords over a third of the measure, so a progression reads chord by chord. The same setting decides how many bars of Greensleeves fit on a system: three, in a 125 mm measure.

#3 · Let a long example run on

minSystemsAtBreak: 2 is what keeps Greensleeves readable across the turn. The melody comes out as five systems; three fit at the foot of page 3, so two go to page 4, and neither page is left with a single system. A score with fewer than four systems moves whole to the next page, as the short cadence examples would.

#4 · Start the engraver before the build

script.js · lines 264–270in full code
// The engraver (Verovio) loads only for books with music: start it before the build,
// or every score is laid out as a grey placeholder box.
await initMusicEngine();
const doc = await buildDocumentWithFonts({ markdown }, config, kitFonts(FONTS));
showPages(doc, { title: t({ en: 'Harmony textbook', es: 'Manual de armonía' }) });
// The PDF draws every system as vector paths, the numerals in the embedded text face.
offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider }), `${RECIPE}.pdf`);

Verovio is about 3.7 MB of WebAssembly, so the engine loads it only when you ask. Without initMusicEngine() the layout still runs, and every score is a grey box of the right size. renderToPdf needs nothing more: the PDF draws each system as vector paths and sets the numerals in the embedded Caslon.

#5 · Open the chapter with a sunk title

script.js · lines 55–72in full code
const opener = {
  enabled: true,
  minHeight: mm(48),
  slot: { elements: [
    { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: LABEL, fontSize: pt(8.5),
      fontWeight: 700, letterSpacing: pt(1.7), textTransform: 'uppercase', color: col('accent'),
      align: 'left', placement: { anchor: { to: 'container', edge: 'top-left' },
        offset: { x: mm(0), y: mm(12) } } },
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: 'Libre Caslon Display',
      fontSize: pt(34), lineHeight: 1.05, color: col('ink'), align: 'left', overflow: 'wrap',
      placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
        size: { width: mm(110), height: 'auto' } } },
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.5),
      color: col('accent'),
      placement: { anchor: { to: '#title', edge: 'below' }, offset: { x: mm(0), y: mm(5) },
        size: { width: mm(16) } } },
  ] },
};

The opener takes its kicker from the heading's kicker attribute and reserves 48 mm, enough for the title and its rule with the first example still on page 1.

The whole recipe

Sandbox
// ═══ Postext Cookbook · Nº 163 · Harmony textbook with ABC examples ═════════════
// https://postext.dev/en/cookbook/harmony-textbook-abc-examples
// Code: MIT · Text: original (CC BY 4.0) · Music: Greensleeves (traditional, PD)
// Fonts: Libre Caslon Text & Display, Libre Franklin (SIL OFL 1.1) · Needs postext ≥ 1.54.0
import {
  buildDocumentWithFonts, renderPageToCanvas, initMusicEngine,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'harmony-textbook-abc-examples';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: a conservatory green for the accents, warm ink for text and notes
const palette = {
  ink: '#1f1c19', // text and notation: a warm near-black
  accent: '#2f5d4f', // kickers, rules, Roman numerals in the table
  tint: '#edf2ef', // the box of key terms
  rule: '#c3cfc9', // hairlines
  muted: '#5c6863', // running heads, colophon
  paper: '#ffffff',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  // The engine's defaults link to main-color: point it at the accent, never the default blue.
  { id: 'main-color', name: 'accent (defaults)', value: { hex: palette.accent, model: 'hex' } },
];
// #endregion
const TRIM = { width: 180, height: 250 };
const MARGIN = { top: 22, bottom: 24, inner: 22, outer: 33 }; // a 125 mm measure
const LEAD = 14.5; // body leading in pt: the baseline grid
const LABEL = 'Libre Franklin';

// #region answer: one score style for every example in the text
// Each ```abc fence in the chapter is engraved at the width of the text column and cut
// into systems. The style is set once here; the Markdown only holds the notation.
const scoreStyle = {
  staffSize: mm(5.5), // a textbook's rastral: examples read at arm's length on a desk
  musicFont: 'Bravura', // the traditional engraved look, heavy enough beside Caslon
  textFont: 'Libre Caslon Text', // the Roman numerals of `w:` lines, in the text face
  spacing: 1.6, // air between the chords: an example reads as a progression, not a cluster
  barNumbers: 'system',
  systemGap: mm(4),
  marginTop: pt(LEAD / 2),
  marginBottom: pt(LEAD / 2),
  color: col('ink'),
  // A long example runs on into the next page between two systems, never leaving one

The composed script.js runs as it is: paste it into any page’s module script, or open the recipe on CodePen. Recipe folder on GitHub ↗ (opens in a new tab)

Variations

#Use Leland for a lighter page

Leland, MuseScore's font, has thinner stems and smaller noteheads than Bravura; it suits a book set in a light text face.

-  musicFont: 'Bravura',
+  musicFont: 'Leland',

#Write a second voice in another format

The ABC reader takes one voice. For a soprano and bass on separate staves, write the example in Humdrum or MusicXML; the conservatory method sets four-part chorales that way.

Pitfalls

  • Write a chord's length inside the brackets, [B2d2g2], or set L: to the chord's value: a length after the bracket, [Bdg]2, is read as the default length.
  • Verovio's ABC reader gives each syllable of a w: line to the next note and ignores * and _, so a w: line cannot skip notes. To mark a few bars, write the marks as chord symbols over the staff, as the Greensleeves example does.
  • A text face rarely has the ♯ and ♭ signs: write "F-sharp" in the prose, or the browser sets the sign from a font of the system.

Credits

Text
  • The chapter on cadences and its examples, written for this recipe · Ignacio Ferro · CC BY 4.0
  • Greensleeves, English tune of the sixteenth century, in its common modern form · Traditional · public domain
Fonts
Libre Caslon Text (SIL OFL 1.1) · Libre Caslon Display (SIL OFL 1.1) · Libre Franklin (SIL OFL 1.1)
SandboxPDF