# 双栏和声教程中的编号众赞歌谱例

> 用Humdrum **kern写的四声部众赞歌成为example类型的乐谱资源：编号为“谱例4.1”，用:ref引用，像插图一样浮动。

- HTML版本: https://postext.dev/zh/cookbook/conservatory-method-chorales
- 食谱 No. 166 · 图与图片 · 难度 2 (中级) · 输出: Canvas
- 体裁: 教科书
- 需要 postext ≥ 1.54.0 · 已用 1.54.0 测试，日期 2026-10-22
- 页面: [61](https://postext.dev/cookbook/conservatory-method-chorales/en/p01.webp?v=892c2a86), [62](https://postext.dev/cookbook/conservatory-method-chorales/en/p02.webp?v=892c2a86), [63](https://postext.dev/cookbook/conservatory-method-chorales/en/p03.webp?v=892c2a86)
- 在沙盒中打开: https://postext.dev/zh/sandbox#recipe=conservatory-method-chorales&lang=en (.postext: https://postext.dev/cookbook/conservatory-method-chorales/en/conservatory-method-chorales.postext)
- 最后更新: 2026-10-22
- 其他语言: [en](https://postext.dev/en/cookbook/conservatory-method-chorales.md), [es](https://postext.dev/es/cookbook/conservatory-method-chorales.md), [ca](https://postext.dev/ca/cookbook/conservatory-method-chorales.md), [de](https://postext.dev/de/cookbook/conservatory-method-chorales.md), [fr](https://postext.dev/fr/cookbook/conservatory-method-chorales.md), [it](https://postext.dev/it/cookbook/conservatory-method-chorales.md), [pt](https://postext.dev/pt/cookbook/conservatory-method-chorales.md), [ja](https://postext.dev/ja/cookbook/conservatory-method-chorales.md), [ar](https://postext.dev/ar/cookbook/conservatory-method-chorales.md)

## 简单来说

一本音乐学院和声教材中讲终止式的一章，分两栏排。五个谱例以文本写成，刻成真正的乐谱，像插图一样编号、配图注，并放在提到它们的段落附近。

## 成品一览

《Four-Part Harmony》第4章，一本供音乐学院低年级使用的和声教程，开本210 × 280 mm，用Crimson Pro排成两栏。这一课讲终止式，五个谱例都是写在两行谱表上的四声部众赞歌，低音下方标出罗马数字级数：G大调的五种终止式、《Nun danket alle Gott》的三句，以及完整的日内瓦诗篇第134篇（*Old Hundredth*）。每个谱例都有编号，从Example 4.1到4.5（西班牙文版为Ejemplo），图注用标签字体，标签为酒红色，并排在引用它的段落之后那一栏的顶部或底部；整首诗篇横跨两栏，排在第63页顶部。这些和声配置是为本教程写的，并检查过平行五度和八度、声部间距以及导音的解决。

**这份食谱回答的问题:**

- 怎样给四声部谱例编号，并让它们像插图一样在双栏中浮动？
- 怎样加一幅带编号题注的图，并在正文里引用它（“见图3.2”）？
- 怎样控制图的位置：页面顶部、横跨两栏、就放在这里，还是放在边栏里？

## 简短回答

```js
// script.js, 行 26–41
// In the Markdown, :ref{id="ex-old-hundredth" style="full"} prints 'Example 4.4' ('Ejemplo
// 4.4' in Spanish) and ::resource{id="…"} anchors the float after the paragraph that cites it.
const example = (id, source, placement, styleId) => {
  const [caption, note, altText] = CAPTIONS[id];
  return { id, typeId: 'example', kind: 'score', caption, note, altText, placement,
    score: { format: 'humdrum', source, styleId }, createdAt: 0, updatedAt: 0 };
};
// The **kern sources are in #region art-scores; captions in content.captions.<lang>.md.
const resources = () => [
  example('ex-cadences', CADENCES, { position: 'bottom' }, 'column'),
  example('ex-nun-danket', NUN_DANKET, { position: 'top' }, 'column'),
  example('ex-nun-danket-3', NUN_DANKET_3, { position: 'auto' }, 'column'),
  // Across both columns in the document's own score style: the whole tune in two systems.
  example('ex-old-hundredth', OLD_HUNDREDTH, { position: 'top', span: 'page' }),
  example('ex-deceptive', DECEPTIVE, { position: 'auto', span: 'page' }),
];
```

## 用料

**讲解**

- [编号谱例](https://postext.dev/zh/docs/music.md#编号谱例): kind为score、类型为example的资源：按文档语言编号（谱例3.2），带图注，用:ref引用，像插图一样就地放置或浮动；每个谱例保持完整。
- [图的放置](https://postext.dev/zh/docs/document-format-resources.md#位置): 顶部、底部、自动或此处，排在一栏内或横跨页面，占宽度的某个比例，可按资源或按类型设置；同一序列的浮动体从不互相超越。
- [决定图位置的引用](https://postext.dev/zh/docs/document-format-resources.md#行内引用主要写法): :ref引用一个资源（“见图3.2”），首次引用决定它的位置：图浮动到引用之后第一个空位。

**还用到**

- [乐谱记谱](https://postext.dev/zh/docs/music.md#正文中的乐谱)
- [乐谱样式](https://postext.dev/zh/docs/configuration-resources.md#乐谱样式)
- [编号题注](https://postext.dev/zh/docs/document-format-resources.md#按首次引用编号)
- [用你的语言显示“图”和“表”](https://postext.dev/zh/docs/configuration-resources.md#资源类型)
- [自定义资源类型](https://postext.dev/zh/docs/configuration-resources.md#资源类型)
- [题注样式](https://postext.dev/zh/docs/configuration-resources.md#题注样式)
- [来源与出处行](https://postext.dev/zh/docs/configuration-resources.md#题注样式)
- [单栏或双栏](https://postext.dev/zh/docs/configuration-page-layout.md#版式类型)
- [设计过的章首页](https://postext.dev/zh/docs/configuration-headings.md#通栏与高级设计)
- [标题属性](https://postext.dev/zh/docs/document-format.md#标题属性)
- [书眉与页码](https://postext.dev/zh/docs/configuration-headers-footers.md#页眉与页脚)
- [对称页边距](https://postext.dev/zh/docs/configuration-page-layout.md#对称页边距)
- [按页面角色显示书眉](https://postext.dev/zh/docs/configuration-headers-footers.md#文本元素)
- [标注框](https://postext.dev/zh/docs/configuration-callouts.md#标注框样式)
- [段落样式](https://postext.dev/zh/docs/configuration-styles.md#段落样式)
- [语义调色板](https://postext.dev/zh/docs/configuration-fonts-colors-viewers.md#调色板)
- [图就放在这里](https://postext.dev/zh/docs/document-format-resources.md#块级嵌入可选显式的行内放置)
- [通栏章首色带](https://postext.dev/zh/docs/configuration-headings.md#通栏与高级设计)
- [上标与下标](https://postext.dev/zh/docs/document-format-inline.md#行内格式)

**配置一览**

- [`bodyText`](https://postext.dev/zh/docs/configuration-text.md#正文), [`calloutStyles`](https://postext.dev/zh/docs/configuration-callouts.md#标注框样式), [`captionStyle`](https://postext.dev/zh/docs/configuration-resources.md#题注样式), [`colorPalette`](https://postext.dev/zh/docs/configuration-fonts-colors-viewers.md#调色板), [`footer`](https://postext.dev/zh/docs/configuration-headers-footers.md#页眉与页脚), [`header`](https://postext.dev/zh/docs/configuration-headers-footers.md#页眉与页脚), [`headings`](https://postext.dev/zh/docs/configuration-headings.md#标题), [`layout`](https://postext.dev/zh/docs/configuration-page-layout.md#版式), [`locale`](https://postext.dev/zh/docs/configuration-language.md#断词), [`orderedLists`](https://postext.dev/zh/docs/configuration-lists-math.md#有序列表), [`page`](https://postext.dev/zh/docs/configuration-page-layout.md#页面), [`paragraphStyles`](https://postext.dev/zh/docs/configuration-styles.md#段落样式), [`resourceTypes`](https://postext.dev/zh/docs/configuration-resources.md#资源类型), [`scoreStyle`](https://postext.dev/zh/docs/configuration-resources.md#乐谱样式), [`scoreStyles`](https://postext.dev/zh/docs/configuration-resources.md#乐谱样式), [`unorderedLists`](https://postext.dev/zh/docs/configuration-lists-math.md#无序列表)

**API**

- [`buildDocumentWithFonts`](https://postext.dev/zh/docs/configuration-programmatic-usage.md#排版前加载字体), [`defaultResourceTypes`](https://postext.dev/zh/docs/configuration-resources.md#资源类型), [`initMusicEngine`](https://postext.dev/zh/docs/music.md#正文中的乐谱), [`renderPageToCanvas`](https://postext.dev/zh/docs/configuration-programmatic-usage.md#把页面渲染为位图)

**字体**

- Crimson Pro (OFL-1.1), Playfair Display (OFL-1.1), Fira Sans Condensed (OFL-1.1)

## 做法

### 1 · 用Humdrum **kern写每首众赞歌

```js
// script.js, 行 263–273
// Each example lists its spines as Humdrum wants them, lowest first: the bass staff split into
// tenor and bass, the **harm spine (printed under that staff), the treble staff split into
// soprano and alto. Writing them with spaces keeps the pen free of tab characters.
const kern = (meter, body) => [
  '**kern **harm **kern', '*clefF4 * *clefG2', '*k[f#] * *k[f#]', `*M${meter} * *M${meter}`,
  '*^ * *^', ...body.trim().split('\n'), '== == == == ==', '*- *- *- *- *-',
].map((row) => row.trim().split(/ +/)
  // Spanish harmony books write every degree in capitals: vi is VI, iib is IIb.
  .map((cell) => (LANG === 'es' && /^(?:i{1,3}|iv|vi{0,2})[bc7]?$/.test(cell)
    ? cell.replace(/^[iv]+/, (degree) => degree.toUpperCase()) : cell))
  .join('\t')).join('\n');
```

Humdrum把各声部写成用制表符分隔的列，从最低的声部写起。每行谱表是一列（*spine*），用`*^`一分为二：低音谱表上是男高音和男低音，高音谱表上是女高音和女低音，于是刻谱器在每行谱表上排两个声部，符干分开（左边的声部朝上，右边的朝下）。低音谱表之后的`**harm`列在它下方印出级数；`Ib`是第一转位，`Ic`是四六和弦。脚本用空格写各列，再把空格换成制表符，这样任何编辑器都弄不坏文件；西班牙文版的级数全用大写（VI、IIb），这是西班牙和声教材的写法。

### 2 · 把每个谱例做成example类型的资源

[简短回答](https://postext.dev/zh/cookbook/conservatory-method-chorales.md#简短回答)创建了这五个资源。`typeId: 'example'`使用内置的“谱例”类型：资源用到它时，引擎就把它加入书的资源类型，名称随文档语言而定（`locale: 'es'`得到Ejemplo），按章编号，所以第4章的第一个谱例是4.1。乐谱资源始终完整，像插图一样浮动：`position: 'top'`或`'bottom'`占据引用之后第一个有空间的栏的顶部或底部，`span: 'page'`让*Old Hundredth*横跨两栏。图注、出处和替代文本来自每个版本的第二个Markdown文件。

### 3 · 给栏内谱例更小的谱表

```js
// script.js, 行 45–53
const scoreStyle = {
  staffSize: mm(5.4), musicFont: 'Bravura', barNumbers: 'system', color: col('ink'),
  textFont: TEXT, // the Roman numerals under the bass, in the text face
  marginTop: pt(4), marginBottom: pt(2),
};
const scoreStyles = [ // rastral 4.6 mm: four staves of a chorale fit a column in two systems
  { id: 'column', name: t({ en: 'Column examples', es: 'Ejemplos de columna' }),
    staffSize: mm(4.6), barNumbers: 'none', spacing: 0.8 },
];
```

两个通栏的众赞歌使用文档的`scoreStyle`：谱表高5.4 mm，每行开头标小节号。栏内谱例选用名为`column`的样式：谱表4.6 mm，不标小节号，间距0.8，使谱例4.2的五个小节留在同一行，而不是把最后一小节单独留到第二行。

### 4 · 排版前启动刻谱器

```js
// script.js, 行 398–404
await initMusicEngine(); // until it runs, every score is laid out as a grey box
const continuation = { pageNumbering: { startAt: 61 }, // odd: the opener is a recto
  headings: { h1: 3, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 } }; // the next # is chapter 4
const doc = await buildDocumentWithFonts({ markdown, resources: resources(), continuation },
  config, kitFonts(FONTS));
showPages(doc, { title: t({ en: 'Numbered chorale examples in a harmony method',
  es: 'Corales numerados en un método de armonía' }) });
```

`initMusicEngine()`从软件包加载刻谱器Verovio；在它运行之前，每份乐谱都排成灰色方框。`continuation`让这一章成为一本更长的书的第4章，所以谱例编号为4.1到4.5，页码从右页的61开始。

### 5 · 让章首横跨两栏

```js
// script.js, 行 57–77
const label = { fontFamily: LABEL, fontWeight: 600, textTransform: 'uppercase' };
const opener = {
  enabled: true, minHeight: mm(46),
  slot: { elements: [
      { kind: 'text', id: 'kicker', content: '{attr.kicker}', ...label, fontSize: pt(9),
        letterSpacing: pt(1.8), color: col('accent'), align: 'left',
        placement: { anchor: { to: 'container', edge: 'top-left' },
          offset: { x: mm(0), y: mm(6) } } },
      { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontSize: pt(36),
        lineHeight: 1.05, fontWeight: 400, color: col('ink'), align: 'left',
        placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
          size: { width: mm(150), height: 'auto' } } },
      { kind: 'text', id: 'deck', content: '{attr.deck}', fontFamily: TEXT, fontSize: pt(13),
        lineHeight: 1.25, italic: true, color: col('muted'), align: 'left',
        placement: { anchor: { to: '#title', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
          size: { width: mm(120), height: 'auto' } } },
      { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.5), color: col('accent'),
        placement: { anchor: { to: '#deck', edge: 'below' }, offset: { x: mm(0), y: mm(5) },
          size: { width: mm(18) } } },
  ] },
};
```

章首页横跨两栏（一级标题上的`span: 'page'`），眉题和导语取自标题的属性。`minHeight`让正文下沉46 mm，两栏都从短线下方的同一行开始。

## 完整食谱

一个文件，由食谱文件夹合成，示例文本和排版食谱的公共工具包都已内联；它自己排出页面。要运行它，把它放进空白页面的`<script type="module">`，或粘贴到新建CodePen的JS面板里（设为模块）。它从esm.sh导入postext，不需要安装或构建。

- 食谱文件夹: https://github.com/drnachio/postext/tree/main/cookbook/conservatory-method-chorales

### script.js

```js
// ═══ Postext Cookbook · Nº 166 · Numbered chorale examples in a harmony method ══
// https://postext.dev/en/cookbook/conservatory-method-chorales
// Code: MIT · Text and four-part settings: original (CC BY 4.0) · Melodies: public domain
// Fonts: Crimson Pro, Playfair Display, Fira Sans Condensed (OFL 1.1) · Needs postext ≥ 1.54.0
import {
  buildDocumentWithFonts, renderPageToCanvas, defaultResourceTypes, initMusicEngine,
} from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'conservatory-method-chorales';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// A claret accent on warm paper tones, every colour linked by id.
const palette = { ink: '#1f1a17', accent: '#7a2531', tint: '#f5eee6', rule: '#cdbfb2',
  muted: '#6a5f57', 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' } })),
  { id: 'main-color', name: 'accent (defaults)', value: { hex: palette.accent, model: 'hex' } },
];
const [TEXT, DISPLAY, LABEL] = ['Crimson Pro', 'Playfair Display', 'Fira Sans Condensed'];
const OUTER = 16; // outer margin in mm: running heads and folios align to it
const LEAD = 14.2; // body leading in pt

// #region answer: score resources typed 'example', floated and numbered like figures
// In the Markdown, :ref{id="ex-old-hundredth" style="full"} prints 'Example 4.4' ('Ejemplo
// 4.4' in Spanish) and ::resource{id="…"} anchors the float after the paragraph that cites it.
const example = (id, source, placement, styleId) => {
  const [caption, note, altText] = CAPTIONS[id];
  return { id, typeId: 'example', kind: 'score', caption, note, altText, placement,
    score: { format: 'humdrum', source, styleId }, createdAt: 0, updatedAt: 0 };
};
// The **kern sources are in #region art-scores; captions in content.captions.<lang>.md.
const resources = () => [
  example('ex-cadences', CADENCES, { position: 'bottom' }, 'column'),
  example('ex-nun-danket', NUN_DANKET, { position: 'top' }, 'column'),
  example('ex-nun-danket-3', NUN_DANKET_3, { position: 'auto' }, 'column'),
  // Across both columns in the document's own score style: the whole tune in two systems.
  example('ex-old-hundredth', OLD_HUNDREDTH, { position: 'top', span: 'page' }),
  example('ex-deceptive', DECEPTIVE, { position: 'auto', span: 'page' }),
];
// #endregion

// #region scores: one score style for the page-wide chorale, a smaller one for the columns
const scoreStyle = {
  staffSize: mm(5.4), musicFont: 'Bravura', barNumbers: 'system', color: col('ink'),
  textFont: TEXT, // the Roman numerals under the bass, in the text face
  marginTop: pt(4), marginBottom: pt(2),
};
const scoreStyles = [ // rastral 4.6 mm: four staves of a chorale fit a column in two systems
  { id: 'column', name: t({ en: 'Column examples', es: 'Ejemplos de columna' }),
    staffSize: mm(4.6), barNumbers: 'none', spacing: 0.8 },
];
// #endregion

// #region opener: kicker, title, deck and a short rule across both columns, sunk 46 mm
const label = { fontFamily: LABEL, fontWeight: 600, textTransform: 'uppercase' };
const opener = {
  enabled: true, minHeight: mm(46),
  slot: { elements: [
      { kind: 'text', id: 'kicker', content: '{attr.kicker}', ...label, fontSize: pt(9),
        letterSpacing: pt(1.8), color: col('accent'), align: 'left',
        placement: { anchor: { to: 'container', edge: 'top-left' },
          offset: { x: mm(0), y: mm(6) } } },
      { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontSize: pt(36),
        lineHeight: 1.05, fontWeight: 400, color: col('ink'), align: 'left',
        placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
          size: { width: mm(150), height: 'auto' } } },
      { kind: 'text', id: 'deck', content: '{attr.deck}', fontFamily: TEXT, fontSize: pt(13),
        lineHeight: 1.25, italic: true, color: col('muted'), align: 'left',
        placement: { anchor: { to: '#title', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
          size: { width: mm(120), height: 'auto' } } },
      { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.5), color: col('accent'),
        placement: { anchor: { to: '#deck', edge: 'below' }, offset: { x: mm(0), y: mm(5) },
          size: { width: mm(18) } } },
  ] },
};
// #endregion

// #region running-heads: book title on the verso, chapter on the recto, folios outside
const head = (id, content, parity, edge, x, extra = {}) => ({
  kind: 'text', id, content, parity, pages: 'body', ...label, fontSize: pt(7.8),
  letterSpacing: pt(1.2), color: col('muted'),
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(12) } }, ...extra,
});
const folio = { fontWeight: 700, color: col('accent') };
const header = {
  elements: [
    head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
    head('verso-title', '{title}', 'even', 'top-left', OUTER + 9),
    head('recto-title', '{chapterTitle}', 'odd', 'top-right', -(OUTER + 9)),
    head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
  ],
};
const footer = { // the opener's drop folio, centred under the text block
  elements: [head('drop-folio', '{pageNumber}', 'all', 'top', 0, { ...folio, pages: 'opener',
    placement: { anchor: { to: 'container', edge: 'top' }, offset: { x: mm(0), y: mm(8) } } })],
};
// #endregion

const config = {
  locale: t({ en: 'en-us', es: 'es' }), // hyphenation, and the name of the Example type
  resourceTypes: defaultResourceTypes(LANG), // the engine adds Example (Ejemplo) in locale
  colorPalette,
  page: {
    sizePreset: 'custom', width: mm(210), height: mm(280), dpi: 150,
    margins: { top: mm(22), bottom: mm(22), left: mm(20), right: mm(OUTER), mirror: true },
  },
  layout: { layoutType: 'double', gutterWidth: mm(7) },
  bodyText: {
    fontFamily: TEXT, fontSize: pt(10.8), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('accent'),
    textAlign: 'justify', firstLineIndent: mm(4), indentAfterHeading: false,
    hyphenation: { enabled: true }, optimalLineBreaking: true,
    avoidWidows: true, avoidOrphans: true, avoidRunts: true,
  },
  headings: {
    fontFamily: DISPLAY, color: col('ink'), fontWeight: 700,
    levels: [
      { level: 1, span: 'page', fontSize: pt(36), breakBefore: { parity: 'odd' },
        advancedDesign: opener, marginBottom: pt(0) },
      { level: 2, fontSize: pt(13.5), lineHeight: pt(LEAD), marginTop: pt(LEAD),
        marginBottom: pt(LEAD / 2) },
    ],
  },
  unorderedLists: { bulletChar: '–', color: col('accent'),
    marginTop: pt(0), marginBottom: pt(0) },
  orderedLists: { numberFormat: 'arabic', color: col('accent'), fontWeight: 700,
    marginTop: pt(0), marginBottom: pt(0) },
  paragraphStyles: [
    { id: 'colophon', fontFamily: LABEL, fontSize: pt(7.5), lineHeight: pt(10),
      color: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) },
  ],
  calloutStyles: [
    { id: 'rule', title: t({ en: 'Rule', es: 'Regla' }), background: col('tint'),
      stripe: { enabled: true, side: 'top', width: pt(2.5), color: col('accent') },
      padding: { top: mm(3), right: mm(3.5), bottom: mm(3), left: mm(3.5) },
      titleStyle: { ...label, fontSize: pt(8), fontWeight: 700, letterSpacing: pt(1.2),
        color: col('accent') },
      body: { fontFamily: TEXT, fontSize: pt(10), lineHeight: pt(13), color: col('ink'),
        firstLineIndent: pt(0), boldColor: col('ink'), italicColor: col('ink') } },
  ],
  captionStyle: { fontFamily: LABEL, fontSize: pt(8.4), color: col('ink'), gap: mm(1.5),
    labelBold: true, labelColor: col('accent'), note: { color: col('muted'), gap: mm(0.5) } },
  scoreStyle,
  scoreStyles,
  header,
  footer,
};

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "Four-Part Harmony"
---

# Cadences {kicker="Chapter 4" deck="Where a phrase comes to rest, and how the four voices take it there"}

A phrase of music breathes out at its end, and the chord that closes it tells the listener how far the music has come to rest. That closing formula is the cadence. Students who can already connect any two chords in four parts tend to treat the last two chords of a phrase like any others, and their exercises end without conviction. This chapter slows down at the end of the phrase. It names the five cadences of the major key, shows how each one is voiced, and follows them through two chorale melodies that every harmony class sings: *Nun danket alle Gott* and the Old Hundredth.

:ref{id="ex-cadences" style="full"} sets the five side by side in G major, one to a bar, with the Roman numerals under the bass. Play them at the keyboard before you read on, and listen for how final each one sounds: the order of the bars runs from the most closed to the most open.

::resource{id="ex-cadences"}

## The authentic cadence

An authentic cadence is the progression V–I. It is the strongest close the key has, because the bass falls a fifth (or rises a fourth) from the dominant to the tonic while the leading tone, the seventh degree, rises a semitone to the tonic in one of the upper voices.

We call it **perfect** when both chords are in root position and the soprano ends on the tonic. If either condition fails, the cadence is **imperfect**: the soprano ends on the third or the fifth of the tonic chord, or one of the two chords is inverted. The first two bars of :ref{id="ex-cadences" case="lower"} keep the same bass and change the top: F sharp rising to G in the first, A rising to B in the second.

The leading tone needs care. In the soprano it must rise to the tonic. In an inner voice it may fall a third to the fifth of the tonic chord, so that the final chord is complete, and Bach does this in many of his chorales; but never let it fall in the bass or the soprano. After a dominant seventh the seventh itself (C in G major) resolves down by step. If the leading tone rises as well, the tonic chord that follows has a tripled root and no fifth; letting it fall in an inner voice keeps the chord complete. The last bar of :ref{id="ex-nun-danket" case="lower"} takes the second way: the tenor holds C from the chord before and falls to B, the alto lets F sharp drop to D, and the soprano sings A–G over the bass D–G.

::resource{id="ex-nun-danket"}

## The plagal cadence

The progression IV–I is the plagal cadence. It has no leading tone, and its effect is gentler; hymn books use it for the *Amen* after a final authentic cadence. In four parts the tonic is the common tone of the two chords and stays in the same voice, while the other two upper voices move down by step, the sixth degree to the fifth and the fourth to the third, as in the fourth bar of :ref{id="ex-cadences" case="lower"}.

The first line of *Nun danket alle Gott* ends this way in our setting. The soprano repeats E over C and then falls to D over G (bars 1–2 of :ref{id="ex-nun-danket" case="lower"}). The cadence comes on the downbeat and the fermata is short, so the singers take a breath and go on: a plagal close in the middle of a hymn reads as a comma, not a full stop.

## The half cadence

A phrase that stops on the dominant ends with a **half cadence**. Any chord may lead to it, most often I, IV or ii, and the dominant is always in root position. The ear hears a question waiting for its answer, which is why the half cadence closes the first half of so many tunes and almost never the last line. In the third line of *Nun danket alle Gott* (:ref{id="ex-nun-danket-3" case="lower"}) the tonic in first inversion leads to V under a soprano that falls from B to A.

::resource{id="ex-nun-danket-3"}

Many half cadences approach the dominant through the cadential six-four: the tonic chord with its fifth in the bass, on a strong beat, followed by the dominant on the same bass note. The sixth and the fourth above the bass fall by step to the fifth and the third. Treat the six-four as an ornament of the dominant, not as a tonic, and double its bass.

## Four cadences in one tune

The Old Hundredth, printed with Psalm 134 in the Genevan Psalter of 1551, closes its four lines on three kinds of cadence. :ref{id="ex-old-hundredth" style="full"} sets it complete, barred in 2/2, with the long and short notes of the Genevan rhythm.

::resource{id="ex-old-hundredth"}

- **Line 1** ends on B over G: an imperfect authentic cadence, because the soprano stops on the third.
- **Line 2** ends with the cadential six-four and V, a half cadence with the suspension in the soprano.
- **Line 3** ends F sharp–G over D–G: a perfect authentic cadence with the leading tone resolving in the soprano. Coming before the last line, it is answered by a stronger close.
- **Line 4** descends B–A–G over the cadential six-four, V and I: the most complete close of the four.

Sing the bass while a classmate plays the upper voices: the bass line alone tells you which cadence is coming.

## The deceptive cadence

When V moves to vi instead of I, the cadence is **deceptive**. The leading tone still rises to the tonic, but the bass climbs a step to the sixth degree, and the phrase that seemed finished has to go on. Double the third of vi (the tonic of the key) so that the leading tone can resolve and the other voices fall without parallel octaves.

:::callout{type="rule" title="Before you write the cadence"}
- Decide the cadence from the melody first: the last two notes of the soprano allow only some of them.
- Put the dominant in root position for authentic, half and deceptive cadences.
- Resolve the leading tone up in the outer voices, and the seventh of V^7^ down.
- Check the outer voices for parallel and hidden fifths and octaves.
:::

Composers use the deceptive cadence to lengthen a phrase. :ref{id="ex-deceptive" style="full"} sings the last line of the Old Hundredth twice: the first time it closes on vi, an E minor chord under the tonic note, and the second time on the perfect cadence of :ref{id="ex-old-hundredth" case="lower"}.

::resource{id="ex-deceptive"}

## In the minor key

Everything in this chapter holds in minor, with one condition: the leading tone is not in the key signature. In A minor the dominant chord is E–G sharp–B, and the sharp has to be written each time; without it the chord is minor and the authentic cadence loses its pull. In the deceptive cadence the raised leading tone rises to the tonic and the sixth degree, F, sits a semitone above the dominant in the bass, so the alto or the tenor must avoid the augmented second G sharp–F.

Two cadences belong to the minor mode. A chorale in minor often closes on a major tonic chord, raising its third at the last moment: this is the *tierce de Picardie*, which Bach uses at the end of most of his minor chorales. And a half cadence may approach the dominant from the first inversion of iv, with the bass falling a semitone from the sixth degree to the fifth: the Phrygian half cadence, common at the end of a slow movement before a fast one.

## Exercises

1. Write the five cadences of :ref{id="ex-cadences" case="lower"} in D major and in F major, in close and in open position.
2. Harmonize the first line of the Old Hundredth with a perfect authentic cadence in place of the imperfect one. Which note of the melody must change?
3. Find the cadence at the end of each line in three chorales of your choice and label it.
4. Rewrite the last two bars of :ref{id="ex-old-hundredth" case="lower"} with a plagal *Amen* after the final chord.

:::paragraphs{style="colophon"}
*Four-Part Harmony*, a method for the first years of the conservatory · Set in Crimson Pro, Playfair Display and Fira Sans Condensed (SIL OFL) · Music engraved in Bravura · Melodies in the public domain; harmonizations and text original, CC BY 4.0.
:::
`; // content.<lang>.md, inlined by the Cookbook
// Caption, credit note ('-' for none) and alt text of each example, one block per example.
const CAPTIONS = Object.fromEntries((String.raw`ex-cadences
The five cadences in G major: perfect and imperfect authentic, half, plagal, deceptive
-
Five two-chord cadences in G major for four voices on two staves, one per bar, with Roman numerals under the bass: V–I twice, I–V, IV–I and V–vi.

ex-nun-danket
*Nun danket alle Gott*, lines 1 and 2: a plagal close, then a perfect cadence
Melody: Johann Crüger, 1647, as in Bach’s BWV 386, in G · Setting: original
The first two lines of the chorale Nun danket alle Gott in G major for four voices, the first ending IV–I and the second ending V7–I.

ex-nun-danket-3
*Nun danket alle Gott*, line 3: a half cadence
-
The third line of Nun danket alle Gott in G major for four voices, ending on the dominant chord.

ex-old-hundredth
The Old Hundredth: an imperfect authentic cadence, a half cadence and two perfect ones
Melody: Genevan Psalter, 1551, attributed to Louis Bourgeois · Setting: original
The Old Hundredth complete in G major for four voices on two staves, thirteen bars in 2/2 with a fermata at the end of each of its four lines.

ex-deceptive
The last line of the Old Hundredth, closing first on vi, then on I
-
The last line of the Old Hundredth sung twice in four parts: the first time the dominant moves to an E minor chord, the second time to the G major tonic.
`).trim().split(/\n\s*\n/)
  .map((block) => block.split('\n').map((line) => line.trim()))
  .map(([id, caption, note, alt]) => [id, [caption, note === '-' ? undefined : note, alt]]));

// #region humdrum: aligned columns become tab-separated **kern, the Spanish numerals uppercase
// Each example lists its spines as Humdrum wants them, lowest first: the bass staff split into
// tenor and bass, the **harm spine (printed under that staff), the treble staff split into
// soprano and alto. Writing them with spaces keeps the pen free of tab characters.
const kern = (meter, body) => [
  '**kern **harm **kern', '*clefF4 * *clefG2', '*k[f#] * *k[f#]', `*M${meter} * *M${meter}`,
  '*^ * *^', ...body.trim().split('\n'), '== == == == ==', '*- *- *- *- *-',
].map((row) => row.trim().split(/ +/)
  // Spanish harmony books write every degree in capitals: vi is VI, iib is IIb.
  .map((cell) => (LANG === 'es' && /^(?:i{1,3}|iv|vi{0,2})[bc7]?$/.test(cell)
    ? cell.replace(/^[iv]+/, (degree) => degree.toUpperCase()) : cell))
  .join('\t')).join('\n');
// #endregion

// #region art-scores: the five examples; spines: tenor bass | harm | soprano alto
const CADENCES = kern('2/2', `
  2A   2D    V    2f#  2d
  2B   2GG   I    2g   2d
  =||  =||   =||  =||  =||
  2d   2D    V    2a   2f#
  2d   2GG   I    2b   2g
  =||  =||   =||  =||  =||
  2d   2GG   I    2b   2g
  2d   2D    V    2a   2f#
  =||  =||   =||  =||  =||
  2c   2C    IV   2g   2e
  2B   2GG   I    2g   2d
  =||  =||   =||  =||  =||
  2A   2D    V    2f#  2d
  2G   2E    vi   2g   2B`);

const NUN_DANKET = kern('4/4', `
  4B   4GG   I    4dd  4g
  =    =     =    =    =
  4A   4D    V    4dd  4f#
  4G   4BB   Ib   4dd  4g
  4c   4AA   ii   4ee  4e
  4G   4C    IV   4ee  4e
  =    =     =    =    =
  2B   2GG;  I    2dd; 2g
  4r   4ry   .    4r   4ry
  4d   4GG   I    4b   4g
  =    =     =    =    =
  4e   4C    IV   4cc  4g
  4d   4GG   I    4b   4g
  4A   4FF#  Vb   4a   4d
  8G   8GG   I    8b   8d
  8G   8EE   IVb  8cc  8c
  =    =     =    =    =
  2c   4C    iib  2a   4e
  .    4D    V7   .    4f#
  4B   4GG;  I    4g;  4d`);

const NUN_DANKET_3 = kern('4/4', `
  4c   4AA   ii   4a   4e
  =    =     =    =    =
  4A   4C    iib  4a   4e
  4d   4D    V    4a   4f#
  4d   4GG   I    4b   4g
  4d   4BB   Ib   4b   4g
  =    =     =    =    =
  2d   2D;   V    2a;  2f#
  4r   4ry   .    4r   4ry`);

const OLD_HUNDREDTH = kern('2/2', `
  2B   2G    I    2g   2d
  4B   4E    vi   4g   4e
  4B   4BB   iii  4f#  4d
  =    =     =    =    =
  4G   4C    IV   4e   4c
  4G   4GG   I    4d   4B
  2G   2E    vi   2g   2B
  =    =     =    =    =
  2F#  2D    V    2a   2d
  2G   2GG;  I    2b;  2d
  =||  =||   =||  =||  =||
  2G   2GG   I    2b   2d
  4G   4E    vi   4b   4e
  4G   4D    Ic   4b   4g
  =    =     =    =    =
  4c   4C    iib  4a   4e
  4G   4BB   Ib   4g   4d
  2G   2E    IVb  2cc  2g
  =    =     =    =    =
  2d   2D    Ic   2b   2g
  2d   2D;   V    2a;  2f#
  =||  =||   =||  =||  =||
  2B   2GG   I    2g   2d
  4A   4FF#  Vb   4a   4d
  4G   4GG   I    4b   4d
  =    =     =    =    =
  4E   4C    iib  4a   4e
  4G   4BB   Ib   4g   4d
  2G   2E    IVb  2e   2c
  =    =     =    =    =
  2A   2D    V    2f#  2d
  2B   2GG;  I    2g;  2d
  =||  =||   =||  =||  =||
  2G   2BB   Ib   2dd  2d
  2G   2GG   I    2b   2d
  =    =     =    =    =
  2G   2C    IV   2g   2e
  4c   4C    iib  4a   4e
  4c   4AA   ii   4cc  4e
  =    =     =    =    =
  2G   2D    Ic   2b   2d
  2F#  2D    V    2a   2d
  =    =     =    =    =
  1G   1GG;  I    1g;  1B`);

// The last line twice: the first time V moves to vi (E minor under the tonic note).
const LAST_LINE = (bass, degree) => `
  2G   2BB   Ib   2dd  2d
  2G   2GG   I    2b   2d
  =    =     =    =    =
  2G   2C    IV   2g   2e
  4c   4C    iib  4a   4e
  4c   4AA   ii   4cc  4e
  =    =     =    =    =
  2G   2D    Ic   2b   2d
  2F#  2D    V    2a   2d
  =    =     =    =    =
  1G   ${bass}  ${degree}  1g;  1B`;
const DECEPTIVE = kern('2/2', `${LAST_LINE('1E;', 'vi')}
  =  =  =  =  =${LAST_LINE('1GG;', 'I')}`);
// #endregion

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = {
  'Crimson Pro': ['400', '400i', '600', '700'], // text, and the numerals under the music
  'Playfair Display': ['400', '700'], // display: chapter title, section heads
  'Fira Sans Condensed': ['400', '600', '700'], // labels: kicker, heads, captions, the box
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
// #region build: start the engraver, then set chapter 4 of the method from page 61
await initMusicEngine(); // until it runs, every score is laid out as a grey box
const continuation = { pageNumbering: { startAt: 61 }, // odd: the opener is a recto
  headings: { h1: 3, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 } }; // the next # is chapter 4
const doc = await buildDocumentWithFonts({ markdown, resources: resources(), continuation },
  config, kitFonts(FONTS));
showPages(doc, { title: t({ en: 'Numbered chorale examples in a harmony method',
  es: 'Corales numerados en un método de armonía' }) });
// #endregion

// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ─────

// ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook
function mm(value) { return { value, unit: 'mm' }; }
function pt(value) { return { value, unit: 'pt' }; }
function em(value) { return { value, unit: 'em' }; }
/** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */
function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; }
/** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */
function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; }

// ─── Kit · fonts v3 ── the same in every recipe · postext.dev/cookbook
// Postext loads the faces a config asks for before layout (prepareFonts,
// buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the
// files the PDF embeds too.

/** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts:
 *  the Fontsource resolver, and the faces of FONTS loaded with the ones the
 *  config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */
function kitFonts(FONTS = {}) {
  const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) =>
    ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' })));
  return { resolve: fontsourceResolver, faces };
}

/** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek
 *  when `text` needs them and the family ships them (kitSubsetsFor), each with
 *  its unicode-range. Null for a weight or slant Fontsource does not ship. */
async function fontsourceResolver(family, weight, style, { text = '' } = {}) {
  const id = fontsourceId(family);
  const meta = await fontsourceMeta(family);
  if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null;
  kitStatus('Loading fonts…');
  const at = await fontsourceAt(id);
  return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({
    source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`,
    unicodeRange: kitRanges()[subset],
  }));
}

/** The unicode-range of each Fontsource file the kit loads (a function: the
 *  kit is inlined after the pen's code, which runs before a const would be set). */
function kitRanges() {
  return {
    latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,'
      + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD',
    'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,'
      + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF',
    greek: 'U+0370-03FF',
  };
}

/** For the cjk, arabic and comics blocks, which add their own files: the
 *  faces of `faces` = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample:
 *  č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With
 *  `optional`, a face Fontsource does not ship is skipped instead of failing.
 *  The files are added once all have loaded, in the order asked (latin, then
 *  latin-ext, then greek), whatever order they arrive in: the last one added
 *  wins a character two files hold. Resolves to the number of faces added. */
async function loadFonts(faces, text = '', { optional = false } = {}) {
  kitStatus('Loading fonts…');
  const ranges = kitRanges();
  const jobs = [];
  for (const [family, specs] of Object.entries(faces)) {
    const id = fontsourceId(family);
    const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal'])
      .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await
    const at = fontsourceAt(id);
    const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null;
    const subsets = ['latin', ...kitSubsetsFor(text, meta)];
    for (const [weight, style] of todo) {
      if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue;
      for (const subset of subsets) {
        const url = `${await at}files/${id}-${subset}-${weight}-${style}.woff2`;
        const face = new FontFace(family, `url(${url}) format('woff2')`,
          { weight: String(weight), style, unicodeRange: ranges[subset] });
        jobs.push(face.load().catch(() => {
          if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`);
        }));
      }
    }
  }
  const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; });
  return ready.filter((face) => face && document.fonts.add(face)).length;
}

/** A loaded FontFace covers this family, weight and style (fonts.check() would
 *  also say yes for families nobody declared). */
function hasFace(family, weight, style) {
  for (const face of document.fonts) {
    if (face.status !== 'loaded' || face.style !== style) continue;
    if (face.family.replace(/^["']|["']$/g, '') !== family) continue;
    const [low, high = low] = face.weight.split(' ').map(Number);
    if (weight >= low && weight <= high) return true;
  }
  return false;
}

/** The files beyond latin `text` needs that `meta`'s family ships. */
function kitSubsetsFor(text, meta) {
  return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']]
    .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x);
}

/** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */
function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); }

/** A package's folder at the one version @5 names now (#766). */
function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) {
  return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json())
    .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`);
}

/** The family's Fontsource metadata (weights, styles, subsets), or null. */
function fontsourceMeta(family) {
  fontsourceMeta.cache ??= new Map();
  const id = fontsourceId(family);
  if (!fontsourceMeta.cache.has(id)) {
    fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`)
      .then((res) => (res.ok ? res.json() : null), () => null));
  }
  return fontsourceMeta.cache.get(id);
}

// ─── Kit · viewer v2 ── the same in every recipe · postext.dev/cookbook
/** The pages as spreads on a dark desk, page 1 alone, then verso | recto,
 *  each painted when it scrolls near. Other options go to renderPageToCanvas. */
function showPages(docs, { title, width = 460, ...render } = {}) {
  const root = viewer(title);
  const pages = [docs].flat().flatMap((doc) =>
    doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index })));
  const spreads = [];
  let verso = null;
  for (const p of pages) {
    if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; }
    else { spreads.push([verso, p]); verso = null; }
  }
  if (verso) spreads.push([verso, null]);
  const density = Math.min(window.devicePixelRatio || 1, 2);
  showPages.painter?.disconnect();
  const painter = new IntersectionObserver((entries) => {
    for (const { isIntersecting, target } of entries) {
      if (!isIntersecting) continue;
      painter.unobserve(target);
      const { doc, page } = target.postext;
      renderPageToCanvas(page, doc, target, { ...render, scale: (width * density) / page.width });
    }
  }, { rootMargin: '800px' });
  showPages.painter = painter;
  root.replaceChildren(...spreads.map((pair) => {
    const spread = document.createElement('div');
    spread.className = 'pt-spread';
    for (const p of pair) {
      const figure = document.createElement('figure');
      if (p) {
        const label = p.page.pageLabel || String(p.n + 1);
        const canvas = document.createElement('canvas');
        canvas.postext = p;
        canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`;
        canvas.setAttribute('role', 'img');
        canvas.setAttribute('aria-label', `Page ${label}`);
        const folio = document.createElement('figcaption');
        folio.textContent = label;
        figure.append(canvas, folio);
        painter.observe(canvas);
      } else figure.className = 'pt-blank';
      spread.append(figure);
    }
    return spread;
  }));
  kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`);
  document.documentElement.dataset.postext = 'ready';
  return pages.length;
}

/** The desk, the bar and the error reporting, created once. */
function viewer(title) {
  if (!document.getElementById('pt-kit')) {
    document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit">
      :root { color-scheme: dark; }
      body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; }
      #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center;
        gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px);
        border-bottom: 1px solid #23262d; }
      #pt-bar strong { color: #f4f1ea; font-weight: 600; }
      #pt-actions { display: flex; gap: 12px; margin-left: auto; }
      #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
      #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; }
      .pt-spread { display: flex; }
      .pt-spread figure { margin: 0; width: min(460px, 44vw); }
      .pt-spread canvas { display: block; width: 100%; background: #fff;
        box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); }
      .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); }
      .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif;
        letter-spacing: .18em; text-transform: uppercase; color: #6c7079; }
      .pt-blank { visibility: hidden; }
      @media (max-width: 760px) {
        .pt-spread { flex-direction: column; gap: 32px; }
        .pt-spread figure { width: min(460px, 92vw); }
        .pt-blank { display: none; }
      }
    </style>`);
    document.body.insertAdjacentHTML('afterbegin',
      '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>');
    document.getElementById('pt-title').textContent = document.title || 'Postext';
    addEventListener('error', (event) => kitFail(event.error ?? event.message));
    addEventListener('unhandledrejection', (event) => kitFail(event.reason));
  }
  if (title) document.getElementById('pt-title').textContent = title;
  return document.getElementById('pages')
    ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' }));
}

function kitStatus(text) {
  viewer();
  document.getElementById('pt-status').textContent = text;
}

function kitFail(error) {
  document.documentElement.dataset.postext = 'error';
  kitStatus(`Error: ${error?.message ?? error}`);
}

// ─── /Kit ───────────────────────────────────────────────────────────────────────
```

## 变化

### 在正文中放一个不编号的谱例

用代码块写的乐谱留在正文里，正文并不引用它，像独立的公式一样：没有编号，也没有图注。

```diff
- ::resource{id="ex-cadences"}
+ ```humdrum {style="column" alt="G大调的五种终止式"}
+ **kern	**harm	**kern
+ …
+ ```
```

### 改称“音乐谱例”

为另一个名称定义自己的类型，并让`typeId`指向它。

```diff
- resourceTypes: defaultResourceTypes(LANG),
+ resourceTypes: [...defaultResourceTypes(LANG), { id: 'musex', name: '音乐谱例',
+   shortLabel: '谱例', captionPrefix: '音乐谱例', numberingTemplate: '{h1}.{n}',
+   resetOn: 'h1', counterFormat: 'decimal' }],
```

## 常见问题

- **::resource{id="…"}只接受双引号.** 块级嵌入只有写成带双引号的::resource{id="…"}才能被识别；其他写法会作为一行可见文字留在正文中。

- 乐谱资源从不在栏与栏、页与页之间断开。浮动谱例要限制在几个小节之内；较长的曲子放进代码块，它会在行与行之间断开。
- 刻谱器把每行谱填满到所在位置的宽度，最后一行保留自然宽度。如果某个谱例把一个小节单独留在第二行，就调低其样式的`spacing`或谱表大小。

## 致谢

- 食谱: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- 文字: Chapter text and the four-part settings of every example: Ignacio Ferro, CC-BY-4.0
- 文字: Melody of Nun danket alle Gott (Praxis pietatis melica, 1647), read from the soprano of Bach's setting BWV 386: Johann Crüger ([来源](https://github.com/craigsapp/bach-370-chorales/blob/master/kern/chor032.krn)), 公有领域
- 文字: Melody of the Old Hundredth (Psalm 134 in the Genevan Psalter, 1551): Louis Bourgeois (attributed) ([来源](https://en.wikipedia.org/wiki/Old_100th)), 公有领域
- 字体: Crimson Pro (OFL-1.1), Playfair Display (OFL-1.1), Fira Sans Condensed (OFL-1.1)
- 代码: MIT · 示例内容: CC-BY-4.0

## 相关食谱

- [No. 009 · 浮动到引用处的图](https://postext.dev/zh/cookbook/figures-float-where-cited.md): 一个双栏章节，含七幅编号的图：六幅从首次:ref处浮动到其位置设置允许的第一个空位，一幅放在::resource指定的地方。 · 难度 3 (高级) · 教科书
- [No. 163 · 用ABC写谱例的和声教程](https://postext.dev/zh/cookbook/harmony-textbook-abc-examples.md): 和声教程中讲终止式的一章：谱例是写在Markdown中的ABC代码块，按栏宽刻印成谱，其中最长的一例像段落一样从一页接排到下一页。 · 难度 2 (中级) · 教科书
- [No. 001 · 带边栏的教材](https://postext.dev/zh/cookbook/textbook-margin-column.md): 一栏半的页面，外侧栏只放浮动体：span 'side'的图和术语注在那里依次堆叠，captionSide把其余图的题注移进侧栏。 · 难度 3 (高级) · 教科书
