# أمثلة كورالية مرقّمة في منهج للهارموني بعمودين

> كورالات رباعية الأصوات مكتوبة بصيغة Humdrum **kern تصير موارد نوتة من نوع example: مرقّمة «مثال 4.1»، يُحال إليها بـ :ref وتطفو كما تطفو الأشكال.

- نسخة HTML: https://postext.dev/ar/cookbook/conservatory-method-chorales
- وصفة رقم 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)
- افتح في Sandbox: https://postext.dev/ar/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), [zh](https://postext.dev/zh/cookbook/conservatory-method-chorales.md), [ja](https://postext.dev/ja/cookbook/conservatory-method-chorales.md)

## باختصار

فصل عن القفلات من كتاب هارموني لطلاب المعهد الموسيقي، في عمودين. أمثلته الموسيقية الخمسة تُكتب نصًّا، وتُرسم نوتةً حقيقية، وتُرقَّم ويُكتب لها تعليق كما للأشكال، وتوضع قرب الفقرة التي تذكرها.

## ما الذي ستنضده

الفصل الرابع من *Four-Part Harmony*، وهو منهج للسنوات الأولى في المعهد الموسيقي، على صفحة بمقاس 210 × 280 mm في عمودين بخط Crimson Pro. موضوع الدرس القفلات، وأمثلته الخمسة كورالات رباعية الأصوات على مدرجين، تحت صوت الباص أرقام رومانية للدرجات: القفلات الخمس في سلّم صول الكبير، وثلاثة أسطر من *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/ar/docs/music.md#أمثلة-موسيقية-مرقّمة): موارد من نوع score بالنوع example: تُرقَّم بلغة المستند (مثال 3.2)، ولها تعليق، ويُستشهد بها بـ:ref، وتوضع في مكانها أو تطفو كالأشكال؛ ويبقى كل منها كاملًا.
- [موضع الأشكال](https://postext.dev/ar/docs/document-format-resources.md#الموضع): في الأعلى أو الأسفل أو تلقائيًا أو هنا، داخل عمود أو عبر الصفحة، بجزء من العرض، لكل مورد أو لكل نوع؛ والعناصر العائمة من تسلسل واحد لا يتجاوز بعضها بعضًا أبدًا.
- [استشهادات تضع الأشكال](https://postext.dev/ar/docs/document-format-resources.md#الإحالة-داخل-السطر-الصيغة-الأساسية): يستشهد :ref بمورد («انظر الشكل 3.2»)، وأول استشهاد به يحدّد موضعه: يطفو الشكل إلى أول موضع فارغ بعده.

**تستخدم أيضًا**

- [التدوين الموسيقي](https://postext.dev/ar/docs/music.md#مدوّنة-داخل-النص)
- [نمط المدوّنة الموسيقية](https://postext.dev/ar/docs/configuration-resources.md#نمط-المدوّنة-الموسيقية)
- [تعليقات مرقّمة](https://postext.dev/ar/docs/document-format-resources.md#الترقيم-بحسب-الإحالة-الأولى)
- [«شكل» و«جدول» بلغتك](https://postext.dev/ar/docs/configuration-resources.md#أنواع-الموارد)
- [أنواع موارد مخصّصة](https://postext.dev/ar/docs/configuration-resources.md#أنواع-الموارد)
- [نمط التعليق](https://postext.dev/ar/docs/configuration-resources.md#نمط-التعليقات)
- [سطور المصدر والحقوق](https://postext.dev/ar/docs/configuration-resources.md#نمط-التعليقات)
- [عمود واحد أو عمودان](https://postext.dev/ar/docs/configuration-page-layout.md#أنواع-التخطيط)
- [صفحات افتتاح مصمَّمة](https://postext.dev/ar/docs/configuration-headings.md#الامتداد-والتصميم-المتقدم)
- [سمات العنوان](https://postext.dev/ar/docs/document-format.md#سمات-العناوين)
- [الترويسات وأرقام الصفحات](https://postext.dev/ar/docs/configuration-headers-footers.md#الترويسات-والتذييلات)
- [هوامش متناظرة](https://postext.dev/ar/docs/configuration-page-layout.md#الهوامش-المتناظرة)
- [الترويسات بحسب دور الصفحة](https://postext.dev/ar/docs/configuration-headers-footers.md#عناصر-النص)
- [إطارات التنبيه](https://postext.dev/ar/docs/configuration-callouts.md#أنماط-الإطارات)
- [أنماط الفقرات](https://postext.dev/ar/docs/configuration-styles.md#أنماط-الفقرات)
- [لوحة ألوان دلالية](https://postext.dev/ar/docs/configuration-fonts-colors-viewers.md#لوحة-الألوان)
- [أشكال في هذا الموضع بالضبط](https://postext.dev/ar/docs/document-format-resources.md#التضمين-في-كتلة-اختياري-لوضع-صريح-داخل-التدفق)
- [شريط فصل بعرض الصفحة](https://postext.dev/ar/docs/configuration-headings.md#الامتداد-والتصميم-المتقدم)
- [النص المرتفع والمنخفض](https://postext.dev/ar/docs/document-format-inline.md#التنسيق-داخل-السطر)

**الإعدادات في لمحة**

- [`bodyText`](https://postext.dev/ar/docs/configuration-text.md#نص-المتن), [`calloutStyles`](https://postext.dev/ar/docs/configuration-callouts.md#أنماط-الإطارات), [`captionStyle`](https://postext.dev/ar/docs/configuration-resources.md#نمط-التعليقات), [`colorPalette`](https://postext.dev/ar/docs/configuration-fonts-colors-viewers.md#لوحة-الألوان), [`footer`](https://postext.dev/ar/docs/configuration-headers-footers.md#الترويسات-والتذييلات), [`header`](https://postext.dev/ar/docs/configuration-headers-footers.md#الترويسات-والتذييلات), [`headings`](https://postext.dev/ar/docs/configuration-headings.md#العناوين), [`layout`](https://postext.dev/ar/docs/configuration-page-layout.md#التخطيط), [`locale`](https://postext.dev/ar/docs/configuration-language.md#تقسيم-الكلمات-بالواصلة), [`orderedLists`](https://postext.dev/ar/docs/configuration-lists-math.md#القوائم-المرقّمة), [`page`](https://postext.dev/ar/docs/configuration-page-layout.md#الصفحة), [`paragraphStyles`](https://postext.dev/ar/docs/configuration-styles.md#أنماط-الفقرات), [`resourceTypes`](https://postext.dev/ar/docs/configuration-resources.md#أنواع-الموارد), [`scoreStyle`](https://postext.dev/ar/docs/configuration-resources.md#نمط-المدوّنة-الموسيقية), [`scoreStyles`](https://postext.dev/ar/docs/configuration-resources.md#نمط-المدوّنة-الموسيقية), [`unorderedLists`](https://postext.dev/ar/docs/configuration-lists-math.md#القوائم-غير-المرقّمة)

**واجهات API**

- [`buildDocumentWithFonts`](https://postext.dev/ar/docs/configuration-programmatic-usage.md#تحميل-الخطوط-قبل-الإخراج), [`defaultResourceTypes`](https://postext.dev/ar/docs/configuration-resources.md#أنواع-الموارد), [`initMusicEngine`](https://postext.dev/ar/docs/music.md#مدوّنة-داخل-النص), [`renderPageToCanvas`](https://postext.dev/ar/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/ar/cookbook/conservatory-method-chorales.md#الجواب-المختصر) الموارد الخمسة. `typeId: 'example'` يأخذ نوع «مثال» المدمج، الذي يضيفه المحرّك إلى أنواع موارد الكتاب حين يستعمله مورد، باسم بلغة المستند (`locale: 'es'` يعطي Ejemplo) وبترقيم لكل فصل، فيكون أول مثال في الفصل الرابع 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.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">` في صفحة فارغة أو الصقه في لوحة JS في مثال CodePen جديد (وحدةً). يستورد postext من esm.sh، فلا حاجة إلى تثبيت أو بناء.

- مجلد الوصفة: 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="القفلات الخمس في صول الكبير"}
+ **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

## وصفات ذات صلة

- [رقم 009 · أشكال تطفو إلى حيث تستشهد بها](https://postext.dev/ar/cookbook/figures-float-where-cited.md): فصل بعمودين فيه سبعة أشكال مرقّمة: ستة تطفو من أول :ref لها إلى أول موضع يسمح به موضعها، وواحد يقف حيث يضعه ::resource. · المستوى 3 (متقدم) · الكتب المدرسية
- [رقم 163 · كتاب في الهارموني بأمثلة موسيقية مكتوبة بلغة ABC](https://postext.dev/ar/cookbook/harmony-textbook-abc-examples.md): فصل عن القفلات في كتاب للهارموني، أمثلته كتل ABC داخل Markdown تُنقش بعرض العمود، ويمتد أطولها على صفحتين. · المستوى 2 (متوسط) · الكتب المدرسية
- [رقم 001 · كتاب مدرسي بعمود في الهامش](https://postext.dev/ar/cookbook/textbook-margin-column.md): صفحة بعمود ونصف ليس في عمودها الخارجي إلا العناصر العائمة: تتراكم فيه الأشكال ذات span 'side' والحواشي الشارحة، وينقل captionSide إليه بقية التعليقات. · المستوى 3 (متقدم) · الكتب المدرسية
