# نص واحد بأربعة أساليب استشهاد

> مقالة قصيرة واحدة فيها ستة استشهادات، تُبنى بأساليب APA 7 وشيكاغو مؤلف-تاريخ وIEEE وISO 690: إعداد الأسلوب هو الشيء الوحيد الذي يتغير.

- نسخة HTML: https://postext.dev/ar/cookbook/one-text-four-citation-styles
- وصفة رقم 093 · بنية الكتاب · المستوى 2 (متوسط) · المخرجات: Canvas
- الأنواع: الأوراق البحثية والأكاديمية, التقارير
- تتطلب postext ≥ 1.12.0 · اختُبرت مع 1.12.0 بتاريخ 2026-10-01
- الصفحات: [1](https://postext.dev/cookbook/one-text-four-citation-styles/en/p01.webp?v=10048327), [1](https://postext.dev/cookbook/one-text-four-citation-styles/en/p02.webp?v=10048327), [1](https://postext.dev/cookbook/one-text-four-citation-styles/en/p03.webp?v=10048327), [1](https://postext.dev/cookbook/one-text-four-citation-styles/en/p04.webp?v=10048327)
- افتح في Sandbox: https://postext.dev/ar/sandbox#recipe=one-text-four-citation-styles&lang=en (.postext: https://postext.dev/cookbook/one-text-four-citation-styles/en/one-text-four-citation-styles.postext)
- آخر تحديث: 2026-10-01
- لغات أخرى: [en](https://postext.dev/en/cookbook/one-text-four-citation-styles.md), [es](https://postext.dev/es/cookbook/one-text-four-citation-styles.md), [ca](https://postext.dev/ca/cookbook/one-text-four-citation-styles.md), [zh](https://postext.dev/zh/cookbook/one-text-four-citation-styles.md)

## باختصار

الفقرتان نفسهما عن فن الطباعة، بالمصادر نفسها، مطبوعتان أربع مرات. كل صفحة تذكر المصادر بطريقة مختلفة، فتتغير الاستشهادات والقائمة في الآخر معها، مع أن النص كُتب مرة واحدة.

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

مقالة من فقرتين عن سبب مرور الطباعة الجيدة دون أن يلاحظها أحد، بستة مصادر: استشهاد سردي، واستشهاد بصفحة، واستشهاد يُسقط المؤلف لأن الجملة تسمّيه، وعملان يُستشهد بهما معًا. يُنضَّد النص أربع مرات على صفحة 155 × 235 مم، والتغيير الوحيد بين البناءات الأربعة هو `citations.style`: APA 7، وشيكاغو مؤلف-تاريخ، وIEEE، وISO 690. تحمل كل صفحة اسم الأسلوب في شريط بلون خاص به، فإذا وُضعت الصفحات متجاورة قُرئت جدول مقارنة. تتغير الاستشهادات في شكلها وترتيبها وترقيمها، وتغيّر القائمة ترتيبها وأسماءها، أما المقالة نفسها فهي الملف ذاته.

**تجيب هذه الوصفة عن:**

- كيف أنقل نصًّا من أسلوب استشهاد إلى آخر دون أن ألمسه؟
- كيف أستشهد بالأعمال وأبني قائمة المراجع بأسلوب APA أو IEEE أو أسلوب استشهاد آخر؟

## الجواب المختصر

```js
// script.js, سطرًا 28–44
// Citations are written once, Pandoc's way: [@key], [@a; @b], [-@key] without the author,
// @key [p. 17] inside the sentence. citeproc-js writes them, and the list, in the style named.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const SHOWN = [ // the same text, built once per entry
  { style: 'apa', band: 'apa', name: 'APA 7', system: t({ en: 'author-date', es: 'autor-año' }) },
  { style: 'chicago-author-date', band: 'chicago', name: 'Chicago',
    system: t({ en: 'author-date', es: 'autor-año' }) },
  { style: 'ieee', band: 'ieee', name: 'IEEE', system: t({ en: 'numbered', es: 'numérico' }) },
  { style: t({ en: 'iso690-author-date-en', es: 'iso690-author-date-es' }), band: 'iso',
    name: 'ISO 690', system: t({ en: 'author-date', es: 'autor-año' }) },
];
const citations = (style) => ({
  style, // 'apa' → '(Warde, 1955)'; 'ieee' → '[1]'; the list is sorted and set to match
  link: true,
  bibliography: { fontSize: em(0.85), lineHeight: pt(11.2), hangingIndent: mm(6),
    entrySpacing: pt(2), labelWidth: mm(6), doi: 'hide' },
});
```

## المكونات

**تعلّم**

- [الاستشهادات بأسلوب توثيق](https://postext.dev/ar/docs/document-format.md#الاستشهادات-وقائمة-المراجع): أعمال يُستشهد بها بصيغة [@key, p. 33] وتُنسَّق بأسلوب CSL (APA، Chicago، MLA، IEEE، Vancouver، ISO 690، GB/T 7714…) يُختار في الإعدادات، وترتبط بمداخلها.

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

- [قائمة المراجع من البيانات المرجعية](https://postext.dev/ar/docs/document-format.md#الاستشهادات-وقائمة-المراجع)
- [صفحات افتتاح مصمَّمة](https://postext.dev/ar/docs/configuration.md#الامتداد-والتصميم-المتقدم)
- [لوحة ألوان دلالية](https://postext.dev/ar/docs/configuration.md#لوحة-الألوان)

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

- [`bodyText`](https://postext.dev/ar/docs/configuration.md#نص-المتن), [`citations`](https://postext.dev/ar/docs/configuration.md#الاستشهادات), [`colorPalette`](https://postext.dev/ar/docs/configuration.md#لوحة-الألوان), [`footer`](https://postext.dev/ar/docs/configuration.md#رؤوس-الصفحات-وتذييلاتها), [`header`](https://postext.dev/ar/docs/configuration.md#رؤوس-الصفحات-وتذييلاتها), [`headings`](https://postext.dev/ar/docs/configuration.md#العناوين), [`layout`](https://postext.dev/ar/docs/configuration.md#التخطيط), [`locale`](https://postext.dev/ar/docs/configuration.md#تقسيم-الكلمات-بالواصلة), [`page`](https://postext.dev/ar/docs/configuration.md#الصفحة)

**واجهات API**

- [`LOCALES`](https://postext.dev/ar/docs/document-format.md#الاستشهادات-وقائمة-المراجع), [`STYLES`](https://postext.dev/ar/docs/document-format.md#الاستشهادات-وقائمة-المراجع), [`buildDocument`](https://postext.dev/ar/docs/configuration.md#بناء-مستند), [`clearMeasurementCache`](https://postext.dev/ar/docs/configuration.md#ذاكرة-القياس-المؤقتة), [`createCiteprocEngine`](https://postext.dev/ar/docs/document-format.md#الاستشهادات-وقائمة-المراجع), [`registerCitationEngine`](https://postext.dev/ar/docs/document-format.md#الاستشهادات-وقائمة-المراجع), [`renderPageToCanvas`](https://postext.dev/ar/docs/configuration.md#رسم-صفحة-في-صورة-نقطية)

**الخطوط**

- Source Serif 4 (OFL-1.1), Source Sans 3 (OFL-1.1)

## طريقة التحضير

### 1 · إعداد واحد

الشيفرة هي [الجواب المختصر](#الجواب-المختصر) أعلاه. تُكتب الاستشهادات مرة واحدة في المحتوى، بصيغة Pandoc، وتقع المراجع في كتلة BibTeX في الآخر. يسمّي `citations.style` أسلوب CSL، ومنه يكتب citeproc-js كل استشهاد وكل مدخل: يضع APA فاصلة قبل السنة ويرتب العملين بحسب المؤلف، ويحتفظ شيكاغو بالترتيب الذي كُتبا به، ويحوّلهما IEEE إلى أرقام بترتيب أول استشهاد، ويضع ISO 690 أسماء العائلات في القائمة بحروف كبيرة. وفي كتاب قيد التأليف هذا هو التغيير كله: قيمة واحدة في الإعداد، والنص لا يُمس.

### 2 · أربعة بناءات وصفحتان متقابلتان

```js
// script.js, سطرًا 147–154
// The style is set for the whole document, so each style is a build of its own.
const docs = [];
for (at = 0; at < SHOWN.length; at++) {
  // pageIndexOffset makes each page the next one of a book, so the desk pairs them:
  // APA beside Chicago, IEEE beside ISO 690.
  const content = { markdown, continuation: { pageIndexOffset: at + 1 } };
  docs.push(await buildWithFonts(() => buildDocument(content, config()), markdown));
}
```

الأسلوب يسري على المستند كله، لذا فالنسخ الأربع أربعة بناءات لـ Markdown نفسه. يأخذ كل منها `pageIndexOffset` أعلى بواحد من سابقه، فتصير صفحته الصفحة التالية من كتاب متخيَّل؛ ثم تجمعها طاولة العرض في صفحتين متقابلتين مرتين، والأسلوبان مؤلف-تاريخ الأقل اختلافًا متجاوران.

### 3 · اسم الأسلوب في شريط

```js
// script.js, سطرًا 48–69
const opener = (s, i) => ({
  enabled: true,
  minHeight: mm(41), // from the top margin to under the title, 63 mm from the trim
  slot: { elements: [
    { kind: 'box', id: 'band', style: { backgroundColor: col(s.band) },
      placement: { anchor: { to: 'page', edge: 'top-left' },
        size: { width: 'fill', height: mm(44) } } },
    { kind: 'text', id: 'kicker', content: `${t({ en: 'Citation style', es: 'Estilo de cita' })}`
      + ` · ${i + 1} / ${SHOWN.length} · ${s.system}`, fontFamily: SANS, fontSize: pt(8),
    fontWeight: 600, letterSpacing: pt(1.4), textTransform: 'uppercase', color: col('paper'),
    align: 'left',
    placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: mm(18), y: mm(12) } } },
    { kind: 'text', id: 'name', content: s.name, fontFamily: SERIF, fontSize: pt(54),
      fontWeight: 600, lineHeight: 1, color: col('paper'), align: 'left', overflow: 'clip',
      placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(5) },
        size: { width: mm(119), height: 'auto' } } },
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: SERIF, fontSize: pt(20),
      fontWeight: 600, lineHeight: 1.15, color: col('ink'), align: 'left', overflow: 'wrap',
      placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: mm(18), y: mm(52) },
        size: { width: mm(119), height: 'auto' } } },
  ] },
});
```

يرسم تصميم العنوان الشريطَ من حدّ القص، والعنوانَ التمهيدي برقم الأسلوب ونظامه، والاسمَ بحجم 54 pt. ويأتي الاسم والنظام من النص البرمجي لا من Markdown، لأن النص واحد في المستندات الأربعة.

```js
// script.js, سطرًا 15–22
const palette = {
  ink: '#1d1b19', muted: '#66605a', rule: '#cfc8bf', paper: '#ffffff',
  apa: '#2f5d8a', chicago: '#8c2f2b', ieee: '#24476b', iso: '#3c6b4f', // the four bands
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
// Each document points main-color at its own band, so any default left follows it.
const colorPalette = (band) => Object.entries({ ...palette, 'main-color': palette[band] })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
```

يوجّه كل مستند `main-color` إلى شريطه، فتكون الألوان الأربعة مداخل مربوطة في لوحة الألوان، وتتبع كل قيمة افتراضية لا يذكرها الإعداد شريطَ صفحتها.

## الوصفة كاملة

ملف واحد، مركّب من مجلد الوصفة ومعه نص المثال وأدوات دليل الوصفات المشتركة مضمّنة؛ وهو ينشئ صفحته بنفسه. لتشغيله، ضعه داخل `<script type="module">` في صفحة فارغة أو الصقه في لوحة JS في مثال CodePen جديد (وحدةً). يستورد postext من esm.sh، فلا حاجة إلى تثبيت أو بناء.

- مجلد الوصفة: https://github.com/drnachio/postext/tree/main/cookbook/one-text-four-citation-styles

### script.js

```js
// ═══ Postext Cookbook · Nº 093 · One text in four citation styles ════════════════
// https://postext.dev/en/cookbook/one-text-four-citation-styles
// Code: MIT · Text: original (CC BY 4.0) · Pictures: none
// Fonts: Source Serif 4, Source Sans 3 (SIL OFL 1.1) · Needs postext ≥ 1.12.0
import {
  buildDocument, renderPageToCanvas, clearMeasurementCache, registerCitationEngine,
} from 'https://esm.sh/postext';
import { createCiteprocEngine, STYLES, LOCALES } from 'https://esm.sh/postext-citeproc';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'one-text-four-citation-styles';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: one ink, and a colour for each style's band
const palette = {
  ink: '#1d1b19', muted: '#66605a', rule: '#cfc8bf', paper: '#ffffff',
  apa: '#2f5d8a', chicago: '#8c2f2b', ieee: '#24476b', iso: '#3c6b4f', // the four bands
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
// Each document points main-color at its own band, so any default left follows it.
const colorPalette = (band) => Object.entries({ ...palette, 'main-color': palette[band] })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
// #endregion
const [SERIF, SANS] = ['Source Serif 4', 'Source Sans 3'];
const LEAD = 14; // pt

// #region answer: the style is one setting; everything else stays the same
// Citations are written once, Pandoc's way: [@key], [@a; @b], [-@key] without the author,
// @key [p. 17] inside the sentence. citeproc-js writes them, and the list, in the style named.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const SHOWN = [ // the same text, built once per entry
  { style: 'apa', band: 'apa', name: 'APA 7', system: t({ en: 'author-date', es: 'autor-año' }) },
  { style: 'chicago-author-date', band: 'chicago', name: 'Chicago',
    system: t({ en: 'author-date', es: 'autor-año' }) },
  { style: 'ieee', band: 'ieee', name: 'IEEE', system: t({ en: 'numbered', es: 'numérico' }) },
  { style: t({ en: 'iso690-author-date-en', es: 'iso690-author-date-es' }), band: 'iso',
    name: 'ISO 690', system: t({ en: 'author-date', es: 'autor-año' }) },
];
const citations = (style) => ({
  style, // 'apa' → '(Warde, 1955)'; 'ieee' → '[1]'; the list is sorted and set to match
  link: true,
  bibliography: { fontSize: em(0.85), lineHeight: pt(11.2), hangingIndent: mm(6),
    entrySpacing: pt(2), labelWidth: mm(6), doi: 'hide' },
});
// #endregion

// #region band: the style's name, large, in a band of its colour
const opener = (s, i) => ({
  enabled: true,
  minHeight: mm(41), // from the top margin to under the title, 63 mm from the trim
  slot: { elements: [
    { kind: 'box', id: 'band', style: { backgroundColor: col(s.band) },
      placement: { anchor: { to: 'page', edge: 'top-left' },
        size: { width: 'fill', height: mm(44) } } },
    { kind: 'text', id: 'kicker', content: `${t({ en: 'Citation style', es: 'Estilo de cita' })}`
      + ` · ${i + 1} / ${SHOWN.length} · ${s.system}`, fontFamily: SANS, fontSize: pt(8),
    fontWeight: 600, letterSpacing: pt(1.4), textTransform: 'uppercase', color: col('paper'),
    align: 'left',
    placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: mm(18), y: mm(12) } } },
    { kind: 'text', id: 'name', content: s.name, fontFamily: SERIF, fontSize: pt(54),
      fontWeight: 600, lineHeight: 1, color: col('paper'), align: 'left', overflow: 'clip',
      placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(5) },
        size: { width: mm(119), height: 'auto' } } },
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: SERIF, fontSize: pt(20),
      fontWeight: 600, lineHeight: 1.15, color: col('ink'), align: 'left', overflow: 'wrap',
      placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: mm(18), y: mm(52) },
        size: { width: mm(119), height: 'auto' } } },
  ] },
});
// #endregion

let at = 0; // the entry of SHOWN being built
const config = () => ({ // a factory: the engine caches resolved configs per object
  locale: t({ en: 'en-us', es: 'es' }),
  colorPalette: colorPalette(SHOWN[at].band),
  citations: citations(SHOWN[at].style),
  page: { sizePreset: 'custom', width: mm(155), height: mm(235), dpi: 150,
    margins: { top: mm(22), bottom: mm(20), left: mm(18), right: mm(18) } },
  layout: { layoutType: 'single' },
  bodyText: {
    fontFamily: SERIF, fontSize: pt(10), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    textAlign: 'justify', firstLineIndent: mm(4.5), indentAfterHeading: false,
    hyphenation: { enabled: true }, optimalLineBreaking: true,
    avoidWidows: true, avoidOrphans: true, avoidRunts: true,
  },
  headings: { fontFamily: SERIF, color: col('ink'), levels: [
    { level: 1, breakBefore: { enabled: true, parity: 'any' }, // gotcha: headings-drop-h1-break
      advancedDesign: opener(SHOWN[at], at) },
  ] },
  header: { elements: [] },
  footer: { elements: [
    { kind: 'text', id: 'colophon', content: t({
      en: 'Set in Source Serif 4 and Source Sans 3 (SIL OFL) · Text: original, CC BY 4.0',
      es: 'Compuesto en Source Serif 4 y Source Sans 3 (SIL OFL) · Texto: original, CC BY 4.0' }),
    fontFamily: SANS, fontSize: pt(7), color: col('muted'),
    placement: { anchor: { to: 'page', edge: 'bottom-left' }, offset: { x: mm(18), y: mm(-11) } } },
  ] },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "The invisible page"
author: "Postext Cookbook"
---

# The page that is not seen

Good typography is meant to go unnoticed. @warde1955 put the idea in an image that printers still quote: a page should be a crystal goblet, a clear glass that shows the wine, rather than a cup of gold that hides it. Tschichold argued much the same about the book, which in his essays serves its reader before it serves its designer [-@tschichold1991]. The argument is moral as much as practical, and @bringhurst2004 [p. 17] makes it the first principle of his manual: typography exists to honour the content.

The practical half has evidence behind it. Tests of legibility measured how the size of the type, the length of the line and the leading change the speed of reading, and the study of eye movements later explained why an even texture matters: the eye takes in a few letters at each fixation and plans its next jump from the shape of the line ahead [@tinker1963; @rayner1998]. A page that draws attention to itself costs the reader something, however little. None of this makes design a matter of taste alone. A reader who never notices a well-made page notices a badly made one at once, and the grammar of a good one can be taught [@lupton2010].

:::references{format=bibtex}
@book{warde1955,
  author = {Warde, Beatrice}, title = {The Crystal Goblet: Sixteen Essays on Typography},
  editor = {Jacob, Henry}, publisher = {Sylvan Press}, address = {London}, year = 1955}
@book{tschichold1991,
  author = {Tschichold, Jan}, title = {The Form of the Book: Essays on the Morality of Good Design},
  translator = {Hadeler, Hajo}, publisher = {Hartley \& Marks}, address = {Point Roberts, WA},
  year = 1991}
@book{bringhurst2004,
  author = {Bringhurst, Robert}, title = {The Elements of Typographic Style}, edition = {3},
  publisher = {Hartley \& Marks}, address = {Point Roberts, WA}, year = 2004}
@book{tinker1963,
  author = {Tinker, Miles A.}, title = {Legibility of Print},
  publisher = {Iowa State University Press}, address = {Ames, IA}, year = 1963}
@article{rayner1998,
  author = {Rayner, Keith},
  title = {Eye movements in reading and information processing: 20 years of research},
  journal = {Psychological Bulletin}, volume = 124, number = 3, pages = {372--422}, year = 1998,
  doi = {10.1037/0033-2909.124.3.372}}
@book{lupton2010,
  author = {Lupton, Ellen}, title = {Thinking with Type}, edition = {2},
  publisher = {Princeton Architectural Press}, address = {New York}, year = 2010}
:::
`;

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = {
  'Source Serif 4': ['400', '400i', '600', '600i', '700', '700i'], // 700: the list's title
  'Source Sans 3': ['400', '600'],
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
await loadFonts(FONTS, markdown);
// #region build: one text, four documents, shown as two spreads
// The style is set for the whole document, so each style is a build of its own.
const docs = [];
for (at = 0; at < SHOWN.length; at++) {
  // pageIndexOffset makes each page the next one of a book, so the desk pairs them:
  // APA beside Chicago, IEEE beside ISO 690.
  const content = { markdown, continuation: { pageIndexOffset: at + 1 } };
  docs.push(await buildWithFonts(() => buildDocument(content, config()), markdown));
}
// #endregion
showPages(docs, { title: t({ en: 'One text, four styles', es: 'Un texto, cuatro estilos' }) });

// ─── 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 v1 ── the same in every recipe · postext.dev/cookbook ────────
// Postext measures text with the faces the browser has loaded, and caches the
// widths, so every face must be ready before the first build. Faces come from
// Fontsource: the same static files the PDF embeds, so screen and PDF agree.

/** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample:
 *  letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With
 *  `optional`, a face Fontsource does not ship is skipped instead of failing.
 *  Resolves to the number of faces added. */
async function loadFonts(faces, text = '', { optional = false } = {}) {
  kitStatus('Loading fonts…');
  const ranges = {
    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',
  };
  const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin'];
  const jobs = [];
  let added = 0;
  for (const [family, specs] of Object.entries(faces)) {
    const id = fontsourceId(family);
    const meta = optional ? await fontsourceMeta(family) : null;
    for (const spec of new Set(specs)) {
      const weight = parseInt(spec, 10);
      const style = spec.endsWith('i') ? 'italic' : 'normal';
      if (hasFace(family, weight, style)) continue;
      if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue;
      for (const subset of subsets) {
        const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/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().then((ready) => { document.fonts.add(ready); added++; }, () => {
          if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`);
        }));
      }
    }
  }
  await Promise.all(jobs).catch((error) => { kitFail(error); throw error; });
  return added;
}

/** Runs `build` (a buildDocument or buildBundle call) and checks the faces
 *  the pages use. A regular face missing from FONTS is loaded with a warning;
 *  bold and italic variants are loaded when the family ships them. Then the
 *  measurement caches are cleared and the build runs again. */
async function buildWithFonts(build, text = '') {
  const tried = new Set();
  for (let round = 0; round < 3; round++) {
    kitStatus('Laying out…');
    await new Promise(requestAnimationFrame);          // let the status paint first
    const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; });
    const wanted = { base: {}, variants: {} };
    for (const { font, base } of [result].flat().flatMap(fontStringsOf)) {
      const { family, weight, style } = parseFont(font);
      const key = `${family}|${weight}|${style}`;
      if (tried.has(key) || hasFace(family, weight, style)) continue;
      tried.add(key);
      (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`);
    }
    if (Object.keys(wanted.base).length) {
      console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`);
    }
    const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true });
    if (added === 0) return result;
    clearMeasurementCache();
  }
  throw new Error('The fonts did not settle after three builds.');
}

/** Every font string of the layout. `base` marks a block's own face; its
 *  bold, italic and bold-italic variants are listed whether or not used. */
function fontStringsOf(doc) {
  const found = new Map();
  const walk = (node) => {
    if (!node || typeof node !== 'object') return;
    if (Array.isArray(node)) { node.forEach(walk); return; }
    for (const [key, value] of Object.entries(node)) {
      if (typeof value === 'string' && /fontString$/i.test(key)) {
        found.set(value, found.get(value) || key === 'fontString');
      } else if (value && typeof value === 'object') walk(value);
    }
  };
  walk(doc.pages);
  walk(doc.blocks);
  return [...found].map(([font, base]) => ({ font, base }));
}

/** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }.
 *  A string with no weight ('95.8px Young Serif', from a design text) is 400. */
function parseFont(font) {
  const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim());
  if (!m) throw new Error(`Unexpected font string: ${font}`);
  const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]);
  return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' };
}

/** True when a loaded FontFace covers exactly this family, weight and style
 *  (document.fonts.check() is also true 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;
}

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

/** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), 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 v1 ── the same in every recipe · postext.dev/cookbook ───────
/** Shows the pages as facing spreads on a dark desk: the first page is a
 *  recto on its own, then verso | recto pairs, as in a bound book. Pages
 *  are painted when they scroll near the screen. */
function showPages(docs, { title, width = 460 } = {}) {
  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, { 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
-  { style: 'apa', band: 'apa', name: 'APA 7', system: t({ en: 'author-date', es: 'autor-año' }) },
+  { style: 'chicago-notes-bibliography', band: 'apa', name: 'Chicago', system: 'notes' },
```

### اكتب الأرقام مرتفعة

يستطيع الأسلوب المرقم أن يحتفظ بقائمته ويطبع الاستشهادات أرقامًا مرتفعة، كما تفعل Nature وفانكوفر.

```diff
-  link: true,
+  link: true, marker: 'superscript',
```

## أخطاء شائعة

- **أي كائن headings يُلغي فاصل الصفحة قبل H1.** ينتقل H1 افتراضيًا إلى صفحة فردية (always-odd)، لكن تمرير أي كائن headings يعيد ضبط هذا الافتراض، فتتوالى الفصول دون فاصل ولا يفعل span: 'page' شيئًا. أعد كتابة headings.levels[0].breakBefore: { enabled: true, parity } في كل إعداد.
- **حمّل كل أوجه الخط قبل الإخراج.** يقيس الإخراج النص بأوجه الخط التي حمّلها المتصفح ويخزّن العروض مؤقتًا، فالوجه الذي يصل بعد البناء الأول يترك فواصل أسطر خاطئة وملف PDF لم يعد يطابق الشاشة. حمّل كل وزن وكل نمط أولًا، واستدعِ clearMeasurementCache() قبل إعادة البناء إذا تأخر وصول أحدها.
- **ضع كل قيمة في الترويسة الأمامية (frontmatter) بين علامتي اقتباس.** يقرأ YAML القيمة title: 1984 رقمًا، ويقرأ التاريخ كائن Date، والقيم غير النصية تُطبع فارغة في العناصر النائبة وتترك ملف PDF بلا عنوان. ضع كل قيمة بين علامتي اقتباس: title: "1984".
- **يُخزَّن الإعداد مؤقتًا بحسب هويته: ابنِ كائنًا جديدًا.** يخزّن المحرّك الإعدادات المحسوبة مؤقتًا بحسب هوية الكائن، فتعديل الإعداد في مكانه ثم البناء مجددًا يعيد استخدام النتيجة القديمة. ابنِ كائنًا جديدًا في كل بناء، ولهذا يكون إعداد الوصفة دالة مصنِّعة: config().

- يُكتب محدِّد الموضع مرة واحدة ويطبع كل أسلوب كلمته الخاصة له: «p. 17» في الأساليب الأربعة هنا. أما أسلوب ISO 690 الإسباني المضمَّن فيطبع «p.» لكل نوع من محدِّدات الموضع، بما فيها القسم والفصل، ولهذا يستشهد هذا النص بصفحة.
- أرقام الطبعات تُكتب في BibTeX أرقامًا (`edition = {3}`): فيكتب كل أسلوب «3rd ed.» أو «3.ª ed.» أو «3.» على طريقته. وإذا كُتبت «3rd» طُبعت الكلمة كما هي وضاعت صيغة الأسلوب.

## الحقوق

- الوصفة: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- الخطوط: Source Serif 4 (OFL-1.1), Source Sans 3 (OFL-1.1)
- الشيفرة: MIT · محتوى المثال: CC-BY-4.0

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

- [رقم 097 · قائمة قراءات من ملف BibTeX مصدَّر من Zotero](https://postext.dev/ar/cookbook/zotero-bibtex-nocite.md): قائمة قراءات لحلقة دراسية مراجعها ملف BibTeX صدّره Zotero ولُصق كما هو، والاستشهاد بأسلوب Chicago مؤلف-تاريخ، وnocite للأعمال التي لا يستشهد بها النص. · المستوى 2 (متوسط) · الكتب المدرسية, التقارير
- [رقم 087 · فصل من أطروحة باستشهادات APA 7](https://postext.dev/ar/cookbook/apa-thesis-with-bibtex.md): فصل من أطروحة دكتوراه تتحول فيه استشهادات [@key, p. 33] إلى نمط APA 7 عبر citeproc-js، وتُبنى قائمة مراجعه من كتلة BibTeX. · المستوى 2 (متوسط) · الأوراق البحثية والأكاديمية, التقارير
- [رقم 096 · مقالة في الإنسانيات بقائمة أعمال مستشهد بها وفق MLA](https://postext.dev/ar/cookbook/mla-humanities-essay.md): مقالة أدبية تتحول فيها استشهادات [@key, 48] إلى إحالات MLA 9 بالمؤلف والصفحة، مع اقتباس كتلي وقائمة Works Cited مبنية من بيانات CSL-YAML. · المستوى 2 (متوسط) · الأوراق البحثية والأكاديمية
