باختصار
الصفحات الأولى من طبعة إيطالية للأطفال من Pinocchio. يبدأ كل فصل بصورة مرسومة بألوان هادئة خاصة به وملخّص قصير لما يجري فيه.
ما الذي ستنضده
الصفحات الأولى من طبعة إيطالية للأطفال من رواية Le avventure di Pinocchio لـ Collodi، منضّدة بخط Averia Serif Libre بقياس 13 على 19 pt في صفحة مقاسها 190 × 240 mm. صفحة عنوان على الصفحة الزوجية تقابل الفصل الأول: لوحة مائية للتلال التوسكانية، تحمل فيها عربة حمار الحطب إلى ورشة نجار، فوق العنوان بخط Fredericka the Great. يبدأ كل فصل تحت صورة رأسية بالألوان المائية تتلاشى في الورق (Maestro Ciliegia وفأسه مرفوعة فوق الجذع المتكلّم في الفصل الأول، وGeppetto بشعره المستعار الأصفر يأخذ الجذع في الفصل الثاني)، ثم سطر الفصل وملخّص Collodi بالمائل في الوسط. الصور الأربع كلها مولّدة بنماذج الانتشار من وصف أسلوب واحد ولوحة ألوان الصفحة. لا تُطبع الترويسات إلا في الصفحتين 8 و9، وهما الصفحتان اللتان ليس فيهما صفحة افتتاح. أرقام الصفحات في أقراص بلون الخشب، وحبتا كرز تختمان الفصل الأول من غير تعليق ولا رقم.
تجيب هذه الوصفة عن
- كيف أعطي كل فصل صورته أو لونه أو شكل افتتاحه الخاص؟
- كيف أنضد رسومًا غير مرقّمة: زخارف، وحليات، وشعارات؟
- كيف أضيف إلى صفحة الافتتاح سطر المؤلف أو مقدمة تمهيدية أو فقرة أولى بحرف استهلالي؟
- كيف أخفي الترويسات في صفحات الافتتاح والصفحات الفارغة، أو ألوّن صفحة زوجية فارغة بلون الجزء؟
الجواب المختصر
// # Capitolo I {style="capitolo" vignetta="ceppo" summary="Come andò che Maestro Ciliegia…"}
// resourceId takes placeholders like a design text: '{attr.vignetta}' draws the resource the
// heading names, so every chapter shares one style.
const VIGNETTE = { width: 84, height: 56 }; // mm, centred at the head of the text block
const SUMMARY = 124; // mm: the summary's measure, narrower than the text's
const opener = { enabled: true,
// A floor for the height the opener reserves, in whole lines, so the text under chapter I's
// two-line summary starts on the same grid line as the text under chapter II's three lines.
minHeight: pt(14 * LEAD),
slot: { elements: [
{ kind: 'image', id: 'vignette', resourceId: '{attr.vignetta}',
placement: at('container', 'top', 0, 0,
{ width: mm(VIGNETTE.width), height: mm(VIGNETTE.height) }) },
// Placed from the container, not from #vignette: images do not count in the height an
// opener reserves (gotcha: opener-image-no-reserve), the texts under them do.
text('chapter', '{titleText}', DISPLAY, 24, at('container', 'top-left', trail(2.4),
VIGNETTE.height + 6, { width: mm(MEASURE) }), { ...caps(2.4), lineHeight: 1 }),
text('summary', '{attr.summary}', TEXT, 12.5, at('#chapter', 'below',
(MEASURE - SUMMARY) / 2 - trail(2.4), 5, { width: mm(SUMMARY) }),
{ italic: true, lineHeight: 1.38 }), // a multiple (gotcha: design-lineheight-multiple)
] },
};
const chapter = { id: 'capitolo', advancedDesign: opener };
المكونات
- الميزات
- صفحات افتتاح مصمَّمةالصور في تصاميم الصفحةالترويسات وأرقام الصفحاتأنماط العناوينسمات العنوانفصول تبدأ في صفحة فرديةالنصوص والخطوط والمربعات في تصاميم الصفحةالترويسات بحسب دور الصفحةأنواع موارد مخصّصةأشكال في هذا الموضع بالضبطالأشكال والجداول بوصفها مواردتقسيم الكلمات ولغة المستندهوامش متناظرةلوحة ألوان دلاليةصفحات على اللوحة (Canvas)
- تستخدم أيضًا
- لون الورقأنماط الفقراتترويسات لكل قسم
- الخطوط
- Averia Serif Libre, Fredericka the Great, Quicksand (SIL OFL 1.1)
- الأصول
ceppo-1200.jpgciliegie-600.jpgfrontespizio-1400.jpgpolenta-1200.jpg- The frontispiece: Tuscan hills, a hill village and a donkey cart of firewood on its way to the carpenter’s workshop (Generated With Diffusion Models, أصلي)
- Chapter I’s vignette: Maestro Ciliegia, axe raised over the talking log (Generated With Diffusion Models, أصلي)
- Chapter II’s vignette: Geppetto in his yellow wig receives the log, beside a pot of polenta (Generated With Diffusion Models, أصلي)
- The cherry tailpiece (Generated With Diffusion Models, أصلي)
طريقة التحضير
#1 · نمط عنوان واحد، وصورة رأسية يسمّيها كل عنوان
شيفرة هذه الخطوة هي الجواب المختصر. يقبل resourceId في عنصر الصورة العناصر النائبة نفسها التي يقبلها نص التصميم، فيرسم '{attr.vignetta}' المورد الذي يسمّيه العنوان، كما يطبع {attr.summary} ملخّصه. يتقاسم الفصلان النمط capitolo ويسمّي كل منهما صورته على سطر العنوان: # Capitolo II {style="capitolo" vignetta="polenta" summary="…"}. الفصل الثالث يحتاج موردًا إضافيًا وعنوانًا إضافيًا، لا نمطًا آخر (عناصر الصورة). يوضع سطر الفصل بالقياس من أعلى الحاوية، على بعد 6 mm تحت صندوق الصورة الرأسية، بدلًا من تثبيته على #vignette. الصورة لا تحجز ارتفاعًا، فالنصوص المثبّتة تحتها تُقاس كأن الصورة غير موجودة؛ ومن دون minHeight يبدأ نص الفصل الأول عندئذ على بعد 62 mm من أعلى الصفحة، داخل الرسم. ومع minHeight حدًّا أدنى من 14 سطرًا، يبدأ نص الفصل الأول على السطر نفسه الذي يبدأ عليه نص الفصل الثاني، الذي يزيد ملخّصه سطرًا؛ ومن دونه يبدأ نص الفصل الأول أعلى بـ 6.7 mm (الامتداد والتصميم المتقدم).
#2 · حمّل اللوحات موارد نقطية
const PICTURES = { // id: [file, width, height, alt text in the book's language]
frontespizio: ['frontespizio-1400.jpg', 1400, 1105, 'Colline toscane con cipressi e un borgo; '
+ 'un contadino guida un asinello con un carretto di legna verso la bottega di un falegname.'],
ceppo: ['ceppo-1200.jpg', 1200, 800, 'Maestro Ciliegia, con la scure alzata sopra un pezzo di '
+ 'legno sul banco, si guarda intorno spaventato.'],
polenta: ['polenta-1200.jpg', 1200, 800, 'Geppetto, con la parrucca gialla, riceve il pezzo di '
+ 'legno da Maestro Ciliegia; accanto, un paiolo di polenta fumante.'],
ciliegie: ['ciliegie-600.jpg', 600, 626, 'Due ciliegie rosse su un picciolo, con una foglia.'],
};
const resources = Object.entries(PICTURES).map(([id, [fileId, w, h, altText]]) => ({
id, typeId: 'fregio', kind: 'bitmap', createdAt: 0, updatedAt: 0, altText,
bitmap: { fileId, format: 'jpeg', width: w, height: h } }));
كل صورة ملف JPEG في المجلد assets/ الخاص بالوصفة، مصرَّح بها بمقاسها بالبكسل: لا يقرأ الإخراج إلا هذه المقاسات، وترسم اللوحة (Canvas) ما سجّلته loadImage() تحت معرّف كل ملف حين ترسم الصفحة. وُلّدت الصور الرأسية وحبتا الكرز على خلفية بيضاء ثم ضُربت في لون الورق #fffaf0، فتُطبع حوافها بلون الصفحة ولا تظهر للصور حافة. صورة الواجهة مقصوصة بنسبة 190:150، أي بشكل مساحتها، فتملأ الصورة المحجّمة المساحة من غير شريط من الورق. المنظر الطبيعي والصور الرأسية ترسمها عناصر صورة في التصاميم، وحبتا الكرز يرسمهما ::resource{id="ciliegie"} في Markdown.
#3 · اجعل صفحة العنوان نمط عنوان
const SKY = 150; // mm from the top edge: the foot of the painted field
// # Pinocchio {style="frontespizio" kicker="Le avventure di"}; subtitle and author come from
// the frontmatter. Every element hangs from the page, so the field runs off three edges.
const frontispiece = {
id: 'frontespizio',
span: 'page', // or the field is cut at the top of the text block (gotcha: opener-clipped-at-top)
footer: { elements: [] }, // no folio; the running heads already skip openers
advancedDesign: { enabled: true, slot: { elements: [ // array order is paint order
{ kind: 'image', id: 'landscape', resourceId: 'frontespizio', // cut to the field's 190:150
placement: at('page', 'top-left', 0, 0, { width: 'fill', height: mm(SKY) }) },
text('kicker', '{attr.kicker}', LABEL, 11, at('page', 'top', trail(2.6), SKY + 11),
{ ...caps(2.6), fontWeight: 700 }),
text('title', '{titleText}', DISPLAY, 70, at('page', 'top', 0, SKY + 17),
{ lineHeight: 1, color: col('bark') }),
text('subtitle', '{subtitle}', TEXT, 15, at('page', 'top', 0, SKY + 44), { italic: true }),
text('author', '{author}', LABEL, 9, at('page', 'top', trail(2), SKY + 58),
{ ...caps(2), fontWeight: 600 }),
] } },
};
صفحة العنوان هي أول عنوان في Markdown، # Pinocchio {style="frontespizio" kicker="Le avventure di"}، ونمطها يرسم كل ما في الصفحة: المنظر الطبيعي، بعمق 150 mm من الحافة العليا، ثم السطر التمهيدي من السمة، والعنوان، والعنوان الفرعي والمؤلف من البيانات التمهيدية (frontmatter). يضبط النمط span: 'page' مع أن للكتاب عمودًا واحدًا، لأن التصميم الذي يبقى في العمود يُقصّ عند أعلى كتلة النص، فتُطبع الـ 22 mm العليا من اللوحة ورقًا فارغًا. ويزيل footer الفارغ قرص رقم الصفحة الذي تطبعه صفحات افتتاح الفصول، وpages: 'body' يُبعد الترويسات عن الصفحة أصلًا (أنماط العناوين).
#4 · ترويسات على صفحات النص، وأرقام الصفحات في أقراص
// The excerpt opens on book page 6, a verso, so the title page faces chapter I.
const continuation = { pageIndexOffset: 5, pageNumbering: { startAt: 6 } };
const SHIFT = (INNER - OUTER) / 2; // mm: the text block's centre sits off the page's centre
const head = (id, content, parity, x) => text(id, content, LABEL, 8,
at('page', 'top', x + trail(1.8), 12), { ...caps(1.8), fontWeight: 600, parity, pages: 'body' });
const DISC = 7.4; // mm: the folio's disc
// A fixed square with a radius of half its side is a circle; the figures sit in its middle.
// lineHeight 1.17: the baseline falls at 0.8 of the line, 0.94 em down, so Quicksand's
// figures (0.70 em tall) leave 0.23 em above and below them, and 'middle' centres that line.
const disc = (id, parity, pages, edge, x) => text(id, '{pageNumber}', LABEL, 8.5,
at('page', edge, x, -(BOTTOM - DISC) / 2, { width: mm(DISC), height: mm(DISC) }),
{ fontWeight: 700, color: col('paper'), lineHeight: 1.17, verticalAlign: 'middle', parity,
pages, box: { backgroundColor: col('wood'), borderRadius: mm(DISC / 2) } });
const header = { elements: [
head('verso-title', '{title}', 'even', -SHIFT), // the book on the left-hand page
head('recto-chapter', '{chapterTitle}', 'odd', SHIFT), // the chapter on the right
] };
const footer = { elements: [
disc('verso-folio', 'even', 'body', 'bottom-left', OUTER),
disc('recto-folio', 'odd', 'body', 'bottom-right', -OUTER),
disc('opener-folio', 'all', 'opener', 'bottom', 0), // a chapter opens on either side
] };
يرقّم continuation المقتطف ابتداءً من الصفحة 6 من الكتاب، وهي صفحة زوجية، فتقابل صفحة العنوان الفصل الأول، وأول رقم صفحة مطبوع هو 7. يُبعد pages: 'body' الترويسات عن صفحة العنوان وعن صفحتَي الافتتاح، فلا تُطبع إلا في الصفحتين 8 و9، عنوان الكتاب على الصفحة الزوجية وعنوان الفصل على الفردية (عناصر النص). رقم الصفحة نص تصميم في مربع ثابت ضلعه 7.4 mm، ونصف قطر زواياه نصف ضلعه، فيصير دائرة؛ ويضع verticalAlign: 'middle' وارتفاع السطر المحسوب في التعليق الأرقامَ في وسطها. الأرقام بلون الورق نسبة تباينها 4.98:1 مع لون الخشب. وتحرّك trail() كل تسمية متباعدة الحروف إلى اليمين بنصف تتبّعها، لأن الإصدار 1.4.1 يوسّط السطر المتباعد الحروف كأن المسافة بعد آخر حرف فيه جزء منه.
#5 · اختم الفصل بزخرفة ختامية غير مرقّمة
// No caption prefix and no caption, so the cherries that close chapter I print with no
// label or number. The pictures the design slots paint are of this type too.
const resourceTypes = [{ id: 'fregio', name: 'Fregio', namePlural: 'Fregi', shortLabel: 'Fregio',
captionPrefix: '', numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal',
defaultPlacement: { position: 'here', width: 0.16, align: 'center' } }];
يطبع نوع المورد captionPrefix الخاص به ورقمه قبل التعليق. مع بادئة فارغة ومن غير تعليق لا يُطبع شيء تحت حبتي الكرز؛ ومع captionPrefix: 'Fregio' يطبع المورد نفسه Fregio 1. تحت الرسم ويزيد طوله 10 mm (أنواع الموارد). يضع defaultPlacement في النوع كل مورد حيث يقف سطر ::resource الخاص به، بعرض سدس عرض السطر وفي الوسط، فلا يحتاج Markdown إلا إلى ::resource{id="ciliegie"} بعد آخر فقرة في الفصل. الصور الأخرى تتقاسم النوع، لكن لا :ref ولا ::resource يسمّيها، فلا توضع ولا تُعدّ أبدًا.
الوصفة كاملة
// ═══ Postext Cookbook · Nº 066 · Storybook chapters with vignettes and summaries ═══ // https://postext.dev/en/cookbook/storybook-chapter-vignettes // Code: MIT · Text: C. Collodi, 1883 (PD, Gutenberg #52484) · Pictures: diffusion models // Fonts: Averia Serif Libre, Fredericka the Great, Quicksand (SIL OFL 1.1) · Needs postext ≥ 1.8.0 import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerResourceImage } from 'https://esm.sh/postext'; const LANG = 'en'; // @lang: the language of the sample document (the sample is Italian in both) const RECIPE = 'storybook-chapter-vignettes'; // ─── 1 · Design ───────────────────────────────────────────────────────────── const palette = { // every colour in the config comes from here; the pictures were painted to it ink: '#2b2118', // text: a brown near-black paper: '#fffaf0', // a warm book paper, which the vignettes' margins were tinted to bark: '#6b4224', // chapter lines, running heads, the title wood: '#9a5f30', // the folio discs }; // Each colour names its palette entry and carries its hex (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 defaults linked to main-color, bold among them, print in bark instead of blue. { id: 'main-color', name: 'bark (defaults)', value: { hex: palette.bark, model: 'hex' } }, ]; // Text and summaries; the book's title and the chapter lines; running heads and folios. const [TEXT, DISPLAY, LABEL] = ['Averia Serif Libre', 'Fredericka the Great', 'Quicksand']; const TRIM = { width: 190, height: 240 }; // mm const [TOP, INNER, OUTER] = [22, 22, 20]; // mm; mirrored, so the spine margin swaps sides const MEASURE = TRIM.width - INNER - OUTER; // 148 mm const [BODY, LEAD, LINES] = [13, 19, 29]; // pt, pt, and whole lines in the text block const BOTTOM = TRIM.height - TOP - (LINES * LEAD * 25.4) / 72; // mm: 23.6 // Design-slot shorthands: a placement from an anchor's edge (mm), and a design text. const at = (to, edge, x = 0, y = 0, size) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) }, ...(size && { size }) }); const text = (id, content, fontFamily, size, placement, extra = {}) => ({ kind: 'text', id, content, fontFamily, fontSize: pt(size), color: col('ink'), align: 'center', overflow: 'wrap', // wrap, never '…' (gotcha: overflow-ellipsis-default) placement, ...extra }); const caps = (tracking) => ({ textTransform: 'uppercase', letterSpacing: pt(tracking), color: col('bark') }); // Half a tracking (mm): 1.4.1 tracks after a centred line's last letter, shifting it left. const trail = (tracking) => (tracking * 25.4) / 72 / 2; // #region answer: one opener for every chapter: the vignette the heading names, then its summary // # Capitolo I {style="capitolo" vignetta="ceppo" summary="Come andò che Maestro Ciliegia…"} // resourceId takes placeholders like a design text: '{attr.vignetta}' draws the resource the // heading names, so every chapter shares one style. const VIGNETTE = { width: 84, height: 56 }; // mm, centred at the head of the text block const SUMMARY = 124; // mm: the summary's measure, narrower than the text's const opener = { enabled: true, // A floor for the height the opener reserves, in whole lines, so the text under chapter I's // two-line summary starts on the same grid line as the text under chapter II's three lines. minHeight: pt(14 * LEAD), slot: { elements: [ { kind: 'image', id: 'vignette', resourceId: '{attr.vignetta}', placement: at('container', 'top', 0, 0, { width: mm(VIGNETTE.width), height: mm(VIGNETTE.height) }) }, // Placed from the container, not from #vignette: images do not count in the height an // opener reserves (gotcha: opener-image-no-reserve), the texts under them do. text('chapter', '{titleText}', DISPLAY, 24, at('container', 'top-left', trail(2.4), VIGNETTE.height + 6, { width: mm(MEASURE) }), { ...caps(2.4), lineHeight: 1 }), text('summary', '{attr.summary}', TEXT, 12.5, at('#chapter', 'below', (MEASURE - SUMMARY) / 2 - trail(2.4), 5, { width: mm(SUMMARY) }), { italic: true, lineHeight: 1.38 }), // a multiple (gotcha: design-lineheight-multiple) ] }, }; const chapter = { id: 'capitolo', advancedDesign: opener }; // #endregion // #region frontispiece: the title page as a heading style: a painted field, then the title const SKY = 150; // mm from the top edge: the foot of the painted field // # Pinocchio {style="frontespizio" kicker="Le avventure di"}; subtitle and author come from // the frontmatter. Every element hangs from the page, so the field runs off three edges. const frontispiece = { id: 'frontespizio', span: 'page', // or the field is cut at the top of the text block (gotcha: opener-clipped-at-top) footer: { elements: [] }, // no folio; the running heads already skip openers advancedDesign: { enabled: true, slot: { elements: [ // array order is paint order { kind: 'image', id: 'landscape', resourceId: 'frontespizio', // cut to the field's 190:150 placement: at('page', 'top-left', 0, 0, { width: 'fill', height: mm(SKY) }) }, text('kicker', '{attr.kicker}', LABEL, 11, at('page', 'top', trail(2.6), SKY + 11), { ...caps(2.6), fontWeight: 700 }), text('title', '{titleText}', DISPLAY, 70, at('page', 'top', 0, SKY + 17), { lineHeight: 1, color: col('bark') }), text('subtitle', '{subtitle}', TEXT, 15, at('page', 'top', 0, SKY + 44), { italic: true }), text('author', '{author}', LABEL, 9, at('page', 'top', trail(2), SKY + 58), { ...caps(2), fontWeight: 600 }), ] } }, }; // #endregion // #region folios: running heads on body pages only, folios in wood-coloured discs // The excerpt opens on book page 6, a verso, so the title page faces chapter I. const continuation = { pageIndexOffset: 5, pageNumbering: { startAt: 6 } }; const SHIFT = (INNER - OUTER) / 2; // mm: the text block's centre sits off the page's centre const head = (id, content, parity, x) => text(id, content, LABEL, 8, at('page', 'top', x + trail(1.8), 12), { ...caps(1.8), fontWeight: 600, parity, pages: 'body' }); const DISC = 7.4; // mm: the folio's disc // A fixed square with a radius of half its side is a circle; the figures sit in its middle. // lineHeight 1.17: the baseline falls at 0.8 of the line, 0.94 em down, so Quicksand's // figures (0.70 em tall) leave 0.23 em above and below them, and 'middle' centres that line. const disc = (id, parity, pages, edge, x) => text(id, '{pageNumber}', LABEL, 8.5, at('page', edge, x, -(BOTTOM - DISC) / 2, { width: mm(DISC), height: mm(DISC) }), { fontWeight: 700, color: col('paper'), lineHeight: 1.17, verticalAlign: 'middle', parity, pages, box: { backgroundColor: col('wood'), borderRadius: mm(DISC / 2) } }); const header = { elements: [ head('verso-title', '{title}', 'even', -SHIFT), // the book on the left-hand page head('recto-chapter', '{chapterTitle}', 'odd', SHIFT), // the chapter on the right ] }; const footer = { elements: [ disc('verso-folio', 'even', 'body', 'bottom-left', OUTER), disc('recto-folio', 'odd', 'body', 'bottom-right', -OUTER), disc('opener-folio', 'all', 'opener', 'bottom', 0), // a chapter opens on either side ] }; // #endregion // #region tailpiece: an unnumbered resource type, set where ::resource stands // No caption prefix and no caption, so the cherries that close chapter I print with no // label or number. The pictures the design slots paint are of this type too. const resourceTypes = [{ id: 'fregio', name: 'Fregio', namePlural: 'Fregi', shortLabel: 'Fregio', captionPrefix: '', numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal', defaultPlacement: { position: 'here', width: 0.16, align: 'center' } }]; // #endregion // #region pictures: four paintings, declared at their pixel size under the file ids they load as const PICTURES = { // id: [file, width, height, alt text in the book's language] frontespizio: ['frontespizio-1400.jpg', 1400, 1105, 'Colline toscane con cipressi e un borgo; ' + 'un contadino guida un asinello con un carretto di legna verso la bottega di un falegname.'], ceppo: ['ceppo-1200.jpg', 1200, 800, 'Maestro Ciliegia, con la scure alzata sopra un pezzo di ' + 'legno sul banco, si guarda intorno spaventato.'], polenta: ['polenta-1200.jpg', 1200, 800, 'Geppetto, con la parrucca gialla, riceve il pezzo di ' + 'legno da Maestro Ciliegia; accanto, un paiolo di polenta fumante.'], ciliegie: ['ciliegie-600.jpg', 600, 626, 'Due ciliegie rosse su un picciolo, con una foglia.'], }; const resources = Object.entries(PICTURES).map(([id, [fileId, w, h, altText]]) => ({ id, typeId: 'fregio', kind: 'bitmap', createdAt: 0, updatedAt: 0, altText, bitmap: { fileId, format: 'jpeg', width: w, height: h } })); // #endregion const config = () => ({ // a factory: the engine caches resolved configs per object locale: 'it', // Italian hyphenation, by its exact code (gotcha: hyphenation-locales) resourceTypes, colorPalette, header, footer, layout: { layoutType: 'single' }, page: { width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150, backgroundColor: col('paper'), margins: { top: mm(TOP), bottom: mm(BOTTOM), left: mm(INNER), right: mm(OUTER), mirror: true } }, bodyText: { fontFamily: TEXT, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'), italicColor: col('ink'), // *ohi* in ink, not in the defaults' bark // The book cites nothing, but a :ref added later prints in ink: main-color does not reach // this colour, which stays #295AA3 (gotcha: palette-skips-designs). referenceColor: col('ink'), firstLineIndent: mm(5), indentAfterHeading: false, maxWordSpacing: 1.45, // the default 2 lets 14 lines stretch past 1.45×; 3 still do, to 1.58× }, headings: { fontFamily: DISPLAY, fontWeight: 400, // the designs paint the titles: no Open Sans to load // Any headings object drops the H1 break (gotcha: headings-drop-h1-break): every // chapter on a new page, on either side, so chapter II opens on a verso. levels: [{ level: 1, breakBefore: { enabled: true, parity: 'any' } }], }, headingStyles: [frontispiece, chapter], paragraphStyles: [{ id: 'colofon', fontFamily: LABEL, fontSize: pt(7.5), lineHeight: pt(11), color: col('bark'), textAlign: 'center', firstLineIndent: pt(0), marginTop: pt(LEAD) }], }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---نموذج Markdown · أسطر: 74 · content.en.md
title: "Le avventure di Pinocchio" subtitle: "Storia di un burattino" author: "Carlo Collodi" --- # Pinocchio {style="frontespizio" kicker="Le avventure di"} # Capitolo I {style="capitolo" vignetta="ceppo" summary="Come andò che Maestro Ciliegia, falegname, trovò un pezzo di legno che piangeva e rideva come un bambino."} — C’era una volta… — Un re! — diranno subito i miei piccoli lettori. — No, ragazzi, avete sbagliato. C’era una volta un pezzo di legno. Non era un legno di lusso, ma un semplice pezzo da catasta, di quelli che d’inverno si mettono nelle stufe e nei caminetti per accendere il fuoco e per riscaldare le stanze. Non so come andasse, ma il fatto gli è che un bel giorno questo pezzo di legno capitò nella bottega di un vecchio falegname, il quale aveva nome mastr’Antonio, se non che tutti lo chiamavano maestro Ciliegia, per via della punta del suo naso, che era sempre lustra e paonazza, come una ciliegia matura. Appena maestro Ciliegia ebbe visto quel pezzo di legno, si rallegrò tutto; e dandosi una fregatina di mani per la contentezza, borbottò a mezza voce: — Questo legno è capitato a tempo; voglio servirmene per fare una gamba di tavolino. — Detto fatto, prese subito l’ascia arrotata per cominciare a levargli la scorza e a digrossarlo; ma quando fu lì per lasciare andare la prima asciata, rimase col braccio sospeso in aria, perchè sentì una vocina sottile sottile, che disse raccomandandosi: — Non mi picchiar tanto forte! — Figuratevi come rimase quel buon vecchio di maestro Ciliegia! Girò gli occhi smarriti intorno alla stanza per vedere di dove mai poteva essere uscita quella vocina, e non vide nessuno! Guardò sotto il banco, e nessuno: guardò dentro un armadio che stava sempre chiuso, e nessuno; guardò nel corbello dei trucioli e della segatura, e nessuno; aprì l’uscio di bottega per dare un’occhiata anche sulla strada, e nessuno. O dunque?… — Ho capito; — disse allora ridendo e grattandosi la parrucca — si vede che quella vocina me la son figurata io. Rimettiamoci a lavorare. — E ripresa l’ascia in mano, tirò giù un solennissimo colpo sul pezzo di legno. — Ohi! tu m’hai fatto male! — gridò rammaricandosi la solita vocina. Questa volta maestro Ciliegia restò di stucco, cogli occhi fuori del capo per la paura, colla bocca spalancata e colla lingua giù ciondoloni fino al mento, come un mascherone da fontana. Appena riebbe l’uso della parola, cominciò a dire tremando e balbettando dallo spavento: — Ma di dove sarà uscita questa vocina che ha detto ohi?… Eppure qui non c’è anima viva. Che sia per caso questo pezzo di legno che abbia imparato a piangere e a lamentarsi come un bambino? Io non lo posso credere. Questo legno eccolo qui; è un pezzo di legno da caminetto, come tutti gli altri, e a buttarlo sul fuoco, c’è da far bollire una pentola di fagioli… O dunque? Che ci sia nascosto dentro qualcuno? Se c’è nascosto qualcuno, tanto peggio per lui. Ora l’accomodo io! — E così dicendo, agguantò con tutt’e due le mani quel povero pezzo di legno, e si pose a sbatacchiarlo senza carità contro le pareti della stanza. Poi si messe in ascolto, per sentire se c’era qualche vocina che si lamentasse. Aspettò due minuti, e nulla; cinque minuti, e nulla; dieci minuti, e nulla! — Ho capito — disse allora sforzandosi di ridere e arruffandosi la parrucca — si vede che quella vocina che ha detto *ohi*, me la son figurata io! Rimettiamoci a lavorare. — E perchè gli era entrato addosso una gran paura, si provò a canterellare per farsi un po’ di coraggio. Intanto, posata da una parte l’ascia, prese in mano la pialla, per piallare e tirare a pulimento il pezzo di legno; ma nel mentre che lo piallava in su e in giù, sentì la solita vocina che gli disse ridendo: — Smetti! tu mi fai il pizzicorino sul corpo! — Questa volta il povero maestro Ciliegia cadde giù come fulminato. Quando riaprì gli occhi, si trovò seduto per terra. Il suo viso pareva trasfigurito, e perfino la punta del naso, di paonazza come era quasi sempre, gli era diventata turchina dalla gran paura. ::resource{id="ciliegie"} # Capitolo II {style="capitolo" vignetta="polenta" summary="Maestro Ciliegia regala il pezzo di legno al suo amico Geppetto, il quale lo prende per fabbricarsi un burattino maraviglioso, che sappia ballare, tirar di scherma e fare i salti mortali."} In quel punto fu bussato alla porta. — Passate pure, — disse il falegname, senza aver la forza di rizzarsi in piedi. Allora entrò in bottega un vecchietto tutto arzillo, il quale aveva nome Geppetto; ma i ragazzi del vicinato, quando lo volevano far montare su tutte le furie, lo chiamavano col soprannome di Polendina, a motivo della sua parrucca gialla, che somigliava moltissimo alla polendina di granturco. Geppetto era bizzosissimo. Guai a chiamarlo Polendina! Diventava subito una bestia, e non c’era più verso di tenerlo. :::paragraphs{style="colofon"} Composto in Averia Serif Libre, Fredericka the Great e Quicksand (SIL Open Font License). Testo di Carlo Collodi (1883) secondo l’edizione Bemporad del 1902, Project Gutenberg n. 52484. Illustrazioni generate con modelli di diffusione. :::`; // content.<lang>.md: Collodi's Italian in both editions // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { 'Averia Serif Libre': ['400', '400i'], 'Fredericka the Great': ['400'], Quicksand: ['400', '600', '700'] }; // every face, loaded before layout (gotcha: fonts-first) // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); // A design slot or ::resource names a resource, the resource a file id: load each file. await Promise.all(Object.values(PICTURES).map(([file]) => loadImage(file, asset(file)))); const doc = await buildWithFonts( () => buildDocument({ markdown, resources, continuation }, config()), markdown); showPages(doc, { title: 'Le avventure di Pinocchio' });العُدّة · 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 ↗ (يفتح في تبويب جديد)
تنويعات
#ابدأ كل فصل في صفحة فردية
مع 'odd' ينتقل الفصل الثاني إلى الصفحة 11 وتُترك الصفحة 10 فارغة؛ وتوقف صفحة العنوان فاصلها الخاص، لأنها ترث فاصل المستوى، ولولا ذلك لانتقلت إلى الصفحة 7 بعد صفحة 6 فارغة.
@@ the frontispiece style @@
id: 'frontespizio',
+ breakBefore: { enabled: false },
@@ headings, in config() @@
- levels: [{ level: 1, breakBefore: { enabled: true, parity: 'any' } }],
+ levels: [{ level: 1, breakBefore: { enabled: true, parity: 'odd' } }],أخطاء شائعة
خطأ شائع
صور الافتتاحية لا تُحتسب أبدًا في الارتفاع الذي تحجزه
في postext 1.4.1 يقيس العنوان ذو التصميم المتقدّم الارتفاعَ الذي يحجزه دون صوره: تُحتسب نصوصه وخطوطه وإطاراته، حتى المثبّتة على الصفحة، أما الصورة، كصورة تمتد بالنزف عبر رأس الصفحة، فلا تحجز شيئًا، فقد يبدأ النص فوقها. اضبط minHeight على الموضع الذي يجب أن يبدأ فيه النص. صفحات افتتاح مصمَّمة →
خطأ شائع
الافتتاحية الباقية في عمودها تُقصّ عند أعلى كتلة النص
في postext 1.4.1 يُقصّ العنوان ذو التصميم المتقدّم الباقي في عموده عند الحافة العليا للعمود: الإطار أو الصورة المثبّتة على الصفحة أو على النزف يُرسَم في الهوامش الجانبية لكن لا في الهامش العلوي، ولا يقول ذلك أي تحذير. أعطِ هذا العنوان span: 'page'، حتى في كتاب بعمود واحد: فيُرسَم تصميمه حينئذ كاملًا شريطَ افتتاحية الصفحة. صفحات افتتاح مصمَّمة →
خطأ شائع
قيم السمات: لا { ولا }؛ ضع القيمة التي فيها " بين علامتي اقتباس مفردتين
تنتهي قيمة السمة عند القوس المعقوف الختامي، فلا يمكن أن تحتوي { أو }. القيمة التي فيها علامة اقتباس مزدوجة توضع بين علامتي اقتباس مفردتين؛ أما علامة الدولار فلا مشكلة فيها. سمات العنوان →
خطأ شائع
ضع كل قيمة في الترويسة الأمامية (frontmatter) بين علامتي اقتباس
يقرأ YAML القيمة title: 1984 رقمًا، ويقرأ التاريخ كائن Date، والقيم غير النصية تُطبع فارغة في العناصر النائبة وتترك ملف PDF بلا عنوان. ضع كل قيمة بين علامتي اقتباس: title: "1984". البيانات الوصفية للمستند →
خطأ شائع
أي كائن headings يُلغي فاصل الصفحة قبل H1
ينتقل H1 افتراضيًا إلى صفحة فردية (always-odd)، لكن تمرير أي كائن headings يعيد ضبط هذا الافتراض، فتتوالى الفصول دون فاصل ولا يفعل span: 'page' شيئًا. أعد كتابة headings.levels[0].breakBefore: { enabled: true, parity } في كل إعداد. فصول تبدأ في صفحة فردية →
خطأ شائع
لوحة الألوان المستبدلة لا تصل إلى عناصر التصميم ولا إلى لون الإحالة
يقرأ postext 1.4.1 الإعداد colorPalette في أنماط النص (المتن والعناوين والقوائم والتعليقات والجداول والإطارات) لكن لا في عناصر الترويسات والتذييلات والافتتاحيات وصفحات الأجزاء، ولا في bodyText.referenceColor: تحتفظ بالقيمة الست عشرية المكتوبة بجانب paletteId الخاص بها. حين تستبدل لوحة الألوان، لنسخة شاشة داكنة أو لإعادة تلوين، أعِد كتابة كل لون مرتبط من colorPalette قبل البناء. لوحة ألوان دلالية →
خطأ شائع
lineHeight لنص التصميم مُضاعِف، لا بُعد أبدًا
في خانة التصميم، يضاعف lineHeight لعنصر النص مقاس خطه (lineHeight: 1.05). في postext 1.4.1 لا يُرفض بُعدٌ مثل pt(15): يُقاس ارتفاع الافتتاحية NaN، ويسقط ما تحجزه من مساحة، بما فيه minHeight، دون تحذير، فيجري النص تحت العنوان. النصوص والخطوط والمربعات في تصاميم الصفحة →
خطأ شائع
فيض نص التصميم افتراضيًا 'ellipsis-end'
عنصر نص التصميم الذي لا يتسع له عرضه ينتهي افتراضيًا بعلامة الحذف. اضبط overflow: 'wrap' للعناوين التي يجب أن تنكسر على أسطر أكثر. النصوص والخطوط والمربعات في تصاميم الصفحة →
خطأ شائع
8 لغات فقط تُقسَّم بالواصلة، بالرمز المطابق تمامًا
يتوفر تقسيم الكلمات بالواصلة للغات en-us وes وfr وde وit وpt وca وnl، بمطابقة تامة للرمز: 'es-ES' أو أي لغة أخرى تعود دون تنبيه إلى الإنجليزية الأمريكية. تقسيم الكلمات ولغة المستند →
خطأ شائع
حمّل كل أوجه الخط قبل الإخراج
يقيس الإخراج النص بأوجه الخط التي حمّلها المتصفح ويخزّن العروض مؤقتًا، فالوجه الذي يصل بعد البناء الأول يترك فواصل أسطر خاطئة وملف PDF لم يعد يطابق الشاشة. حمّل كل وزن وكل نمط أولًا، واستدعِ clearMeasurementCache() قبل إعادة البناء إذا تأخر وصول أحدها. الخطوط قبل الإخراج →
- في Markdown تربط مسافة غير قابلة للكسر (U+00A0) كل شرطة من شرطات الحوار عند Collodi بكلمتها، فلا تبدأ شرطة إغلاق سطرًا أبدًا؛ واثنتان أخريان تبقيان di legno وin piedi متلازمتين في آخر فقرتيهما. في الإصدار 1.4.1 يمكن مع ذلك أن ينكسر السطر عند مسافة غير قابلة للكسر إذا كان في الفقرة عريض أو مائل. والفقرة المربوطة الوحيدة التي فيها كلمة مائلة، وهي التي فيها ohi في الصفحة 9، فيها أربعة روابط، وفي اللقطة يقع كل منها في وسط السطر أو في آخر الفقرة.
الحقوق
- الوصفة
- Ignacio Ferro
- النص
- Le avventure di Pinocchio (1883): chapter I and the opening of chapter II with Collodi’s chapter summaries, from the 1902 Bemporad edition, with typographic apostrophes, dashes and ellipses; chapter I’s summary has the comma after “falegname” that the edition’s table of contents prints and its chapter head leaves out · Carlo Collodi · ملك عام
- الصور
- The frontispiece: Tuscan hills, a hill village and a donkey cart of firewood on its way to the carpenter’s workshop · Generated With Diffusion Models · أصلي
- Chapter I’s vignette: Maestro Ciliegia, axe raised over the talking log · Generated With Diffusion Models · أصلي
- Chapter II’s vignette: Geppetto in his yellow wig receives the log, beside a pot of polenta · Generated With Diffusion Models · أصلي
- The cherry tailpiece · Generated With Diffusion Models · أصلي
- الخطوط
- Averia Serif Libre (SIL OFL 1.1) · Fredericka the Great (SIL OFL 1.1) · Quicksand (SIL OFL 1.1)
- الكود
- MIT، مثل Postext
حرّر هذا الشرح ↗ (يفتح في تبويب جديد)مجلد الوصفة على GitHub ↗ (يفتح في تبويب جديد)


