باختصار
مقالة عن الطريقة التي صارت بها المدن البريطانية تعتمد توقيت لندن. كل مصدر يستشهد به الكاتب يصير حاشية مرقّمة أسفل الصفحة، وتأتي قائمة المؤلفات في النهاية.
ما الذي ستنضده
مستلّة من سلسلة تاريخ التقنية في مجلة علمية: مقالة من نحو 1,100 كلمة عن تخلّي المدن البريطانية عن ظهرها المحلي لصالح ظهر لندن بين 1840 و1880. الصفحة 156 × 234 mm بهوامش متناظرة، بخط Crimson Pro بحجم 11/14 pt، وعلى النصف الأعلى من الصفحة الأولى شريط بأخضر السكك الحديدية. يكتب المؤلف كل مصدر مفتاحًا، [@howse1980, 87]، فينضّد Postext كلًّا منها حاشية مرقّمة أسفل الصفحة بأسلوب شيكاغو للحواشي وقائمة المراجع. الحاشية الأولى لعمل ما تذكر المؤلف والعنوان والناشر؛ وكل حاشية بعدها تذكر الصيغة المختصرة. حواشي المؤلف الاستطرادية تأخذ أرقامها من التسلسل نفسه، وقائمة المراجع تختم المقالة.
تجيب هذه الوصفة عن
- كيف أنضد الاستشهادات حواشي سفلية، بأسلوب Chicago، مع صيغ مختصرة للاستشهادات اللاحقة؟
- كيف أستشهد بالأعمال وأبني قائمة المراجع بأسلوب APA أو IEEE أو أسلوب استشهاد آخر؟
الجواب المختصر
// The text cites with [@howse1980, 87]; a note style turns every citation into a
// footnote. The first note for a work gives it in full, later ones in the short form
// (author, short title, page), and the bibliography lists the works by author.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
style: 'chicago-notes-bibliography', // CMOS 18th edition, notes and bibliography
notes: 'footnote', // each citation in a note at the foot of the page
link: true, // a note's citation jumps to its bibliography entry
bibliography: { fontSize: em(0.92), lineHeight: pt(13), hangingIndent: em(1.8),
entrySpacing: pt(3) },
};
// The citation notes share the numbering and the look of the author's own [^notes].
const footnotes = {
placement: 'column', numbering: 'chapter',
fontSize: pt(8.5), lineHeight: pt(10.75), spaceBetween: pt(2),
textAlign: 'left', // ragged: the gap after a note's number stays the same in every note
separator: { width: 0.3, lineWidth: pt(0.5), color: col('rule') },
};
المكونات
- الميزات
- الاستشهادات في الحواشيالاستشهادات بأسلوب توثيققائمة المراجع من البيانات المرجعيةالحواشي السفليةصفحات افتتاح مصمَّمةسمات العنوانالترويسات وأرقام الصفحاتهوامش متناظرةأنماط الفقراتلوحة ألوان دلاليةالتصدير إلى PDF
- الخطوط
- Crimson Pro, IBM Plex Sans Condensed (SIL OFL 1.1)
- الأصول
- لا شيء: كل صورة مرسومة بالكود
طريقة التحضير
#1 · حاشية لكل استشهاد
الشيفرة في الجواب المختصر أعلاه. chicago-notes-bibliography أسلوب حواشٍ، فالاستشهاد في النص يترك رقمًا مرتفعًا، وتذهب كلماته إلى حاشية سفلية مرقّمة مع حواشي المؤلف [^notes]؛ وnotes: 'footnote' يصرّح بذلك. يتتبّع citeproc-js المؤلفات التي سبق الاستشهاد بها، فتقرأ الحاشية 3 Derek Howse, Greenwich Time and the Discovery of the Longitude (Oxford University Press, 1980), 87، وتقرأ الحاشية 4، التي تستشهد بالكتاب نفسه مرة أخرى، Howse, Greenwich Time and the Discovery of the Longitude, 90–91. الأسلوب المضمَّن هو الطبعة الثامنة عشرة من دليل شيكاغو، وهي تُسقط Ibid. وتكرّر الصيغة المختصرة، لذلك تطبع الحاشيتان 11 و12 الصيغة المختصرة مرتين.
#2 · حواشي المؤلف تستشهد هي أيضًا
الاستشهاد داخل [^note] يبقى في تلك الحاشية ويُنضَّد داخل السطر، كاملًا إن كان أول ظهور للعمل: الحاشية 1 جملة المؤلف تتبعها الإحالة الكاملة إلى Schivelbusch. كل استشهاد ينتهي بنقطة الأسلوب، فالحاشية التي تُختم باستشهاد لا تحتاج إلى نقطة خاصة بها في ملف Markdown.
#3 · السوابق واللواحق ومحدِّدات المواضع
يضع [see @simmons1991, 346–47; and @bartky2000, chap. 9] الكلمة see قبل العمل الأول، ويفصل بين العملين بفاصلة منقوطة، ويحتفظ لكل عمل بمحدِّد موضعه. الرقم المجرد صفحة؛ وchap. وcap. تسمّيان فصلًا في كلتا اللغتين. والنص الذي يلي محدِّد الموضع لاحقة: [@howse1980, 82–84, on the mail-coach timepieces] يطبع العبارة بعد الصفحات.
#4 · الافتتاح داخل شريط
const BAND = 122; // mm from the trim's top
const opener = {
enabled: true,
minHeight: mm(BAND - MARGIN.top + 9), // the text starts 9 mm under the band
slot: { elements: [
{ kind: 'box', id: 'band', style: { backgroundColor: col('green') },
placement: { ...at('page', 'top-left'), size: { width: 'fill', height: mm(BAND) } } },
{ kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: LABEL, fontWeight: 600,
fontSize: pt(7.5), letterSpacing: pt(1.4), textTransform: 'uppercase', color: col('tint'),
align: 'left', placement: at('container', 'top-left', 0, 4) },
{ kind: 'text', id: 'years', content: '{attr.years}', fontFamily: TEXT, fontWeight: 300,
fontSize: pt(54), lineHeight: 1, color: col('tint'), align: 'left',
placement: at('container', 'top-left', -1, 16) },
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: TEXT, fontWeight: 600,
fontSize: pt(25), lineHeight: 1.08, color: col('paper'), align: 'left', overflow: 'wrap',
placement: { ...at('container', 'top-left', 0, 47),
size: { width: mm(112), height: 'auto' } } },
{ kind: 'text', id: 'byline', content: '{attr.byline}', fontFamily: LABEL, fontWeight: 600,
fontSize: pt(9), letterSpacing: pt(0.6), color: col('tint'), align: 'left',
placement: at('container', 'top-left', 0, 92) },
] },
};
يمتد الشريط 122 mm نزولًا من حدّ القص، ويبدأ النص على بعد 9 mm تحته. التاريخان يأتيان من سمة في العنوان، years="1840–1880"، منضّدة بخط Crimson Pro Light بحجم 54 pt، فتعرض البطاقة رقمًا بحجم العنوان؛ وسمة اسم المؤلف تغذّي أيضًا الترويسة في الصفحات الزوجية.
const palette = {
ink: '#211d1a', green: '#24493d', tint: '#dfe8e2', rule: '#b7b0a4', muted: '#6a645b',
paper: '#fbf9f4',
};
// A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs).
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.green })
.map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
الوصفة كاملة
// ═══ Postext Cookbook · Nº 089 · A history essay with Chicago notes ═══════════════ // https://postext.dev/en/cookbook/history-essay-chicago-notes // Code: MIT · Text: original (CC BY 4.0) · Pictures: none // Fonts: Crimson Pro, IBM Plex Sans Condensed (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 = 'history-essay-chicago-notes'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: a railway green for the opener band, ink and warm paper for the rest const palette = { ink: '#211d1a', green: '#24493d', tint: '#dfe8e2', rule: '#b7b0a4', muted: '#6a645b', paper: '#fbf9f4', }; // A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs). const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id }); const colorPalette = Object.entries({ ...palette, 'main-color': palette.green }) .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })); // #endregion const TEXT = 'Crimson Pro', LABEL = 'IBM Plex Sans Condensed'; const TRIM = { width: 156, height: 234 }; // a journal offprint, trimmed like a trade book const MARGIN = { top: 22, bottom: 22, inner: 20, outer: 17 }; const LEAD = 14; // pt const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } }); // #region answer: Chicago notes and bibliography, one note per citation // The text cites with [@howse1980, 87]; a note style turns every citation into a // footnote. The first note for a work gives it in full, later ones in the short form // (author, short title, page), and the bibliography lists the works by author. registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES })); const citations = { style: 'chicago-notes-bibliography', // CMOS 18th edition, notes and bibliography notes: 'footnote', // each citation in a note at the foot of the page link: true, // a note's citation jumps to its bibliography entry bibliography: { fontSize: em(0.92), lineHeight: pt(13), hangingIndent: em(1.8), entrySpacing: pt(3) }, }; // The citation notes share the numbering and the look of the author's own [^notes]. const footnotes = { placement: 'column', numbering: 'chapter', fontSize: pt(8.5), lineHeight: pt(10.75), spaceBetween: pt(2), textAlign: 'left', // ragged: the gap after a note's number stays the same in every note separator: { width: 0.3, lineWidth: pt(0.5), color: col('rule') }, }; // #endregion // #region opener: the green band holds the kicker, the dates and the title const BAND = 122; // mm from the trim's top const opener = { enabled: true, minHeight: mm(BAND - MARGIN.top + 9), // the text starts 9 mm under the band slot: { elements: [ { kind: 'box', id: 'band', style: { backgroundColor: col('green') }, placement: { ...at('page', 'top-left'), size: { width: 'fill', height: mm(BAND) } } }, { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: LABEL, fontWeight: 600, fontSize: pt(7.5), letterSpacing: pt(1.4), textTransform: 'uppercase', color: col('tint'), align: 'left', placement: at('container', 'top-left', 0, 4) }, { kind: 'text', id: 'years', content: '{attr.years}', fontFamily: TEXT, fontWeight: 300, fontSize: pt(54), lineHeight: 1, color: col('tint'), align: 'left', placement: at('container', 'top-left', -1, 16) }, { kind: 'text', id: 'title', content: '{titleText}', fontFamily: TEXT, fontWeight: 600, fontSize: pt(25), lineHeight: 1.08, color: col('paper'), align: 'left', overflow: 'wrap', placement: { ...at('container', 'top-left', 0, 47), size: { width: mm(112), height: 'auto' } } }, { kind: 'text', id: 'byline', content: '{attr.byline}', fontFamily: LABEL, fontWeight: 600, fontSize: pt(9), letterSpacing: pt(0.6), color: col('tint'), align: 'left', placement: at('container', 'top-left', 0, 92) }, ] }, }; // #endregion const head = (id, content, parity, edge, x, align) => ({ kind: 'text', id, content, parity, pages: 'body', fontFamily: LABEL, fontWeight: 600, fontSize: pt(7.5), letterSpacing: pt(1.1), textTransform: 'uppercase', color: col('muted'), align, placement: at('page', edge, x, 12), }); const config = () => ({ // a factory: the engine caches resolved configs per object locale: t({ en: 'en-us', es: 'es' }), // exact codes (gotcha: hyphenation-locales) colorPalette, citations, footnotes, page: { sizePreset: 'custom', width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150, backgroundColor: col('paper'), margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner), right: mm(MARGIN.outer), mirror: true }, }, layout: { layoutType: 'single' }, bodyText: { fontFamily: TEXT, fontSize: pt(11), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'), textAlign: 'justify', firstLineIndent: mm(5), indentAfterHeading: false, hyphenation: { enabled: true }, optimalLineBreaking: true, avoidWidows: true, avoidOrphans: true, avoidRunts: true, }, headings: { fontFamily: TEXT, fontWeight: 600, color: col('ink'), levels: [ // Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break). { level: 1, fontSize: pt(25), breakBefore: { enabled: true, parity: 'any' }, advancedDesign: opener }, { level: 2, fontFamily: LABEL, fontWeight: 600, fontSize: pt(8.5), letterSpacing: pt(1.3), textTransform: 'uppercase', color: col('green'), lineHeight: pt(LEAD), marginTop: pt(LEAD), marginBottom: pt(LEAD / 2) }, ], }, paragraphStyles: [ { id: 'colophon', fontFamily: LABEL, fontSize: pt(7), lineHeight: pt(9.5), color: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) }, ], header: { elements: [ head('verso-folio', '{pageNumber}', 'even', 'top-left', MARGIN.outer, 'left'), head('verso-author', '{attr.byline}', 'even', 'top-left', MARGIN.outer + 8, 'left'), head('recto-title', '{title}', 'odd', 'top-right', -(MARGIN.outer + 8), 'right'), head('recto-folio', '{pageNumber}', 'odd', 'top-right', -MARGIN.outer, 'right'), ] }, footer: { elements: [] }, }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---نموذج Markdown · أسطر: 106 · content.en.md
title: "Railway Time" author: "Clara Whitcombe" --- # Railway Time: How British Towns Came to Keep London's Hour {#essay kicker="Essays in the History of Technology · No. 14" years="1840–1880" byline="Clara Whitcombe"} In the autumn of 1840 a traveller who left Paddington on the Great Western Railway carried two times with him. The timetable in his pocket gave the hours of London; the church clock at Bath or Bristol, when he arrived, gave the hours of the place, a few minutes behind. Bristol stood a little over two and a half degrees west of Greenwich, and its noon, the moment the sun crossed the local meridian, came about ten minutes after London's. For centuries the difference had not mattered. A stagecoach that took a day to cover the distance lost more than ten minutes to a lame horse. A train that covered it in four hours could not.[^frame] This essay follows the forty years in which that difference was settled, from the Great Western's first timetables to the act of Parliament that made Greenwich time the legal time of Great Britain in 1880. The story has usually been told as a triumph of engineering over custom. It reads better as a negotiation, in which railway companies, telegraph companies, town councils and courts each settled the question for their own purposes, and not always in the same direction. [^frame]: For the railway's effect on the sense of time and distance, see [@schivelbusch1986, chap. 3] ## The companies decide The first railways ran their trains by the clocks of their own stations. On a line of a few miles this caused no trouble, and the early timetables did not say which time they meant. The difficulty came with the long trunk lines of the late 1830s, which joined towns whose clocks disagreed by a quarter of an hour.[@simmons1991, 345] The Great Western printed London time in its timetables from November 1840 and asked its station clocks to keep it.[@howse1980, 87] In 1847 the Railway Clearing House, the body through which the companies settled their accounts for through traffic, recommended that every member adopt Greenwich time from the first of December. By the end of 1848 nearly all of them had.[@howse1980, 90–91] The decision was commercial before it was scientific. A company that kept one time along its line needed one timetable and one set of instructions for its staff, and it could publish connections with other lines without a table of corrections. Greenwich was chosen because London was where the companies had their offices and the Post Office its mail coaches, not because the observatory stood there.[^post] [^post]: The Post Office had sent its guards out with watches set to London time since the 1790s; see [@howse1980, 82–84, on the mail-coach timepieces] ## The wire carries the hour A time that every station is meant to keep has to reach every station. At first it travelled by hand: a guard on the morning down train carried a watch set at Paddington, and the stationmasters along the line set their clocks from it. The electric telegraph changed the means. From 1852 the Royal Observatory sent a signal each day along the wires of the Electric Telegraph Company, and the company passed it on to the railway stations and post offices on its network.[@standage1998, 59] A clock in Manchester or Edinburgh could now be checked against Greenwich within a second or two of the event. The signal made Greenwich time available everywhere the wires went; it did not oblige anyone to keep it.[@schivelbusch1986, 43–44] ## The towns hesitate Most towns on a main line gave way within a few years, since a clock that disagreed with the station made people miss trains. Some held out. In 1852 the clock on the Corn Exchange at Bristol was given a second minute hand, one for Bristol and one for London, and it still has both.[@simmons1991, 346] At Exeter, which the railway reached in 1844, the cathedral clock went on keeping the time of the city for some years after the station clock had changed.[^exeter] [^exeter]: On the British towns, and on the same quarrel in American towns thirty years later, see [@simmons1991, 346–47; and @bartky2000, chap. 9] The holdouts were not cranks. Local mean time followed the sun, and the sun still ordered the working day of farmers, fishermen and anyone whose hours depended on daylight. An hour fixed in London was an imposition on them. The historian of factory discipline has argued that the clock had already become a means of controlling labour by the eighteenth century;[@thompson1967, 80–82] the dispute over railway time extended that argument to whole towns, which were asked to accept an hour set elsewhere. ## The courts decline The law was slower than the towns. Statutes that named a time meant the time of the place, and that time was local mean time. In 1858 a trial at Dorchester began at ten by the court's clock, which kept London time, and judgment was given against a defendant who arrived at ten by the time of the town, some minutes later. On appeal the judges held that the hour meant was the local one, and the judgment was set aside.[@howse1980, 113] For twenty more years a contract, a polling station or a public house could be bound by one time while the trains ran by another. The Statutes (Definition of Time) Act of 1880 closed the gap. It declared that any expression of time in an act of Parliament, deed or other legal instrument meant, unless stated otherwise, Greenwich mean time in Great Britain and Dublin mean time in Ireland.[@howse1980, 114] ## A settlement, not a conquest Seen from 1840, railway time was one of several possible arrangements. The companies might have kept local times and printed tables of corrections, as some American railroads did until 1883,[@bartky2000, 140–45] and the international system of zones that followed the Washington conference of 1884 took half a century to reach most of the world.[@ogle2015, chap. 1] In Britain the distances were short, the companies were few enough to agree through a single clearing house, and the telegraph reached the stations as the trains did. Those conditions, more than any principle, account for the speed with which a country of local noons came to keep one hour.[@ogle2015, 25; see also @schivelbusch1986, chap. 3] ## Bibliography :::bibliography{title=""} :::paragraphs{style="colophon"} A sample essay written for the Postext Cookbook (CC BY 4.0). The cited works are real; the page numbers in the notes are illustrative. Set in Crimson Pro and IBM Plex Sans Condensed (SIL OFL). ::: :::references{format=csl-yaml} - id: schivelbusch1986 type: book author: [{family: Schivelbusch, given: Wolfgang}] title: "The Railway Journey: The Industrialization of Time and Space in the 19th Century" publisher: University of California Press publisher-place: Berkeley issued: 1986 - id: thompson1967 type: article-journal author: [{family: Thompson, given: E. P.}] title: "Time, Work-Discipline, and Industrial Capitalism" container-title: Past & Present issue: 38 page: 56-97 issued: 1967 - id: howse1980 type: book author: [{family: Howse, given: Derek}] title: Greenwich Time and the Discovery of the Longitude publisher: Oxford University Press publisher-place: Oxford issued: 1980 - id: simmons1991 type: book author: [{family: Simmons, given: Jack}] title: The Victorian Railway publisher: Thames and Hudson publisher-place: London issued: 1991 - id: standage1998 type: book author: [{family: Standage, given: Tom}] title: The Victorian Internet publisher: Walker publisher-place: New York issued: 1998 - id: bartky2000 type: book author: [{family: Bartky, given: Ian R.}] title: "Selling the True Time: Nineteenth-Century Timekeeping in America" publisher: Stanford University Press publisher-place: Stanford, CA issued: 2000 - id: ogle2015 type: book author: [{family: Ogle, given: Vanessa}] title: The Global Transformation of Time, 1870–1950 publisher: Harvard University Press publisher-place: Cambridge, MA issued: 2015 :::`; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── // Every face the design uses (gotcha: fonts-first). const FONTS = { 'Crimson Pro': ['300', '400', '400i', '600', '600i'], 'IBM Plex Sans Condensed': ['400', '600'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); const doc = await buildWithFonts(() => buildDocument({ markdown }, config()), markdown); const title = t({ en: 'A history essay with Chicago notes', es: 'Un ensayo de historia con notas de Chicago' }); 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 ↗ (يفتح في تبويب جديد)
تنويعات
#ضع الحواشي في النهاية
بعض المجلات تجمع الحواشي بعد المقالة؛ وتحتفظ الاستشهادات بصيغتيها الكاملة والمختصرة.
- placement: 'column', numbering: 'chapter',
+ placement: 'chapterEnd', numbering: 'chapter',#اكتب Ibid. من جديد
الطبعة السابعة عشرة من أسلوب شيكاغو تطبع Ibid. في الحاشية التي تكرّر العمل السابق. الصق ملف .csl لذلك الأسلوب، من مستودع أساليب Zotero، في سلسلة نصية واستعمله أسلوبًا مخصصًا.
- style: 'chicago-notes-bibliography', // CMOS 18th edition, notes and bibliography
+ style: 'custom', customStyle: CHICAGO_17TH_CSL, // the XML of the .csl fileأخطاء شائعة
خطأ شائع
أي كائن headings يُلغي فاصل الصفحة قبل H1
ينتقل H1 افتراضيًا إلى صفحة فردية (always-odd)، لكن تمرير أي كائن headings يعيد ضبط هذا الافتراض، فتتوالى الفصول دون فاصل ولا يفعل span: 'page' شيئًا. أعد كتابة headings.levels[0].breakBefore: { enabled: true, parity } في كل إعداد. فصول تبدأ في صفحة فردية →
خطأ شائع
حمّل كل أوجه الخط قبل الإخراج
يقيس الإخراج النص بأوجه الخط التي حمّلها المتصفح ويخزّن العروض مؤقتًا، فالوجه الذي يصل بعد البناء الأول يترك فواصل أسطر خاطئة وملف PDF لم يعد يطابق الشاشة. حمّل كل وزن وكل نمط أولًا، واستدعِ clearMeasurementCache() قبل إعادة البناء إذا تأخر وصول أحدها. الخطوط قبل الإخراج →
خطأ شائع
لوحة الألوان المستبدلة لا تصل إلى عناصر التصميم ولا إلى لون الإحالة
يقرأ postext 1.4.1 الإعداد colorPalette في أنماط النص (المتن والعناوين والقوائم والتعليقات والجداول والإطارات) لكن لا في عناصر الترويسات والتذييلات والافتتاحيات وصفحات الأجزاء، ولا في bodyText.referenceColor: تحتفظ بالقيمة الست عشرية المكتوبة بجانب paletteId الخاص بها. حين تستبدل لوحة الألوان، لنسخة شاشة داكنة أو لإعادة تلوين، أعِد كتابة كل لون مرتبط من colorPalette قبل البناء. لوحة ألوان دلالية →
خطأ شائع
8 لغات فقط تُقسَّم بالواصلة، بالرمز المطابق تمامًا
يتوفر تقسيم الكلمات بالواصلة للغات en-us وes وfr وde وit وpt وca وnl، بمطابقة تامة للرمز: 'es-ES' أو أي لغة أخرى تعود دون تنبيه إلى الإنجليزية الأمريكية. تقسيم الكلمات ولغة المستند →
خطأ شائع
يُخزَّن الإعداد مؤقتًا بحسب هويته: ابنِ كائنًا جديدًا
يخزّن المحرّك الإعدادات المحسوبة مؤقتًا بحسب هوية الكائن، فتعديل الإعداد في مكانه ثم البناء مجددًا يعيد استخدام النتيجة القديمة. ابنِ كائنًا جديدًا في كل بناء، ولهذا يكون إعداد الوصفة دالة مصنِّعة: config(). صفحات على اللوحة (Canvas) →
- لا تختم الحاشية بنقطة بعد الاستشهاد: الأسلوب يختم الاستشهاد بنقطة أصلًا، وPostext لا يدمج النقطتين، فتنتهي الحاشية بنقطتين.
- في أسلوب الحواشي، يترك
@howse1980بلا أقواس رقم الحاشية وحده في الجملة، As ¹ says، بلا اسم المؤلف. اكتب الاسم بنفسك واستشهد بين أقواس:As Howse says,[@howse1980].
الحقوق
- الوصفة
- Ignacio Ferro
- النص
- نص أصلي, CC BY 4.0
- الخطوط
- Crimson Pro (SIL OFL 1.1) · IBM Plex Sans Condensed (SIL OFL 1.1)
- الكود
- MIT، مثل Postext
حرّر هذا الشرح ↗ (يفتح في تبويب جديد)مجلد الوصفة على GitHub ↗ (يفتح في تبويب جديد)


