باختصار
كتاب صغير من ثلاث مقالات عن المشي. يُكتب اسم كاتب كل مقالة وسنتها مرة واحدة، ثم يظهران حيث يحتاجهما الكتاب، كصفحة المحتويات مثلًا.
ما الذي ستنضده
تسع صفحات من Afoot، مختارات جيب من ثلاث مقالات عن المشي كتبها هازلت (1822) وثورو (1862) وستيفنسون (1876). يملأ الغلاف لوحةٌ لأرض بور عند الغسق، صورة JPEG مولَّدة بنماذج الانتشار. تعطي المحتويات كل مقالة رقمًا بلون الصدأ، وعنوانًا بخط Gloock، ونقاط إرشاد متباعدة إلى صفحتها، واسم الكاتب بمائل بلون الخلنج تحته. وتفتتح كل مقالة برقمها وعنوانها، ثم سطر باسم الكاتب بحروف كبيرة متباعدة، ثم سطر تاريخ فيه المجلة وسنة النشر الأول. تحمل الصفحات الزوجية اسم الكاتب، والفردية عنوان المقالة. يحمل عنوان كل مقالة كاتبها وسنتها ومصدرها سماتٍ، ومن هناك تطبعها صفحة الافتتاح والترويسات والمحتويات، فلا تحتاج مقالة رابعة إلى أي تغيير في الإعداد.
تجيب هذه الوصفة عن
- كيف أضيف جدول محتويات يحدّث نفسه (نقاط توجيه، أرقام صفحات، مؤلفون، صفوف الأجزاء)؟
- كيف أضيف إلى صفحة الافتتاح سطر المؤلف أو مقدمة تمهيدية أو فقرة أولى بحرف استهلالي؟
- كيف أضبط الترويسات: عنوان الكتاب في الصفحة اليسرى، وعنوان الفصل في اليمنى، ورقم الصفحة في الجانب الخارجي؟
الجواب المختصر
// Each essay's heading carries its credits, and every {attr.…} below reads them:
// # Walking {author="Henry David Thoreau" year="1862" source="The Atlantic Monthly"}
// (a value cannot hold { or }, and one with " goes in single quotes; gotcha: attr-values)
// 1 · The opener: inside a heading's design, {attr.author} is that heading's attribute.
const byline = [
text('byline', '{attr.author}', { ...label, fontSize: pt(8), letterSpacing: pt(1.6),
color: col('heather') }, below('title', 5)),
text('dateline', '{attr.source}, {attr.year}', { fontFamily: 'Spectral', italic: true,
fontSize: pt(9.5), color: col('muted'), align: 'left' }, below('byline', 1.2)),
];
// 2 · The verso running head (head() is in the running-heads region): in the page header,
// {attr.author} is the attribute of the essay the page belongs to.
const versoAuthor = head('verso-author', '{attr.author}', 'even',
at('page', 'top-left', MARGIN.outer + HEAD.gap, HEAD.y));
// 3 · The contents: toc.subtitle prints the attribute it names as a line under each title.
// 'author' and italic are the defaults; attr is written out so it can become 'source'.
const authorLine = { enabled: true, attr: 'author', fontFamily: 'Spectral', fontSize: pt(10),
color: col('heather') };
// Hooked up below: byline → the opener, versoAuthor → header, authorLine → toc.subtitle.
// A heading without author="…" gets an empty byline and running head and no line in the
// contents, and the build gives no warning: check every heading.
المكونات
- الميزات
- جدول المحتوياتسمات العنوانالترويسات وأرقام الصفحاتصفحات افتتاح مصمَّمةالعناوين المرقّمةأنماط العناوينفصول بلا أرقامالأغلفة وصفحات العنوان وبيانات النشرالصور في تصاميم الصفحةتثبيت عناصر التصميمالترويسات بحسب دور الصفحةهوامش متناظرةأنماط الفقراتإطارات وشارات مثبّتةإطارات التنبيهمسافة عمودية صريحةالبيانات الوصفية للمستندلوحة ألوان دلاليةالأشكال والجداول بوصفها مواردصفحات على اللوحة (Canvas)
- تستخدم أيضًا
- لون الورقترويسات لكل قسم
- الخطوط
- Spectral, Gloock, Hanken Grotesk (SIL OFL 1.1)
- الأصول
cover-1200.jpg- The cover: a heath and hills at dusk, a painting (Generated With Diffusion Models, أصلي)
طريقة التحضير
#1 · رقّم المقالات وأنزل نصّها
const opener = { enabled: true,
// The hairline ends 7 mm above the foot of this reserve, so the text starts on the same grid
// line under every opener whose title fits on one line (SINK = 14 holds a two-line title).
minHeight: pt(SINK * LEAD),
slot: { elements: [
// {number} prints numberingTemplate '{1:I}': I, II, III. {numberRoman} would print
// nothing here: it is filled on part pages only (gotcha: heading-number-placeholders).
text('number', '{number}', { ...gloock, fontSize: pt(34), lineHeight: 1, color: col('rust') },
at('container', 'top-left', 0, 8)),
text('title', '{titleText}', { ...gloock, fontSize: pt(26), lineHeight: 1.08,
color: col('ink') }, below('number', 3, { width: 'fill' })),
...byline,
{ kind: 'rule', id: 'rule', thickness: pt(0.5), color: col('rule'),
placement: below('dateline', 6) }, // a horizontal rule runs to the column's edge
] } };
const essays = { level: 1, numberingTemplate: '{1:I}', advancedDesign: opener,
marginBottom: pt(0), // the heading's default margin would add to minHeight
// Restated (gotcha: headings-drop-h1-break); the cover and contents styles inherit it too.
breakBefore: { enabled: true, parity: 'any' } }; // 'any': each piece opens on the next page
يرقّم numberingTemplate: '{1:I}' في المستوى المقالاتِ I وII وIII، ويطبع {number} الرقم فوق العنوان (الامتداد والتصميم المتقدم). يحجز minHeight اثني عشر سطرًا من 14 pt، وينتهي الخط الشعري قبل نهاية هذا الحجز بـ 7 مم، فيبدأ النص تحت عنوان من سطر واحد على بعد 79 مم من الحافة العليا في كل صفحة افتتاح. يضيف سطر ثانٍ من عنوان بحجم 26 pt مقدار 9.9 مم ويدفع النص خطًّا واحدًا من الشبكة إلى الأسفل؛ ومع SINK = 14 تبدأ العناوين ذات السطر والسطرين نصّها على السطر نفسه. ويمنع marginBottom: pt(0) الهامشَ الافتراضي للعنوان من أن يُضاف إلى الحجز.
#2 · دع المحتويات تملأ نفسها
const ENTRY = 15; // pt: the essay titles in the contents
const MIDDLE = 0.3125; // em: how far Chrome's textBaseline 'middle' sits above Gloock's baseline
const contents = { // passed to the config as `toc`
// 1.4.1 centres an entry's number 0.3 × the entry size above its baseline (gotcha:
// toc-number-baseline): at 0.3 × 15 ÷ 0.3125 = 14.4 pt a Gloock numeral stands on it.
levels: [{ level: 1, fontFamily: 'Gloock', fontSize: pt(ENTRY), color: col('ink'),
numberFontSize: pt((0.3 * ENTRY) / MIDDLE), numberFontWeight: 400, // Gloock has one weight
numberColor: col('rust'), numberWidth: mm(8), numberGap: mm(3), marginBottom: pt(LEAD) }],
pageNumber: { fontFamily: 'Hanken Grotesk', fontSize: pt(8.5), fontWeight: 600,
color: col('muted'), width: mm(6) }, // the leader dots take this face and colour too
leader: { char: '. ', gap: mm(2) }, // spaced dots, right-aligned so they line up
subtitle: authorLine,
};
يطبع :::toc في Markdown مدخلًا لكل عنوان مُدرج: الرقم، والعنوان، ونقاط الإرشاد، ورقم الصفحة، واسم الكاتب عبر subtitle. يعيد buildDocument إخراج الكتاب حتى تكف أرقام الصفحات عن التغيّر، فإذا غيّرت اسم مقالة أو أطلتها تحدّثت المحتويات (جدول المحتويات). يضع Postext 1.4.1 منتصف رقم المدخل فوق خط الأساس بمقدار 0.3 × حجم المدخل، ويضع Chrome منتصف Gloock فوق خط أساسه بمقدار 0.3125 em، فيقف رقم بحجم 0.3 × 15 ÷ 0.3125 = 14.4 pt على خط أساس العنوان ذي 15 pt؛ أما الرقم بحجم 12 pt فيعلو 0.3 مم. والكتاب الذي فيه أجزاء يأخذ أيضًا صفًّا لكل :::part، يرسمه toc.parts.design، كما في أجزاء بالألوان.
#3 · الكاتب على الزوجية والمقالة على الفردية
// Each head: the label face, the pages of its parity, never an opener (pages: 'body'), where
// the byline names the author. A function declaration, so the answer above can call it.
function head(id, content, parity, placement, look = {}) {
return { ...text(id, content, { ...label, fontSize: pt(7.5), letterSpacing: pt(1.3),
color: col('muted'), ...look }, placement), parity, pages: 'body' };
}
const folio = { color: col('ink') };
const header = { elements: [
head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, HEAD.y), folio),
versoAuthor,
head('recto-title', '{chapterTitle}', 'odd',
at('page', 'top-right', -(MARGIN.outer + HEAD.gap), HEAD.y), { align: 'right' }),
head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, HEAD.y),
{ ...folio, align: 'right' }),
] };
const footer = { elements: [{ ...head('drop-folio', '{pageNumber}', 'all', // an opener's folio
at('container', 'top', 0, 9), { ...folio, align: 'center' }), pages: 'opener' }] };
كل ترويسة مثبتة إلى الصفحة، على بعد 11 مم من الحافة العليا، ويختار parity الجانب: يصطف رقم الصفحة مع الحافة الخارجية للنص، ويقع الاسم أو العنوان أبعد منه إلى الداخل بـ 7 مم. يُبعد pages: 'body' في head() الترويسات عن صفحات الافتتاح، حيث يذكر سطر الكاتب اسمه أصلًا، ويطبع pages: 'opener' في التذييل رقم صفحة منخفضًا هناك بدلًا منها (العناصر النصية). ويطبع {chapterTitle} عنوان المقالة بلا رقمها.
#4 · أخرج الغلاف والمحتويات من العدّ
// Unnumbered, so the first essay is I; unlisted; and with no running heads or folio. They
// inherit the level's page break, 'any' (gotcha: style-inherits-break).
const bare = { numbered: false, toc: false, header: { elements: [] }, footer: { elements: [] } };
const cover = { enabled: true, slot: { elements: [
{ kind: 'image', id: 'art', resourceId: 'cover',
placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: 'fill' } } },
text('title', '{titleText}', { ...gloock, fontSize: pt(80), lineHeight: 1, color: col('ink') },
at('page', 'top-left', MARGIN.inner, 22)), // page 1 is a recto: the inner margin is left
text('subtitle', '{subtitle}', { fontFamily: 'Spectral', italic: true, fontSize: pt(14),
color: col('ink'), align: 'left' }, below('title', 1)),
text('authors', '{attr.authors}', { ...label, fontSize: pt(8.5), letterSpacing: pt(2),
color: col('heather') }, below('subtitle', 5)),
text('imprint', '{attr.imprint}', { ...label, fontSize: pt(7.5), letterSpacing: pt(1.8),
color: col('paper') }, at('page', 'bottom-left', MARGIN.inner, -12)),
] } };
// span: 'page' in a one-column book: a design kept in the column is clipped at the column's top
// and bottom edges, which would leave bands of paper above and below the dusk.
const coverStyle = { id: 'cover', ...bare, span: 'page', advancedDesign: cover };
const contentsOpener = { enabled: true, slot: { elements: [ // {title}, {subtitle}: frontmatter
text('kicker', '{title} · {subtitle}', { ...label, fontSize: pt(8), letterSpacing: pt(1.6),
color: col('heather') }, at('container', 'top-left', 0, 8)),
text('title', '{titleText}', { ...gloock, fontSize: pt(26), color: col('ink') },
below('kicker', 2.5)),
] } };
مع numbered: false لا يزيد الغلاف ولا المحتويات العدّاد، فتكون مقالة هازلت I، ويُبعدهما toc: false عن القائمة (أنماط العناوين). يرث النمطان breakBefore من المستوى، بزوجية 'any'، ولهذا تأتي المحتويات بعد الغلاف في الصفحة 2 بلا :::pagebreak. ويمتد نمط الغلاف عبر الصفحة في كتاب ذي عمود واحد، لأن التصميم الذي يبقى داخل العمود يُقص عند حافتي العمود العليا والسفلى، فيترك 20 مم من الورق فوق المنظر.
#5 · اكتب كل لون صراحة مع رابطه في لوحة الألوان
const palette = {
ink: '#241f26', // text: a plum-tinted near-black
paper: '#f7f2e8', // the page
heather: '#6b4468', // the bylines and the authors in the contents
rust: '#a4502a', // the essay numbers
rule: '#d5cabd', // hairlines
muted: '#6d6570', // running heads, datelines, page numbers in the contents
};
// 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).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
// The engine's defaults link to 'main-color': point it at the ink, so nothing prints blue.
{ id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } },
];
في 1.4.1 يصل colorPalette إلى أنماط النص لا إلى عناصر التصميم، فتكتب col() القيمة الست عشرية لكل لون بجانب paletteId الخاص به: أسطر الكتّاب والترويسات والغلاف ترسم بتلك القيمة. وتوجيه main-color إلى لون الحبر يغطي كل قيمة افتراضية لا يعيد الإعداد ذكرها، وكانت ستُطبع لولا ذلك بأزرق المحرّك، #295AA3.
الوصفة كاملة
// ═══ Postext Cookbook · Nº 030 · Anthology with bylines ═══════════════════════════════ // https://postext.dev/en/cookbook/anthology-with-bylines // Code: MIT · Text: Hazlitt, Thoreau, Stevenson (public domain) · Cover: diffusion models // Fonts: Spectral, Gloock, Hanken Grotesk (SIL OFL 1.1) · Needs postext ≥ 1.4.1 import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerResourceImage } from 'https://esm.sh/postext'; const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'anthology-with-bylines'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: heather for the credits, rust for the numbers, plum ink on warm paper const palette = { ink: '#241f26', // text: a plum-tinted near-black paper: '#f7f2e8', // the page heather: '#6b4468', // the bylines and the authors in the contents rust: '#a4502a', // the essay numbers rule: '#d5cabd', // hairlines muted: '#6d6570', // running heads, datelines, page numbers in the contents }; // 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).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })), // The engine's defaults link to 'main-color': point it at the ink, so nothing prints blue. { id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } }, ]; // #endregion const TRIM = { width: 135, height: 180 }; // a pocket book, 3 : 4 const MARGIN = { top: 20, bottom: 22, inner: 19, outer: 15 }; // mirrored const LEAD = 14; // body leading in pt: the baseline grid const SINK = 12; // grid lines an essay opener reserves above its first line of text const HEAD = { y: 11, gap: 7 }; // running heads: mm from the top edge, folio to words const label = { fontFamily: 'Hanken Grotesk', fontWeight: 600, textTransform: 'uppercase', align: 'left' }; // design text is centred by default // One weight, no italic; 'wrap' breaks a long title (gotcha: overflow-ellipsis-default). const gloock = { fontFamily: 'Gloock', overflow: 'wrap', align: 'left' }; const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } }); const below = (id, y, size) => ({ ...at(`#${id}`, 'below', 0, y), ...(size && { size }) }); const text = (id, content, look, placement) => ({ kind: 'text', id, content, ...look, placement }); // #region answer: one set of heading attributes, read in three places // Each essay's heading carries its credits, and every {attr.…} below reads them: // # Walking {author="Henry David Thoreau" year="1862" source="The Atlantic Monthly"} // (a value cannot hold { or }, and one with " goes in single quotes; gotcha: attr-values) // 1 · The opener: inside a heading's design, {attr.author} is that heading's attribute. const byline = [ text('byline', '{attr.author}', { ...label, fontSize: pt(8), letterSpacing: pt(1.6), color: col('heather') }, below('title', 5)), text('dateline', '{attr.source}, {attr.year}', { fontFamily: 'Spectral', italic: true, fontSize: pt(9.5), color: col('muted'), align: 'left' }, below('byline', 1.2)), ]; // 2 · The verso running head (head() is in the running-heads region): in the page header, // {attr.author} is the attribute of the essay the page belongs to. const versoAuthor = head('verso-author', '{attr.author}', 'even', at('page', 'top-left', MARGIN.outer + HEAD.gap, HEAD.y)); // 3 · The contents: toc.subtitle prints the attribute it names as a line under each title. // 'author' and italic are the defaults; attr is written out so it can become 'source'. const authorLine = { enabled: true, attr: 'author', fontFamily: 'Spectral', fontSize: pt(10), color: col('heather') }; // Hooked up below: byline → the opener, versoAuthor → header, authorLine → toc.subtitle. // A heading without author="…" gets an empty byline and running head and no line in the // contents, and the build gives no warning: check every heading. // #endregion // #region opener: the essay's number and title, then the byline, over a sunk first line const opener = { enabled: true, // The hairline ends 7 mm above the foot of this reserve, so the text starts on the same grid // line under every opener whose title fits on one line (SINK = 14 holds a two-line title). minHeight: pt(SINK * LEAD), slot: { elements: [ // {number} prints numberingTemplate '{1:I}': I, II, III. {numberRoman} would print // nothing here: it is filled on part pages only (gotcha: heading-number-placeholders). text('number', '{number}', { ...gloock, fontSize: pt(34), lineHeight: 1, color: col('rust') }, at('container', 'top-left', 0, 8)), text('title', '{titleText}', { ...gloock, fontSize: pt(26), lineHeight: 1.08, color: col('ink') }, below('number', 3, { width: 'fill' })), ...byline, { kind: 'rule', id: 'rule', thickness: pt(0.5), color: col('rule'), placement: below('dateline', 6) }, // a horizontal rule runs to the column's edge ] } }; const essays = { level: 1, numberingTemplate: '{1:I}', advancedDesign: opener, marginBottom: pt(0), // the heading's default margin would add to minHeight // Restated (gotcha: headings-drop-h1-break); the cover and contents styles inherit it too. breakBefore: { enabled: true, parity: 'any' } }; // 'any': each piece opens on the next page // #endregion // #region contents: the essays' numbers, titles, leaders and page labels, from the headings const ENTRY = 15; // pt: the essay titles in the contents const MIDDLE = 0.3125; // em: how far Chrome's textBaseline 'middle' sits above Gloock's baseline const contents = { // passed to the config as `toc` // 1.4.1 centres an entry's number 0.3 × the entry size above its baseline (gotcha: // toc-number-baseline): at 0.3 × 15 ÷ 0.3125 = 14.4 pt a Gloock numeral stands on it. levels: [{ level: 1, fontFamily: 'Gloock', fontSize: pt(ENTRY), color: col('ink'), numberFontSize: pt((0.3 * ENTRY) / MIDDLE), numberFontWeight: 400, // Gloock has one weight numberColor: col('rust'), numberWidth: mm(8), numberGap: mm(3), marginBottom: pt(LEAD) }], pageNumber: { fontFamily: 'Hanken Grotesk', fontSize: pt(8.5), fontWeight: 600, color: col('muted'), width: mm(6) }, // the leader dots take this face and colour too leader: { char: '. ', gap: mm(2) }, // spaced dots, right-aligned so they line up subtitle: authorLine, }; // #endregion // #region running-heads: author on the verso, essay title on the recto, folios outside // Each head: the label face, the pages of its parity, never an opener (pages: 'body'), where // the byline names the author. A function declaration, so the answer above can call it. function head(id, content, parity, placement, look = {}) { return { ...text(id, content, { ...label, fontSize: pt(7.5), letterSpacing: pt(1.3), color: col('muted'), ...look }, placement), parity, pages: 'body' }; } const folio = { color: col('ink') }; const header = { elements: [ head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, HEAD.y), folio), versoAuthor, head('recto-title', '{chapterTitle}', 'odd', at('page', 'top-right', -(MARGIN.outer + HEAD.gap), HEAD.y), { align: 'right' }), head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, HEAD.y), { ...folio, align: 'right' }), ] }; const footer = { elements: [{ ...head('drop-folio', '{pageNumber}', 'all', // an opener's folio at('container', 'top', 0, 9), { ...folio, align: 'center' }), pages: 'opener' }] }; // #endregion // #region front: the cover and the contents page, two headings kept out of the count // Unnumbered, so the first essay is I; unlisted; and with no running heads or folio. They // inherit the level's page break, 'any' (gotcha: style-inherits-break). const bare = { numbered: false, toc: false, header: { elements: [] }, footer: { elements: [] } }; const cover = { enabled: true, slot: { elements: [ { kind: 'image', id: 'art', resourceId: 'cover', placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: 'fill' } } }, text('title', '{titleText}', { ...gloock, fontSize: pt(80), lineHeight: 1, color: col('ink') }, at('page', 'top-left', MARGIN.inner, 22)), // page 1 is a recto: the inner margin is left text('subtitle', '{subtitle}', { fontFamily: 'Spectral', italic: true, fontSize: pt(14), color: col('ink'), align: 'left' }, below('title', 1)), text('authors', '{attr.authors}', { ...label, fontSize: pt(8.5), letterSpacing: pt(2), color: col('heather') }, below('subtitle', 5)), text('imprint', '{attr.imprint}', { ...label, fontSize: pt(7.5), letterSpacing: pt(1.8), color: col('paper') }, at('page', 'bottom-left', MARGIN.inner, -12)), ] } }; // span: 'page' in a one-column book: a design kept in the column is clipped at the column's top // and bottom edges, which would leave bands of paper above and below the dusk. const coverStyle = { id: 'cover', ...bare, span: 'page', advancedDesign: cover }; const contentsOpener = { enabled: true, slot: { elements: [ // {title}, {subtitle}: frontmatter text('kicker', '{title} · {subtitle}', { ...label, fontSize: pt(8), letterSpacing: pt(1.6), color: col('heather') }, at('container', 'top-left', 0, 8)), text('title', '{titleText}', { ...gloock, fontSize: pt(26), color: col('ink') }, below('kicker', 2.5)), ] } }; // #endregion const config = () => ({ // a factory: the engine caches resolved configs per object colorPalette, 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 } }, // left = inner layout: { layoutType: 'single' }, bodyText: { fontFamily: 'Spectral', fontSize: pt(10), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'), firstLineIndent: mm(4), indentAfterHeading: false, minWordSpacing: 0.7, maxWordSpacing: 1.6, // tighter than the 0.6–2 defaults maxRuntTracking: 0 }, // tracking 1.4.1 never paints (gotcha: runt-tracking-unpainted) // The hidden heading line is still measured, in this face; otherwise the build needs Open Sans. headings: { fontFamily: 'Gloock', fontWeight: 400, levels: [essays] }, headingStyles: [coverStyle, { id: 'contents', ...bare, advancedDesign: contentsOpener }], toc: contents, // Quoted verse, one paragraph per line (a paragraph keeps no line breaks, and 1.4.1 prints a // Markdown blockquote in a fixed #666666 grey); 'runon' resumes the sentence after it. paragraphStyles: [{ id: 'verse', firstLineIndent: mm(8), textAlign: 'left' }, { id: 'runon', firstLineIndent: pt(0) }, // In the note, under a :::space: a style's margins do not count inside a box (gotcha: // box-paragraph-margins). It takes the note body's indent, 0. { id: 'colophon', fontSize: pt(7.5), lineHeight: pt(10.5), color: col('muted') }], calloutStyles: [{ id: 'note', placement: 'fixed', backgroundEnabled: false, // the page foot stripe: { enabled: true, side: 'top', width: pt(0.5), color: col('rule') }, padding: { top: mm(3), right: pt(0), bottom: pt(0), left: pt(0) }, titleStyle: { ...label, fontSize: pt(7.5), letterSpacing: pt(1.5), color: col('heather') }, body: { fontSize: pt(9), lineHeight: pt(12.5), firstLineIndent: pt(0) } }], header, footer, }); // #region art: the cover, a painting: a JPEG in assets/ cut to the trim's 3 : 4 // The design's image element names the resource by id; loadImage() below registers the file // under its fileId. A bitmap is declared at its pixels. const resources = [{ id: 'cover', typeId: 'figure', kind: 'bitmap', createdAt: 0, updatedAt: 0, bitmap: { fileId: 'cover-1200.jpg', format: 'jpeg', width: 1200, height: 1600 }, altText: 'Heath and hills at dusk under a pale apricot sky, a low sun on the horizon, three ' + 'trees on a ridge and a footpath winding up from the foot of the page through the ' + 'heather.' }]; // #endregion // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---نموذج Markdown · أسطر: 67 · content.en.md
title: "Afoot" subtitle: "Three essays on walking" --- # Afoot {style="cover" authors="Hazlitt · Thoreau · Stevenson" imprint="The Fieldpath Library"} # Contents {style="contents"} :::toc :::callout{type="note" title="A note on the texts"} Each of these essays was first printed in a magazine, two in London and the third in Boston. Hazlitt’s appeared in *The New Monthly Magazine* in January 1822. Thoreau worked his up from a lecture he first gave in 1851, and *The Atlantic Monthly* printed it in June 1862, a month after his death. Stevenson’s came out in *The Cornhill Magazine* in 1876, and in its second paragraph he quotes Hazlitt by name. Each essay starts at its first line and stops well short of its last, in its author’s own spelling; […] marks a cut within a paragraph. :::space{lines=1} :::paragraphs{style="colophon"} Set in Spectral, Gloock and Hanken Grotesk (SIL Open Font License). The essays are in the public domain, from Project Gutenberg eBooks #3020, #1022 and #386; this note is CC BY 4.0; the cover is a painting. ::: ::: # On Going a Journey {author="William Hazlitt" year="1822" source="The New Monthly Magazine"} One of the pleasantest things in the world is going a journey; but I like to go by myself. I can enjoy society in a room; but out of doors, nature is company enough for me. I am then never less alone than when alone. :::paragraphs{style="verse"} *The fields his study, nature was his book.* ::: I cannot see the wit of walking and talking at the same time. When I am in the country I wish to vegetate like the country. I am not for criticising hedge-rows and black cattle. I go out of town in order to forget the town and all that is in it. There are those who for this purpose go to watering-places, and carry the metropolis with them. I like more elbow-room and fewer encumbrances. I like solitude, when I give myself up to it, for the sake of solitude; nor do I ask for :::paragraphs{style="verse"} *A friend in my retreat,* *Whom I may whisper solitude is sweet.* ::: The soul of a journey is liberty, perfect liberty, to think, feel, do, just as one pleases. We go a journey chiefly to be free of all impediments and of all inconveniences; to leave ourselves behind much more to get rid of others. It is because I want a little breathing-space to muse on indifferent matters, where Contemplation :::paragraphs{style="verse"} *May plume her feathers and let grow her wings,* *That in the various bustle of resort* *Were all too ruffled, and sometimes impair’d,* ::: :::paragraphs{style="runon"} that I absent myself from the town for a while, without feeling at a loss the moment I am left by myself. Instead of a friend in a postchaise or in a Tilbury, to exchange good things with, and vary the same stale topics over again, for once let me have a truce with impertinence. Give me the clear blue sky over my head, and the green turf beneath my feet, a winding road before me, and a three hours’ march to dinner—and then to thinking! It is hard if I cannot start some game on these lone heaths. I laugh, I run, I leap, I sing for joy. […] ::: # Walking {author="Henry David Thoreau" year="1862" source="The Atlantic Monthly"} I wish to speak a word for Nature, for absolute Freedom and Wildness, as contrasted with a freedom and culture merely civil,—to regard man as an inhabitant, or a part and parcel of Nature, rather than a member of society. I wish to make an extreme statement, if so I may make an emphatic one, for there are enough champions of civilization: the minister and the school committee and every one of you will take care of that. I have met with but one or two persons in the course of my life who understood the art of Walking, that is, of taking walks—who had a genius, so to speak, for sauntering, which word is beautifully derived “from idle people who roved about the country, in the Middle Ages, and asked charity, under pretense of going à la Sainte Terre,” to the Holy Land, till the children exclaimed, “There goes a Sainte-Terrer,” a Saunterer, a Holy-Lander. They who never go to the Holy Land in their walks, as they pretend, are indeed mere idlers and vagabonds; but they who do go there are saunterers in the good sense, such as I mean. Some, however, would derive the word from sans terre without land or a home, which, therefore, in the good sense, will mean, having no particular home, but equally at home everywhere. For this is the secret of successful sauntering. He who sits still in a house all the time may be the greatest vagrant of all; but the saunterer, in the good sense, is no more vagrant than the meandering river, which is all the while sedulously seeking the shortest course to the sea. But I prefer the first, which, indeed, is the most probable derivation. For every walk is a sort of crusade, preached by some Peter the Hermit in us, to go forth and reconquer this Holy Land from the hands of the Infidels. It is true, we are but faint-hearted crusaders, even the walkers, nowadays, who undertake no persevering, never-ending enterprises. Our expeditions are but tours, and come round again at evening to the old hearth-side from which we set out. Half the walk is but retracing our steps. We should go forth on the shortest walk, perchance, in the spirit of undying adventure, never to return,—prepared to send back our embalmed hearts only as relics to our desolate kingdoms. If you are ready to leave father and mother, and brother and sister, and wife and child and friends, and never see them again,—if you have paid your debts, and made your will, and settled all your affairs, and are a free man; then you are ready for a walk. To come down to my own experience, my companion and I, for I sometimes have a companion, take pleasure in fancying ourselves knights of a new, or rather an old, order—not Equestrians or Chevaliers, not Ritters or Riders, but Walkers, a still more ancient and honorable class, I trust. The chivalric and heroic spirit which once belonged to the Rider seems now to reside in, or perchance to have subsided into, the Walker—not the Knight, but Walker Errant. He is a sort of fourth estate, outside of Church and State and People. We have felt that we almost alone hereabouts practiced this noble art; though, to tell the truth, at least if their own assertions are to be received, most of my townsmen would fain walk sometimes, as I do, but they cannot. No wealth can buy the requisite leisure, freedom, and independence which are the capital in this profession. It comes only by the grace of God. It requires a direct dispensation from Heaven to become a walker. You must be born into the family of the Walkers. Ambulator nascitur, non fit. Some of my townsmen, it is true, can remember and have described to me some walks which they took ten years ago, in which they were so blessed as to lose themselves for half an hour in the woods; but I know very well that they have confined themselves to the highway ever since, whatever pretensions they may make to belong to this select class. No doubt they were elevated for a moment as by the reminiscence of a previous state of existence, when even they were foresters and outlaws. # Walking Tours {author="Robert Louis Stevenson" year="1876" source="The Cornhill Magazine"} It must not be imagined that a walking tour, as some would have us fancy, is merely a better or worse way of seeing the country. There are many ways of seeing landscape quite as good; and none more vivid, in spite of canting dilettantes, than from a railway train. But landscape on a walking tour is quite accessory. He who is indeed of the brotherhood does not voyage in quest of the picturesque, but of certain jolly humours—of the hope and spirit with which the march begins at morning, and the peace and spiritual repletion of the evening’s rest. He cannot tell whether he puts his knapsack on, or takes it off, with more delight. […] Now, to be properly enjoyed, a walking tour should be gone upon alone. If you go in a company, or even in pairs, it is no longer a walking tour in anything but name; it is something else and more in the nature of a picnic. A walking tour should be gone upon alone, because freedom is of the essence; because you should be able to stop and go on, and follow this way or that, as the freak takes you; and because you must have your own pace, and neither trot alongside a champion walker, nor mince in time with a girl. And then you must be open to all impressions and let your thoughts take colour from what you see. You should be as a pipe for any wind to play upon. “I cannot see the wit,” says Hazlitt, “of walking and talking at the same time. When I am in the country I wish to vegetate like the country,”—which is the gist of all that can be said upon the matter. There should be no cackle of voices at your elbow, to jar on the meditative silence of the morning. And so long as a man is reasoning he cannot surrender himself to that fine intoxication that comes of much motion in the open air, that begins in a sort of dazzle and sluggishness of the brain, and ends in a peace that passes comprehension.`; // content.<lang>.md, inlined by the Cookbook // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { // text, display and label faces (gotcha: fonts-first) Spectral: ['400', '400i'], Gloock: ['400'], 'Hanken Grotesk': ['600'] }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadImage('cover-1200.jpg', asset('cover-1200.jpg')); await loadFonts(FONTS, markdown); const doc = await buildWithFonts(() => buildDocument({ markdown, resources }, config()), markdown); showPages(doc, { title: t({ en: 'Anthology with bylines', es: 'Antología con firmas de autor' }) });العُدّة · core, fonts, viewer, images: نفسها في كل وصفة · أسطر: 270
// ─── 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 · images v1 ── recipes with pictures · postext.dev/cookbook ────────── /** Registers a photo or PNG for the canvas and keeps its bytes for the PDF. * fetch → ImageBitmap never taints the canvas (a plain cross-origin <img> would). */ async function loadImage(fileId, url) { const res = await fetch(url); if (!res.ok) throw new Error(`Image not found (${res.status}): ${url}`); const bytes = new Uint8Array(await res.arrayBuffer()); registerResourceImage(fileId, await createImageBitmap(new Blob([bytes]))); (loadImage.bytes ??= new Map()).set(fileId, bytes); } /** Registers SVG markup (drawn in code, or fetched) as a vector image. */ async function loadSvg(fileId, svg) { const img = new Image(); img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; await img.decode(); registerResourceImage(fileId, img); (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg)); } /** renderToPdf({ resourceBytes: imageBytes }) */ function imageBytes(fileId) { return loadImage.bytes?.get(fileId); } /** renderToHtml({ resourceImageUrl: imageUrl }) */ function imageUrl(fileId) { const bytes = imageBytes(fileId); if (!bytes) return undefined; imageUrl.urls ??= new Map(); if (!imageUrl.urls.has(fileId)) { const type = /\.svg$/i.test(fileId) ? 'image/svg+xml' : /\.png$/i.test(fileId) ? 'image/png' : 'image/jpeg'; imageUrl.urls.set(fileId, URL.createObjectURL(new Blob([bytes], { type }))); } return imageUrl.urls.get(fileId); } // ─── /Kit ───────────────────────────────────────────────────────────────────────
يعمل ملف script.js المجمّع كما هو: الصقه في سكربت الوحدة (module) لأي صفحة، أو افتح الوصفة على CodePen. مجلد الوصفة على GitHub ↗ (يفتح في تبويب جديد)
تنويعات
#اذكر أين نُشرت كل مقالة أول مرة
يطبع toc.subtitle أي سمة من سمات العنوان، فيستطيع Markdown نفسه أن يذكر تحت عنوان كل مقالة المجلة التي نشرتها أولًا.
-const authorLine = { enabled: true, attr: 'author', fontFamily: 'Spectral', fontSize: pt(10),
+const authorLine = { enabled: true, attr: 'source', fontFamily: 'Spectral', fontSize: pt(10),#ضع عنوان الكتاب على الصفحة الزوجية
كثير من الكتب تضع عنوانها على الصفحة الزوجية وعنوان الفصل على الفردية؛ يقرأ {title} العنوان من البيانات التمهيدية لملف Markdown ويطبع AFOOT.
-const versoAuthor = head('verso-author', '{attr.author}', 'even',
+const versoAuthor = head('verso-author', '{title}', 'even',#افتتح كل مقالة على صفحة فردية
مع الزوجية 'odd' تنتقل مقالة ستيفنسون إلى الصفحة 9 بعد صفحة 8 فارغة. وعندها يحتاج نمطا الصفحات التمهيدية إلى 'any' خاص بهما، وإلا ورثت المحتويات 'odd' وافتُتحت في الصفحة 3 بعد صفحة زوجية فارغة.
- breakBefore: { enabled: true, parity: 'any' } }; // 'any': each piece opens on the next page
+ breakBefore: { enabled: true, parity: 'odd' } };
-const bare = { numbered: false, toc: false, header: { elements: [] }, footer: { elements: [] } };
+const bare = { numbered: false, toc: false, breakBefore: { enabled: true, parity: 'any' },
+ header: { elements: [] }, footer: { elements: [] } };أخطاء شائعة
خطأ شائع
قيم السمات: لا { ولا }؛ ضع القيمة التي فيها " بين علامتي اقتباس مفردتين
تنتهي قيمة السمة عند القوس المعقوف الختامي، فلا يمكن أن تحتوي { أو }. القيمة التي فيها علامة اقتباس مزدوجة توضع بين علامتي اقتباس مفردتين؛ أما علامة الدولار فلا مشكلة فيها. سمات العنوان →
خطأ شائع
أي كائن headings يُلغي فاصل الصفحة قبل H1
ينتقل H1 افتراضيًا إلى صفحة فردية (always-odd)، لكن تمرير أي كائن headings يعيد ضبط هذا الافتراض، فتتوالى الفصول دون فاصل ولا يفعل span: 'page' شيئًا. أعد كتابة headings.levels[0].breakBefore: { enabled: true, parity } في كل إعداد. فصول تبدأ في صفحة فردية →
خطأ شائع
يرث نمط العنوان فاصل الصفحة من مستواه
يأخذ مُدخل headingStyles كل حقل يتركه من مستوى عنوانه، بما فيه breakBefore. فصفحة المحتويات أو صفحة بيانات النشر المنسّقة على H1 بعد :::pagebreak ترث الزوجية 'odd' وتقع بعد صفحة فارغة. أعطِ هذا النمط breakBefore: { enabled: false }. أنماط العناوين →
خطأ شائع
أرقام جدول المحتويات تقع أعلى قليلًا من خط أساس المُدخل
في postext 1.4.1 يرسم :::toc رقم كل مُدخل متمركزًا على السطر بدل أن يضعه على خط أساس النص، فترتفع أرقام الفصول قليلًا بجانب عناوينها، نحو 0.7 mm بجانب عنوان بمقاس 16 pt، أيًّا كان الخط أو المقاس الذي تعطيه لها. لا يوجد بعدُ خيار في toc يحرّكها، فانظر إلى صفحة المحتويات بمقاسها الكامل قبل الطباعة. جدول المحتويات →
خطأ شائع
{number}/{chapterNumber} يطبعان رقم H1؛ و{numberRoman} للأجزاء فقط
يطبع {number} و{chapterNumber} الرقم المنسَّق للعنوان، أما {numberRoman} و{numberDecimal} وسائر الصيغ الرقمية فلا تُملأ إلا في صفحات الأجزاء. نسّق رقم الفصل في numberingTemplate الخاص به ({1:I}) أو مرّره سمةً. العناوين المرقّمة →
خطأ شائع
لوحة الألوان المستبدلة لا تصل إلى عناصر التصميم ولا إلى لون الإحالة
يقرأ postext 1.4.1 الإعداد colorPalette في أنماط النص (المتن والعناوين والقوائم والتعليقات والجداول والإطارات) لكن لا في عناصر الترويسات والتذييلات والافتتاحيات وصفحات الأجزاء، ولا في bodyText.referenceColor: تحتفظ بالقيمة الست عشرية المكتوبة بجانب paletteId الخاص بها. حين تستبدل لوحة الألوان، لنسخة شاشة داكنة أو لإعادة تلوين، أعِد كتابة كل لون مرتبط من colorPalette قبل البناء. لوحة ألوان دلالية →
خطأ شائع
هوامش نمط الفقرة لا تُحتسب داخل الإطار
في postext 1.4.1 تتجاهل حاوية :::paragraphs المتداخلة في :::callout القيمتين marginTop وmarginBottom لنمطها، فسطر الحرف الصغير الموضوع تحت نص الملاحظة يلتصق به. أعطِ النمط lineHeight أكبر، فيضع فراغًا فوق سطره الأول، أو أبقِ السطر خارج الإطار. أنماط الفقرات →
خطأ شائع
إصلاح الكلمة المعزولة قد يضيّق تتبّعًا لا يُرسَم أبدًا
في postext 1.4.1، حين تنتهي فقرة بكلمة معزولة في آخرها، يجعلها الإخراج أقصر بسطر: أولًا بتضييق المسافات بين الكلمات، ثم بتتبّع سالب يصل إلى maxRuntTracking من الألف من em. لكن مُخرِجَي Canvas وPDF لا يرسمان التتبّع إلا إذا كان فوق الصفر، فتُطبع الفقرة المتتبَّعة بلا تتبّع: تفقد أسطرها المضبوطة الفرق من مسافات كلماتها فتبدو مضغوطة، وقد يتجاوز سطرها الأخير عرض السطر فيُقصّ عند حافة العمود. اضبط bodyText.maxRuntTracking: 0، فيبقى إصلاح المسافات بين الكلمات، وأعد صياغة أي كلمة معزولة تعود. الأسطر الأرامل والأسطر اليتيمة والكلمات المعزولة →
خطأ شائع
حمّل كل أوجه الخط قبل الإخراج
يقيس الإخراج النص بأوجه الخط التي حمّلها المتصفح ويخزّن العروض مؤقتًا، فالوجه الذي يصل بعد البناء الأول يترك فواصل أسطر خاطئة وملف PDF لم يعد يطابق الشاشة. حمّل كل وزن وكل نمط أولًا، واستدعِ clearMeasurementCache() قبل إعادة البناء إذا تأخر وصول أحدها. الخطوط قبل الإخراج →
خطأ شائع
فيض نص التصميم افتراضيًا 'ellipsis-end'
عنصر نص التصميم الذي لا يتسع له عرضه ينتهي افتراضيًا بعلامة الحذف. اضبط overflow: 'wrap' للعناوين التي يجب أن تنكسر على أسطر أكثر. النصوص والخطوط والمربعات في تصاميم الصفحة →
- يقتبس هازلت شعرًا في وسط جمله. والفقرة لا تحتفظ بفواصل الأسطر، لذا فكل بيت فقرة مستقلة داخل حاوية
:::paragraphs{style="verse"}، والجملة التي تُستأنف بعده تأخذ نمطrunonبلا إزاحة. أما الاقتباس الكتلي في Markdown فيُطبع بمائل رمادي ثابت#666666، مع إزاحة السطر الأول التي للمتن وبلا إزاحة جانبية، ولا يغيّر ذلك أي إعداد في 1.4.1. - السمة التي يخلو منها العنوان لا تطبع شيئًا ولا تُطلق أي تحذير. فبلا
author="…"يخرج سطر كاتب المقالة وترويستها الزوجية فارغين، ويختفي سطرها تحت العنوان من المحتويات. - الـ 14.4 pt في الخطوة 2 تصحّ مع Gloock وحده. ولخط آخر، اعرف كم يضع Chrome منتصفه فوق خط الأساس: يعيده
measureText('I').alphabeticBaselineمعtextBaseline = 'middle'، بإشارة معكوسة، بالبكسل. ثم اجعلnumberFontSizeيساوي 0.3 × حجم المدخل مقسومًا على تلك المسافة بوحدة em (وهي في Hanken Grotesk 0.2675 em).
الحقوق
- الوصفة
- Ignacio Ferro
- النص
- “On Going a Journey” (1822), from its first line to “I sing for joy”, in Table Talk: Essays on Men and Manners · William Hazlitt · ملك عام
- “Walking” (1862), its first five paragraphs · Henry David Thoreau · ملك عام
- “Walking Tours” (1876), the opening of its first paragraph and the whole of its second, in Virginibus Puerisque · Robert Louis Stevenson · ملك عام
- The note on the texts and the colophon · Ignacio Ferro · CC BY 4.0
- الصور
- The cover: a heath and hills at dusk, a painting · Generated With Diffusion Models · أصلي
- الخطوط
- Spectral (SIL OFL 1.1) · Gloock (SIL OFL 1.1) · Hanken Grotesk (SIL OFL 1.1)
- الكود
- MIT، مثل Postext
حرّر هذا الشرح ↗ (يفتح في تبويب جديد)مجلد الوصفة على GitHub ↗ (يفتح في تبويب جديد)


