انتقل إلى المحتوى الرئيسي
الوصفة رقم 87

دليل الوصفات · الفصل 5 · بنية الكتاب

فصل من أطروحة باستشهادات APA 7

فصل من أطروحة دكتوراه تتحول فيه استشهادات [@key, p. 33] إلى نمط APA 7 عبر citeproc-js، وتُبنى قائمة مراجعه من كتلة BibTeX.

في هذه الصفحة
المُخرَج
Canvas · PDF
المستوى
متوسط
Postext
اختُبرت مع Postext 1.12.0
تتطلب ≥ 1.12.0 · postext-pdf ≥ 1.12.0
الترخيص
حُدّثت في 1 أكتوبر 2026
الكود MIT · النص CC BY 4.0
  • نموذج باللغة الإنجليزية: لا توجد طبعة عربية بعد
  • مقاس القص 210 × 297 مم
  • عمود واحد
  • Literata 11/16
  • Public Sans
  • 3 صفحات
  • المستوى
  • Postext 1.12.0
  • أُخرجت في 19 ms
  • أسطر الكود: 118

باختصار

فصل من أطروحة دكتوراه. يكتب المؤلف رمزًا قصيرًا لكل مصدر، فيطبع Postext الاستشهادات وقائمة المراجع في آخر الفصل بأسلوب APA.

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

الفصل الثاني من أطروحة دكتوراه في التربية، وهو مراجعة الأدبيات: ما تقوله عشرون سنة من الدراسات عن القراءة على الورق وعلى الشاشات. يُنضَّد الفصل على صفحة A4 مجلّدة من اليسار، بخط Literata وتباعد أسطر 16 نقطة، تحت شريط باهت بلون الجامعة الأزرق. يحفظ المؤلف مصادره في Zotero ويلصق ما يصدّره منه بصيغة BibTeX؛ وكل مصدر في النص مفتاح. ينسّق Postext كل استشهاد بنمط APA 7، فيذكر المؤلف والسنة والصفحة إن وُجدت، ويكتب "Delgado et al. (2018)" حين يكون الاسم جزءًا من الجملة، ويبني قائمة المراجع بالترتيب والشكل اللذين يطلبهما النمط. وحين يطلب المشرف نمط Chicago، يكون النمط هو السطر الوحيد الذي يتغيّر.

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

  • كيف أستشهد بالأعمال وأبني قائمة المراجع بأسلوب APA أو IEEE أو أسلوب استشهاد آخر؟

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

script.js · الأسطر 27–39في الكود الكامل
// Citations are written [@key, p. 33] and formatted by citeproc-js in the chosen CSL
// style; the references come from the BibTeX block at the end of the chapter. Register
// the engine once, before the first build, then the style is one setting.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
  style: 'apa', // 'chicago-author-date', 'ieee', 'vancouver'… change nothing else
  link: true, // each citation jumps to its entry in the PDF and on screen
  bibliography: {
    // APA asks for a half-inch hanging indent and keeps the list in the text size.
    fontSize: em(1), hangingIndent: mm(12.7), entrySpacing: pt(4),
    doi: 'link', // printed whole, as APA wants, and clickable
  },
};

المكونات

الخطوط
Literata, Public Sans (SIL OFL 1.1)
الأصول
لا شيء: كل صورة مرسومة بالكود

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

#1 · محرّك واحد ونمط واحد

الشيفرة هي الجواب المختصر أعلاه. يقرأ Postext الاستشهادات ويترك صياغتها لمحرّك استشهاد يُسجَّل مرة واحدة قبل أول بناء؛ والحزمة postext-citeproc تغلّف citeproc-js، المحرّك الذي يستعمله Zotero وMendeley، ومعها ستة عشر نمط CSL وملفات لغاتها. يعطي [@mangen2013] النص "(Mangen et al., 2013)"، ويضع @delgado2018 بلا أقواس معقوفة أسماء المؤلفين داخل الجملة، وينقل [@ibanez2023, pp. 12–15] نطاق الصفحات إلى داخل القوسين. وإذا كانت لغة المستند الإسبانية، أخذت المفاتيح نفسها أدوات الربط والاختصارات من ملف اللغة الإسبانية، لأن المحرّك يتبع لغة المستند.

#2 · المراجع تأتي من كتلة BibTeX

آخر كتلة في الفصل هي :::references{format=bibtex} وفيها ما صدّره المؤلف من Zotero ملصوقًا كما هو، والحروف المشكولة مكتوبة على هيئة {\'a}. يضع :::bibliography فوقها القائمة تحت عنوان References؛ وبدونه يُلحق Postext القائمة بآخر الفصل. لا تظهر إلا الأعمال المستشهد بها، مرتبةً كما يرتبها APA، وكل استشهاد في النص رابط إلى مدخله.

#3 · عنوان بلا رقم وإحالات بالكلمات

script.js · الأسطر 105–106في الكود الكامل
  headingStyles: [{ id: 'references', numbered: false }],
  crossRefs: { section: t({ en: 'Section {n}', es: 'Sección {n}' }) },

يأخذ عنوان المراجع النمط references، فيبقى في جدول المحتويات خارج الترقيم، ولا تصير القائمة القسم 2.4. ويطبع :ref{id="sec-speed"} النص "Section 2.2" من تسمية crossRefs ويربطه بالعنوان؛ وتأتي الأرقام من startAt=2 على عنوان الفصل، وهو العدّاد نفسه الذي تستعمله العناوين.

#4 · صفحة الافتتاح داخل شريط

script.js · الأسطر 43–68في الكود الكامل
const BAND = 92; // mm from the trim's top
const opener = {
  enabled: true,
  minHeight: mm(BAND - 28 + 10), // the body starts 10 mm under the band
  slot: { elements: [
    { kind: 'box', id: 'band', style: { backgroundColor: col('tint') },
      placement: { anchor: { to: 'page', edge: 'top-left' },
        size: { width: 'fill', height: mm(BAND) } } },
    { kind: 'text', id: 'label', content: t({ en: 'Chapter', es: 'Capítulo' }),
      fontFamily: LABEL, fontSize: pt(9), fontWeight: 600, letterSpacing: pt(1.8),
      textTransform: 'uppercase',
      color: col('accent'), align: 'left',
      placement: { anchor: { to: 'container', edge: 'top-left' },
        offset: { x: mm(0), y: mm(2) } } },
    { kind: 'text', id: 'number', content: '{number}', fontFamily: TEXT, fontSize: pt(96),
      fontWeight: 300, lineHeight: 0.9, color: col('accent'), align: 'right',
      placement: { anchor: { to: 'container', edge: 'top-right' },
        offset: { x: mm(0), y: mm(-6) },
        size: { width: mm(40), height: 'auto' } } },
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: TEXT, fontSize: pt(24),
      lineHeight: 1.12, fontWeight: 600, color: col('ink'), align: 'left', overflow: 'wrap',
      placement: { anchor: { to: 'container', edge: 'top-left' },
        offset: { x: mm(0), y: mm(14) },
        size: { width: mm(108), height: 'auto' } } },
  ] },
};

الشريط إطار مثبّت إلى حدّ القص، ويبدأ المتن على بعد 10 مم تحته. يُنضَّد الرقم بخط Literata Light بحجم 96 نقطة إلى اليمين، فيُقرأ شكلًا ويترك للعنوان عرض السطر كاملًا.

script.js · الأسطر 16–21في الكود الكامل
const palette = {
  ink: '#1b1b1f', accent: '#22406a', tint: '#e6ecf4', rule: '#b9bec8', muted: '#5d6370',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.accent })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));

الوصفة كاملة

Sandbox
// ═══ Postext Cookbook · Nº 087 · A thesis chapter cited in APA 7 ═══════════════════
// https://postext.dev/en/cookbook/apa-thesis-with-bibtex
// Code: MIT · Text: original (CC BY 4.0) · Pictures: none
// Fonts: Literata, Public Sans (SIL OFL 1.1) · Needs postext ≥ 1.12.0
import {
  buildDocument, renderPageToCanvas, clearMeasurementCache, registerCitationEngine,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';
import { createCiteprocEngine, STYLES, LOCALES } from 'https://esm.sh/postext-citeproc';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'apa-thesis-with-bibtex';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: a thesis prints in black; the one colour is the university's
const palette = {
  ink: '#1b1b1f', accent: '#22406a', tint: '#e6ecf4', rule: '#b9bec8', muted: '#5d6370',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.accent })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
// #endregion
const TEXT = 'Literata', LABEL = 'Public Sans';
const LEAD = 16; // pt: double spacing is a typewriter habit; 1.45 reads as well and fits more

// #region answer: the citation engine, APA 7 and how its references look
// Citations are written [@key, p. 33] and formatted by citeproc-js in the chosen CSL
// style; the references come from the BibTeX block at the end of the chapter. Register
// the engine once, before the first build, then the style is one setting.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
  style: 'apa', // 'chicago-author-date', 'ieee', 'vancouver'… change nothing else
  link: true, // each citation jumps to its entry in the PDF and on screen
  bibliography: {
    // APA asks for a half-inch hanging indent and keeps the list in the text size.
    fontSize: em(1), hangingIndent: mm(12.7), entrySpacing: pt(4),
    doi: 'link', // printed whole, as APA wants, and clickable
  },
};
// #endregion

// #region opener: a pale band at the head of the page, the number large and light in it
const BAND = 92; // mm from the trim's top
const opener = {
  enabled: true,
  minHeight: mm(BAND - 28 + 10), // the body starts 10 mm under the band
  slot: { elements: [
    { kind: 'box', id: 'band', style: { backgroundColor: col('tint') },
      placement: { anchor: { to: 'page', edge: 'top-left' },
        size: { width: 'fill', height: mm(BAND) } } },
    { kind: 'text', id: 'label', content: t({ en: 'Chapter', es: 'Capítulo' }),
      fontFamily: LABEL, fontSize: pt(9), fontWeight: 600, letterSpacing: pt(1.8),
      textTransform: 'uppercase',
      color: col('accent'), align: 'left',
      placement: { anchor: { to: 'container', edge: 'top-left' },
        offset: { x: mm(0), y: mm(2) } } },
    { kind: 'text', id: 'number', content: '{number}', fontFamily: TEXT, fontSize: pt(96),
      fontWeight: 300, lineHeight: 0.9, color: col('accent'), align: 'right',
      placement: { anchor: { to: 'container', edge: 'top-right' },
        offset: { x: mm(0), y: mm(-6) },
        size: { width: mm(40), height: 'auto' } } },
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: TEXT, fontSize: pt(24),
      lineHeight: 1.12, fontWeight: 600, color: col('ink'), align: 'left', overflow: 'wrap',
      placement: { anchor: { to: 'container', edge: 'top-left' },
        offset: { x: mm(0), y: mm(14) },
        size: { width: mm(108), height: 'auto' } } },
  ] },
};
// #endregion

const head = (id, content, edge, x) => ({
  kind: 'text', id, content, pages: 'body', fontFamily: LABEL, fontSize: pt(7.5),
  letterSpacing: pt(0.8), color: col('muted'),
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(14) } },
});

const config = () => ({ // a factory: the engine caches resolved configs per object
  locale: t({ en: 'en-us', es: 'es' }),
  colorPalette,
  citations,
  page: {
    sizePreset: 'custom', width: mm(210), height: mm(297), dpi: 150,
    // one-sided, bound at the left
    margins: { top: mm(28), bottom: mm(28), left: mm(35), right: mm(25) },
  },
  layout: { layoutType: 'single' },
  bodyText: {
    fontFamily: TEXT, fontSize: pt(11), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('accent'),
    textAlign: 'justify', firstLineIndent: mm(6), indentAfterHeading: false,
    hyphenation: { enabled: true }, optimalLineBreaking: true,
    avoidWidows: true, avoidOrphans: true, avoidRunts: true,
  },
  headings: {
    fontFamily: TEXT, color: col('ink'), fontWeight: 600,
    levels: [
      // Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break).
      { level: 1, numberingTemplate: '{1}', fontSize: pt(22),
        breakBefore: { enabled: true, parity: 'any' }, advancedDesign: opener },
      { level: 2, numberingTemplate: '{1}.{2}', numberSeparator: '  ', fontSize: pt(13),
        lineHeight: pt(LEAD), marginTop: pt(LEAD), marginBottom: pt(LEAD / 2) },
    ],
  },
  // #region refs: the references heading goes unnumbered; section references read in words
  headingStyles: [{ id: 'references', numbered: false }],
  crossRefs: { section: t({ en: 'Section {n}', es: 'Sección {n}' }) },
  // #endregion
  header: { elements: [
    head('title', '{title}', 'top-left', 35), head('folio', '{pageNumber}', 'top-right', -25),
  ] },
  footer: { elements: [] },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
نموذج Markdown · أسطر: 69 · content.en.mdtitle: "Reading on Paper and on Screens" author: "Marta Ibáñez Roca" --- # Reading on Paper and on Screens: The State of the Evidence {#ch-review startAt=2} The question this thesis takes up is older than the screens it is usually asked about. Long before the first e-reader, typographers measured how the size of a letter, the length of a line and the space between lines changed the speed and the comfort of reading [@tinker1963]. What changed with screens was the scale of the experiment: by the 2010s a large share of what students read for their courses reached them through a display, and the question of whether that mattered left the laboratory for the lecture hall [@baron2015]. This chapter reviews what is known. :ref{id="sec-comprehension"} gathers the studies of comprehension, :ref{id="sec-speed"} those of speed and line length, and :ref{id="sec-attention"} the arguments about attention that frame the rest of the thesis. The method of the study itself follows in Chapter 3. ## Comprehension {#sec-comprehension} The most cited early comparison asked Norwegian upper-secondary students to read two texts, one narrative and one expository, either on paper or as PDF files on a computer screen [@mangen2013]. Those who read on paper scored better on the comprehension test that followed. The difference was modest, but it pointed in the same direction as a growing number of studies, and it raised a question the authors could not settle: whether the advantage came from the medium, from the way readers navigated it, or from what they expected to do with it. Two meta-analyses later pooled the evidence. @delgado2018 combined 54 studies with more than 170,000 participants and found an advantage for paper in the comprehension of informational texts, larger when reading was done under time pressure and, unexpectedly, larger in the more recent studies than in the older ones. @clinton2019 reached a similar conclusion from a narrower set of experiments: reading from paper led to better comprehension, while the time spent reading did not differ between the media. Neither review found a reliable difference for narrative texts. A pilot study run for this thesis with forty first-year students found the same pattern [@ibanez2023, pp. 12–15], although its sample was too small to settle it. ## Speed and line length {#sec-speed} Speed is the measure readers notice first and researchers trust least. A reader can go faster by understanding less, and a gain in words per minute says nothing of what was retained [@rayner2016]. Studies that control for comprehension tell a more modest story than the claims of speed-reading courses: the eyes move in short jumps, take in a few letters on either side of the fixation, and cannot be trained to take in a whole line at once [@rayner2016]. Line length is the variable typographers have argued about longest. On screen, @dyson2001 measured speed and comprehension at several line lengths and found that the two do not always improve together, which warns against judging a layout by speed alone. Older work on print had already shown that the best length depends on the size of the type and the leading, not on a fixed count of characters [@tinker1963; see also @ibanez2023, sec. 2]. ## Attention and the reading brain {#sec-attention} The empirical studies leave room for a wider argument. @wolf2018 holds that the habits formed by reading on screens, quick and skimming, carry over to the reading of long texts and weaken the slower processes that comprehension of a demanding argument needs. @baron2015 reaches a similar view from surveys of students, many of whom reported that they concentrated better on paper even when they preferred screens for convenience. These are arguments rather than measurements, and this thesis treats them as hypotheses: Chapter 3 tests whether the advantage for paper reported in :ref{id="sec-comprehension"} holds when the screen layout is set with the care a printed page receives. ## References {style="references"} :::bibliography{title=""} :::references{format=bibtex} @book{tinker1963, author = {Tinker, Miles A.}, title = {Legibility of print}, publisher = {Iowa State University Press}, address = {Ames, IA}, year = 1963} @book{baron2015, author = {Baron, Naomi S.}, title = {Words onscreen: The fate of reading in a digital world}, publisher = {Oxford University Press}, address = {New York}, year = 2015} @article{mangen2013, author = {Mangen, Anne and Walgermo, Bente R. and Br{\o}nnick, Kolbj{\o}rn}, title = {Reading linear texts on paper versus computer screen: Effects on reading comprehension}, journal = {International Journal of Educational Research}, volume = 58, pages = {61--68}, year = 2013, doi = {10.1016/j.ijer.2012.12.002}} @article{delgado2018, author = {Delgado, Pablo and Vargas, Crist{\'o}bal and Ackerman, Rakefet and Salmer{\'o}n, Ladislao}, title = {Don't throw away your printed books: A meta-analysis on the effects of reading media on reading comprehension}, journal = {Educational Research Review}, volume = 25, pages = {23--38}, year = 2018, doi = {10.1016/j.edurev.2018.09.003}} @article{clinton2019, author = {Clinton, Virginia}, title = {Reading from paper compared to screens: A systematic review and meta-analysis}, journal = {Journal of Research in Reading}, volume = 42, number = 2, pages = {288--325}, year = 2019, doi = {10.1111/1467-9817.12269}} @article{dyson2001, author = {Dyson, Mary C. and Haselgrove, Mark}, title = {The influence of reading speed and line length on the effectiveness of reading from screen}, journal = {International Journal of Human-Computer Studies}, volume = 54, number = 4, pages = {585--612}, year = 2001, doi = {10.1006/ijhc.2001.0458}} @article{rayner2016, author = {Rayner, Keith and Schotter, Elizabeth R. and Masson, Michael E. J. and Potter, Mary C. and Treiman, Rebecca}, title = {So much to read, so little time: How do we read, and can speed reading help?}, journal = {Psychological Science in the Public Interest}, volume = 17, number = 1, pages = {4--34}, year = 2016, doi = {10.1177/1529100615623267}} @techreport{ibanez2023, author = {Ib{\'a}{\~n}ez Roca, Marta}, title = {Screen layouts and comprehension: A pilot study}, institution = {Reading Lab}, type = {Working paper}, number = 3, year = 2023} @book{wolf2018, author = {Wolf, Maryanne}, title = {Reader, come home: The reading brain in a digital world}, publisher = {Harper}, address = {New York}, year = 2018} :::
`; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { Literata: ['300', '400', '400i', '600', '600i'], 'Public Sans': ['400', '600'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); const doc = await buildWithFonts(() => buildDocument({ markdown }, config()), markdown); const title = t({ en: 'A thesis chapter in APA 7', es: 'Un capítulo de tesis en APA 7' }); showPages(doc, { title }); offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider }), `${RECIPE}.pdf`);
العُدّة · core, fonts, viewer, pdf: نفسها في كل وصفة · أسطر: 275// ─── 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 · pdf v1 ── the same in every recipe that exports a PDF ────────────── /** postext-pdf embeds TrueType bytes. Fetch the Fontsource file the screen * used, snapping to a weight the family ships and falling back to upright * when it has no italic: the PDF asks for every face a block could use. */ async function fontsourceProvider(family, weight, style) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); const weights = meta?.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style; const res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${w}-${s}.woff2`); if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} (${res.status})`); return decompressWoff2(new Uint8Array(await res.arrayBuffer())); } /** A "Build the PDF" button in the bar. Once built: "Open the PDF" (a new * tab, since CodePen's preview frame cannot show PDFs) and a download link. */ function offerPdf(makePdf, filename) { viewer(); const button = Object.assign(document.createElement('button'), { type: 'button', textContent: 'Build the PDF' }); button.dataset.postextPdf = filename; button.addEventListener('click', async () => { button.disabled = true; button.textContent = 'Building the PDF…'; try { const bytes = await makePdf(); const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' })); const size = `${Math.max(1, Math.round(bytes.length / 1024))} KB`; button.replaceWith( Object.assign(document.createElement('a'), { href: url, target: '_blank', rel: 'noopener', textContent: 'Open the PDF ↗' }), Object.assign(document.createElement('a'), { href: url, download: filename, textContent: `Download ${filename} · ${size}` })); } catch (error) { button.disabled = false; button.textContent = 'Build the PDF'; kitFail(error); } }); document.getElementById('pt-actions').append(button); } // ─── /Kit ───────────────────────────────────────────────────────────────────────

يعمل ملف script.js المجمّع كما هو: الصقه في سكربت الوحدة (module) لأي صفحة، أو افتح الوصفة على CodePen. مجلد الوصفة على GitHub ↗ (يفتح في تبويب جديد)

تنويعات

#غيّر النمط

الفصل نفسه بنمط Chicago للمؤلف والسنة يحذف الفاصلة بعد الاسم ويذكر السنة بعد المؤلفين.

-  style: 'apa', // 'chicago-author-date', 'ieee', 'vancouver'… change nothing else
+  style: 'chicago-author-date',

#اجعل القائمة أصغر من النص

تسمح جامعات كثيرة بأن تُنضَّد قائمة المراجع بحجم أصغر بدرجة واحدة وبتباعد أسطر مفرد.

-    fontSize: em(1), hangingIndent: mm(12.7), entrySpacing: pt(4),
+    fontSize: em(0.9), lineHeight: pt(13), hangingIndent: mm(12.7), entrySpacing: pt(3),

أخطاء شائعة

خطأ شائع

أي كائن 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(). صفحات على اللوحة (Canvas) →

  • يكتب APA عناوين المقالات والكتب بحالة الجملة (الحرف الكبير في أول كلمة فقط)، ويطبع citeproc-js العناوين كما تأتي في البيانات. ويحفظ Zotero معظم العناوين بحالة العنوان (حرف كبير في أول كل كلمة)، فصحّحها في Zotero أو في BibTeX قبل أن تصل إلى الصفحة.

الحقوق

الوصفة
Ignacio Ferro
النص
نص أصلي, CC BY 4.0
الخطوط
Literata (SIL OFL 1.1) · Public Sans (SIL OFL 1.1)
SandboxPDF