الفصل 3 · الجزء II · الحرفة
إعدادات Postext
المرجع الكامل لكل خيارات إعدادات الإخراج في Postext
باختصار
تسرد هذه الصفحة كل إعداد يتحكم في مظهر صفحاتك. تجمع مجموعة واحدة من الإعدادات كل الخيارات: مقاس الصفحة وعدد الأعمدة والخطوط وحجم النص والعناوين والإطارات والألوان وغير ذلك. لا تكتب إلا الإعدادات التي تريد تغييرها، لأن لكل إعداد قيمة أولى معقولة. الصفحة مرجع، فانتقل مباشرة إلى الجزء الذي تحتاجه. وتشرح الأقسام الأخيرة للمبرمجين كيف يصنعون صفحات ويب وملفات PDF وكتبًا إلكترونية بصيغة EPUB من الشيفرة.
كل قرار من قرارات الإخراج في Postext يصدر عن كائن إعدادات واحد.
يتحكم PostextConfig في أبعاد الصفحة وترتيب الأعمدة وتنضيد نص المتن وأنماط العناوين ولغة المستند (locale) وغير ذلك. كل خاصية فيه اختيارية: يأتي Postext بقيم افتراضية معقولة مستوحاة من تقاليد تنضيد الكتب. لا تحتاج إلى تحديد شيء إلا ما تريد تغييره.
import { buildDocument } from 'postext';
const document = buildDocument(content, {
page: { sizePreset: '21x28', dpi: 300 },
layout: { layoutType: 'double', gutterWidth: { value: 0.5, unit: 'cm' } },
bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 9, unit: 'pt' } }, // 9 pt overrides the 8 pt default
headings: { fontFamily: 'Open Sans' },
});للاطلاع على طريقة معالجة المحرّك لهذه الإعدادات، راجع صفحة البنية.
#فهرس المرجع
هذا المرجع طويل. هذه كتله الرئيسية:
- الصفحة — المقاس والهوامش وشبكة خطوط الأساس وخطوط القص والتجليد.
- التخطيط — عدد الأعمدة والفواصل بين الأعمدة والخطوط الفاصلة والكتابة العمودية.
- الترويسات والتذييلات — عناصر نصية وخطوط في كل صفحة، مع عناصر نائبة (placeholders) وزوجية الصفحة والمحاذاة.
- نص المتن — التنضيد وتقسيم الكلمات بالواصلة والأسطر اليتيمة (orphans) والأرامل (widows).
- لغة المستند — الخاصية
localeفي المستوى الأعلى: اللغة الاحتياطية لتقسيم الكلمات، وأنواع الموارد المدمجة، وعبارات المتابعة في الجداول والإطارات المقسومة، واللغة الموسومة في ملف PDF الميسّر؛ والأرقام (numerals) واتجاه النص (direction) اللذان تقتضيهما؛ واللغات والأنظمة الكتابية التي ينضّدها Postext. ودليل الكتب المكتوبة من اليمين إلى اليسار هو الإخراج العربي. - تنضيد لغات شرق آسيا —
cjk: الأعراف الإقليمية وقواعد كسر الأسطر في النصوص الصينية واليابانية والكورية، وطريقة توزيع المسافات في أسطرها المضبوطة، وعروض علامات الترقيم وتعليقها، والمسافة بين حروف الهان والحروف اللاتينية، وشبكة الحروف، والأرقام القائمة في النص العمودي، والعلامات وحروف الروبي (ruby) وحواشي الواريتشو (warichu). ودليل ذلك كله هو الإخراج الصيني. - العناوين — القيم الافتراضية المشتركة وتجاوزات H1–H6.
- القوائم غير المرقّمة والقوائم المرقّمة — النقاط والترقيم والتداخل.
- الرياضيات — رسم LaTeX والمقياس واللون والهوامش.
- أنواع الموارد — ترقيم مصنّف للأشكال والجداول والأنواع المخصّصة.
- نمط الجداول (مع أنماط الجداول المسمّاة) ونمط التعليقات — التنضيد والزخرفة لموارد الجداول وتعليقاتها.
- نمط المخططات — إعادة تلوين مخططات SVG المضمّنة بحبر واحد للطباعة بالألوان الخاصة (spot colours).
- أنماط الفقرات — أنماط مسمّاة لحاويات
:::paragraphs: قوائم المراجع والمسارد والملاحظات. - أنماط الإطارات — ملاحظات ونصائح وأهداف داخل إطارات لحاويات
:::callout. - الأجزاء — صفحات فواصل الأجزاء لحاويات
:::part: زوجية الصفحة ومساحة المتن وتصميم صفحة الافتتاح وتنضيد المتن. - أنماط العناوين — أنماط مسمّاة لعناوين
{style="…"}: فصول غير مرقّمة، وصفحات تمهيدية بترويساتها الخاصة، والهندسة ولوحة الألوان. - جدول المحتويات — ما يطبعه
:::toc: تنضيد المداخل، والنقاط الموصِلة، وأرقام الصفحات، وأسطر المؤلفين، وصفوف الأجزاء. - الفهرس الأبجدي في آخر الكتاب — ما يطبعه
:::index: تنضيد المداخل، والإزاحات، وفواصل أرقام الصفحات ونطاقاتها، ومجموعات الحروف، ولغة الترتيب. - الوحدات والألوان + لوحة الألوان —
DimensionوColorValueوالألوان المسمّاة. - الخطوط المخصّصة — عرّف عائلات خطوط يرفعها المستخدم إلى جانب Google Fonts.
- عارض HTML — عرض العمود المستهدف وكسر الأسطر في مُخرِج HTML.
- إنتاج PDF (الإعدادات) — المخطط التفصيلي (outlines)، والمخرجات الميسّرة (الموسومة)، وفرض فضاء ألوان على مُخرِج PDF.
- عارض Folio (الإعدادات) — نوع الورق والتجليد والسطح والإضاءة في عارض الكتاب ثلاثي الأبعاد.
- التصحيح — طبقات بصرية وتحذيرات تأليف للمحرر.
- الاستخدام البرمجي —
buildDocumentوالتحذيرات التي يبلّغ عنها والمحلِّلات (resolvers) وذاكرات التخزين المؤقت. - تشغيل الإخراج في Web Worker — بناء خارج الخيط الرئيسي مع إمكانية الإلغاء.
- دمج عارض HTML وإنتاج ملفات PDF — وصفات كاملة من البداية إلى النهاية.
- كتب EPUB — ملفات EPUB 3 ثابتة التخطيط وأخرى قابلة لإعادة التدفق من الإخراج نفسه، وفحصها بأداة EPUBCheck.
#الصفحة
تتحكم الخاصية page في الأبعاد المادية للصفحة وفي مظهرها.
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
sizePreset | PageSizePreset | '17x24' | مقاس صفحة معرّف مسبقًا. اضبطه على 'custom' لاستخدام عرض وارتفاع صريحين. |
width | Dimension | 17 cm | عرض الصفحة. يؤخذ من sizePreset عند إغفاله؛ والقيمة الصريحة تتقدّم دائمًا (استخدم sizePreset: 'custom' للمقاسات المخصّصة كليًا). |
height | Dimension | 24 cm | ارتفاع الصفحة. يؤخذ من sizePreset عند إغفاله؛ والقيمة الصريحة تتقدّم دائمًا. |
margins | PageMargins | 2 cm لكل الجوانب | المسافة بين حافة الصفحة ومساحة المحتوى. يُضبط كل جانب (الأعلى والأسفل والأيسر والأيمن) على حدة. مع mirror: true تصبح الهوامش هوامش صفحات متقابلة: left هو الهامش الداخلي (جهة الكعب) وright هو الخارجي؛ تُبقي الصفحات الفردية (الصفحة 1 فردية) الهوامش كما كُتبت وتبدّلها الصفحات الزوجية، فتنتقل مساحة المحتوى، ومعها الأعمدة وأشرطة العناصر العائمة وحاويات الترويسة والتذييل وأشرطة صفحات الافتتاح، من جانب إلى آخر بين الصفحتين المتقابلتين. القيمة الافتراضية false. انظر أدناه. |
backgroundColor | ColorValue | transparent | لون خلفية الصفحة. |
dpi | number | 300 | عدد النقاط في البوصة. يؤثر في طريقة تحويل الوحدات المادية (cm وmm وin) إلى بكسلات. |
cutLines | CutLinesConfig | معطّل | يُظهر علامات القص في زوايا الصفحة لقصّ المطبوع. عند تفعيله تتّسع اللوحة (Canvas) لتشمل مساحة النزف وعلامات القص. انظر أدناه. |
baselineGrid | BaselineGridConfig | معطّل | يرسم شبكة خطوط الأساس فوق الصفحات لفحص الإيقاع العمودي. يلتزم الإخراج بالشبكة سواء رُسمت أم لم تُرسم. انظر أدناه. |
binding | 'auto' | 'left' | 'right' | 'auto' | الحافة التي يُجلَّد الكتاب عندها. 'auto' تساوي 'right' حين تكون layout.writingMode هي 'vertical-rl' أو حين يجري المستند من اليمين إلى اليسار (direction)، وإلا فهي 'left'. الكتاب المجلّد على اليمين يُفتتح بصفحة يسرى ويعكس هوامشه في الاتجاه المقابل. إعداد على مستوى الكتاب: لا يغيّره layout الخاص بنمط عنوان أبدًا. انظر التجليد. |
#الهوامش المتناظرة
تُقرأ الكتب صفحتين متقابلتين، وعادةً ما يختلف الهامش الداخلي عن الخارجي. يحوّل margins.mirror الهوامش الأربعة إلى هوامش صفحات متقابلة:
{
"page": {
"margins": {
"top": { "value": 2, "unit": "cm" },
"bottom": { "value": 2.5, "unit": "cm" },
"left": { "value": 2.2, "unit": "cm" },
"right": { "value": 1.4, "unit": "cm" },
"mirror": true
}
}
}بهذه الإعدادات يكون لكل صفحة فردية هامش 2.2 cm على اليسار (الكعب) و1.4 cm على اليمين (الحافة الأمامية)؛ ولكل صفحة زوجية 1.4 cm على اليسار (الحافة الأمامية) و2.2 cm على اليمين (الكعب). تحمل كل صفحة مُخرَجة contentArea الخاصة بها في VDTPage، فكل ما يُشتق منها، أي الأعمدة وأشرطة العناصر العائمة بعرض الصفحة وحاويات الترويسة والتذييل وأشرطة صفحات الافتتاح ذات span: 'page'، يتبع الهندسة المتناظرة تلقائيًا. أما إطارا الصفحة والنزف اللذان تستخدمهما عناصر التصميم المثبّتة إلى 'page' / 'bleed' فلا يتأثران: إنهما يصفان الورقة المادية لا الهوامش.
#التجليد
«تُجلَّد المستندات الصينية المنضّدة عموديًا على الجانب الأيمن، وتُجلَّد المستندات المنضّدة أفقيًا على الجانب الأيسر» (clreq §7.1.1.1). يُخرج page.binding: 'right' الكتابَ للتجليد على الحافة اليمنى:
- تظل الصفحة 1 فردية وتظل وجهَ الورقة (recto)، فتحتفظ
breakBefore.parityو:::pagebreak{parity}وعناصر التصميم ذاتparityوكل عدّ للصفحات بمعانيها. ما يتغيّر هو الجانب الذي يقع عليه وجه الورقة: الصفحة اليسرى من الصفحتين المتقابلتين. والفصل الذي يُفتتح على وجه ورقة جديد يُفتتح على صفحة يسرى (clreq §7.1.3.3). - مع
margins.mirrorيبقىleftهو الهامش الداخلي، لكن الصفحات الفردية هي التي تبدّل: الهامش الداخلي للصفحة 1 على يمينها، وللصفحة 2 على يسارها. ويتبع القاعدة نفسها كلٌّ من العمود الجانبي فيoneAndHalfعند'outer'/'inner'، والعنصر العائم المُدار الملاصق للكعب، وهوامش صفحات الأجزاء، وأيقونات زوايا الإطارات عند'outer'/'inner'. - يصرّح المستند بذلك (
VDTDocument.binding: 'right')، فلا يحتاج المضيف أبدًا إلى قراءة الإعدادات: يعرض Sandbox، بيئة التجربة في المتصفح، صفحاته المتقابلة على هيئة[3 | 2]والصفحة 1 وحدها على يسار الكعب، ويعرض عارض HTML فيه الصفحاتِ من اليمين إلى اليسار، فيُفتح عند الطرف الأيمن ويقود السهم الأيسر إلى الصفحة التالية. وفي وضع multi يرتّبrenderToHtmlالصف من اليمين إلى اليسار. - يحمل ملف PDF
/ViewerPreferences << /Direction /R2L >>و/PageLayout /TwoPageRight(الصفحة 1 وحدها ثم أزواج)، موسومًا كان أم غير موسوم. يتبعهما Acrobat وFoxit؛ ويتجاهلهما كليهما العارض المدمج في Chrome.
لا تتحرك أرقام الصفحات والترويسات من تلقاء نفسها: القالب الذي يطبع رقم الصفحة في الزاوية الخارجية يحتاج إلى ضبط عناصره الفردية والزوجية للحافة اليمنى (تقبل العناصر parity).
والكتاب المكتوب من اليمين إلى اليسار (العربية والفارسية والعبرية…) يُجلَّد على اليمين أيضًا: تعطي 'auto' الحافة اليمنى حين يُحسم direction الخاص بالمستند إلى 'rtl'. ويعكس مثل هذا الكتاب تدفقه كله كذلك، فيكون عموده الأول هو الأيمن، وتقع إزاحاته وعلامات قوائمه وعناصره العائمة وحواشيه على اليمين؛ أما خانات الترويسة والتذييل فتبقى في مواضعها المادية. انظر الإخراج العربي.
#مقاسات الصفحة المسبقة
| المقاس المسبق | العرض | الارتفاع | الاستخدام الشائع |
|---|---|---|---|
'11x17' | 11 cm | 17 cm | كتب الجيب |
'12x19' | 12 cm | 19 cm | الكتب الورقية القياسية |
'17x24' | 17 cm | 24 cm | الكتب التقنية والكتب المدرسية |
'21x28' | 21 cm | 28 cm | المجلات والتقارير (قريب من A4) |
#شبكة خطوط الأساس
شبكة خطوط الأساس هي إيقاع نص المتن: أسطر تفصل بينها مسافة ارتفاع سطر واحد من المتن، تُعدّ من أعلى مساحة المحتوى. يستخدمها الإخراج سواء رُسمت أم لم تُرسم: تعيد العناوين ونهايات القوائم والإطارات والأشكال والمعادلات المعروضة النصَّ إليها (ما لم يكن snapToGrid الخاص بها معطّلًا)، فتبقى أسطر الأعمدة المتجاورة متحاذية. لا يفعل enabled إلا رسم الخطوط، في Canvas وفي PDF وفي عروض Sandbox، لفحص ذلك الإيقاع؛ وتشغيله أو إيقافه لا يحرّك شيئًا. لا تمتد الخطوط إلا على نص الصفحة الفعلي، من أول سطر نص إلى آخره، فلا تظهر شبكة في أشرطة العناصر العائمة ولا في الصفحات الفارغة المضافة لضبط الزوجية ولا في المساحة الفارغة الباقية في آخر الصفحة.
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
enabled | boolean | false | هل تُرسم خطوط الشبكة (في Canvas وفي PDF). الرسم فقط: الإخراج هو نفسه في الحالتين. |
color | ColorValue | #cccccc | لون خطوط الشبكة. |
lineWidth | Dimension | 0.5 pt | سماكة خطوط الشبكة. |
page: {
baselineGrid: { enabled: true, color: { hex: '#e0e0e0', model: 'hex' } }
}#خطوط القص
عند التفعيل تتّسع اللوحة لتشمل مساحة نزف، ويرسم المحرّك علامات القص عند كل زاوية لأغراض الإنتاج الطباعي.
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
enabled | boolean | false | هل تُوسَّع اللوحة بمساحة النزف وتُرسم علامات القص. |
bleed | Dimension | 3 mm | مساحة إضافية حول الصفحة تُستخدم للنزف الطباعي. |
markLength | Dimension | 5 mm | طول كل علامة قص. |
markOffset | Dimension | 3 mm | الفجوة بين حدّ القص وبداية كل علامة قص. لا تبدأ العلامة أبدًا داخل النزف: حين يكون bleed أعرض، تبدأ العلامة عند حافة النزف. |
markWidth | Dimension | 0.25 pt | سماكة علامات القص. |
color | ColorValue | #000000 | لون علامات القص في Canvas وفي ملفات PDF بنظام RGB أو بالتدرّج الرمادي. أما ملف PDF بنظام CMYK فيطليها بلون التسجيل (registration colour) بدلًا من ذلك (انظر أدناه). |
تكبر الورقة بمقدار bleed + markOffset + markLength من كل جانب، وتقع الصفحة المقصوصة في وسطها. تحصل كل زاوية من زوايا حدّ القص على علامتين طول كل منهما markLength، كل منهما على امتداد إحدى الحافتين اللتين تلتقيان عندها. تبدأ العلامة على مسافة markOffset خارج حدّ القص، أو عند حافة النزف حين يكون bleed هو الأعرض بينهما، فلا تقع أي علامة فوق رسم يمتد إلى النزف. مع القيم الافتراضية (3 mm من النزف وإزاحة 3 mm) تمتد العلامات من 3 إلى 8 mm خارج حدّ القص، ويدور حول الورقة خارجها شريط فارغ عرضه 3 mm. تعيد cropMarkSegments(page, doc.config.page, doc.trimOffset) علامات الصفحة الثماني ببكسلات الصفحة، وهي التي يرسمها مُخرِجا Canvas وPDF. الوسيط الثالث هو موضع حدّ القص داخل الورقة، وهو القيمة التي يُكتب منها TrimBox في ملف PDF؛ وإن أُغفل، يُحسب من cutLines بالطريقة نفسها.
لا يُطبع خارج النزف شيء سوى العلامات. كل ما ترسمه الصفحة، بما في ذلك عناصر التصميم المثبّتة إلى 'page' أو 'bleed'، يُقصّ عند صندوق النزف في Canvas وفي PDF وفي مخرجات HTML، كما يقصّه التصدير من برامج النشر المكتبي (DTP): الشريط أو الصورة الموضوعة عمدًا خارج النزف تُقطع عند حافة النزف، حيث كانت آلة القص ستزيلها على أي حال. وحتى الإصدار postext 1.4 كان مثل هذا العنصر يمتد فوق العلامات إلى حافة الورقة.
في PDF (postext-pdf) يكون MediaBox لكل صفحة هو الورقة كلها. وتحمل الصفحة أيضًا TrimBox، أي الصفحة المقصوصة، وBleedBox، أي حدّ القص مضافًا إليه النزف، وهما ما تقرؤه أدوات الترتيب الطباعي (imposition) والفحص قبل الطباعة (preflight). وحين يُكتب ملف PDF بنظام CMYK (colorSpace: 'cmyk'، أو pdfGeneration.forceColorSpace مع colorSpace: 'cmyk')، تُطلى العلامات بلون التسجيل، أي الفصل اللوني /All، فتُطبع على كل لوح. أما ملفات PDF بنظام RGB أو بالتدرّج الرمادي فتطليها باللون color.
#ترقيم الصفحات
يتحكم القسم page.pageNumbering في طريقة تنسيق تسميات الصفحات وفي القيمة التي يبدأ منها العدّاد. وهو لا يعرّف إلا القيمة الافتراضية على مستوى المستند كله؛ ولإعادة بدء الترقيم في منتصف المستند (مثل صفحات تمهيدية بأرقام رومانية تتحول إلى فصول بأرقام عشرية تبدأ من 1)، استخدم التوجيه :::numbering (انظر صيغة المستند › التوجيهات).
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
format | 'decimal' | 'lower-roman' | 'upper-roman' | 'lower-alpha' | 'upper-alpha'، أو أحد أنماط شرق آسيا | 'decimal' | النمط العددي المستخدم في رسم تسميات الصفحات. أنماط شرق آسيا ('trad-chinese-informal' يرقّم الصفحات 一، 二، 三) مذكورة في صيغ كتابة أنماط الترقيم. |
startAt | number | 1 | القيمة العددية المسندة إلى الصفحة الأولى أيًا كان التنسيق. format: 'lower-roman', startAt: 1 تعطي i, ii, iii, …؛ وformat: 'decimal', startAt: 17 تعطي 17, 18, 19, …. |
تُخزَّن التسمية المحسوبة في كل VDTPage باسم pageLabel، وهي ما يُحسم إليه العنصر النائب {pageNumber} في الترويسة والتذييل. وتُصدر ملفات PDF شجرة أرقام /PageLabels فيطابق مؤشر الصفحة في Preview / Acrobat والتنقل عبر «الانتقال إلى الصفحة» التسمياتِ المطبوعة تمامًا. والنمط الذي لا يملك له PDF رمزًا (الأرقام الصينية والأرقام داخل دوائر والأرقام كاملة العرض) يُكتب صفحةً صفحة، فيعرض القارئ 一، 二، 三 أيضًا.
صيغ كتابة أنماط الترقيم
ثلاثة إعدادات تختار تنسيقًا للترقيم، ولكل منها صيغة كتابة نشأت معه: تسميات الصفحات (page.pageNumbering.format و:::numbering{format=…}) تقول lower-roman، والقوائم المرقّمة (orderedLists.numberFormat) تقول arabic للعشري، وأنواع الموارد (counterFormat) تقول roman-lower. يقبل كل منها جميع الصيغ أدناه، فالتنسيق المنسوخ من إعداد يعمل في الإعدادين الآخرين. لا تميّز الأسماء بين الأحرف الكبيرة والصغيرة؛ أما الصيغ ذات الحرف الواحد فتميّز (i وI مختلفان).
| التنسيق | يطبع | الصيغ المقبولة |
|---|---|---|
| عشري | 1, 2, 3 | decimal، arabic، 1 |
| روماني بأحرف صغيرة | i, ii, iii | lower-roman، roman-lower، i |
| روماني بأحرف كبيرة | I, II, III | upper-roman، roman-upper، I |
| حروف لاتينية صغيرة | a, b, c | lower-alpha، alpha-lower، lower-latin، a |
| حروف لاتينية كبيرة | A, B, C | upper-alpha، alpha-upper، upper-latin، A |
| الأعداد الصينية، المبسّطة | 一, 十二, 一百零一 | simp-chinese-informal، 一 في مستند بالصينية المبسّطة |
| الأعداد الصينية، التقليدية | 一, 十二, 一萬 | trad-chinese-informal، cjk-ideographic، 一 في مستند بالصينية التقليدية |
| الأعداد الصينية المالية، المبسّطة | 壹, 壹拾贰, 壹佰贰拾 | simp-chinese-formal، 壹 في مستند بالصينية المبسّطة |
| الأعداد الصينية المالية، التقليدية | 壹, 壹拾貳, 壹佰貳拾 | trad-chinese-formal، 壹 في مستند بالصينية التقليدية |
| الأرقام الصينية خانةً خانة | 一二〇, 二〇二六 | cjk-decimal، 〇 |
| السيقان السماوية | 甲, 乙, 丙 … 癸 | cjk-heavenly-stem، 甲 |
| الفروع الأرضية | 子, 丑, 寅 … 亥 | cjk-earthly-branch، 子 |
| أرقام داخل دوائر | ①, ②, ③ … ㊿ | circled-decimal، ① |
| أرقام كاملة العرض | 1, 2, 3 | fullwidth-decimal، 1 |
| الأرقام الهندية العربية | ١, ٢, ٣ … ١٠ | arabic-indic، ١ |
| الأرقام الفارسية | ۱, ۲, ۳ … ۱۰ | persian، urdu، ۱ |
| الحروف العربية، بترتيب أبجد | أ, ب, ج, د, هـ … غ, أأ | abjad، أبجد |
| الحروف العربية، بالترتيب الهجائي | أ, ب, ت, ث … ي, أأ | hijai، arabic-alpha، arabic-alphabetic، أبتث |
| الأعداد الأبجدية (حساب الجُمَّل) | ا, ب … يا (11), غتمو (1446) | arabic-abjad |
| الأعداد الأبجدية، بالقيم المغربية | ص (60), ض (90), ش (1000) | arabic-abjad-maghrebi، maghrebi-abjad |
تحتفظ أنماط شرق آسيا بأسمائها في CSS Counter Styles في الإعدادات الثلاثة كلها. تكتب الأعداد الصينية غير الرسمية 十 للأعداد من 10 إلى 19 دون 一 في أولها (十二، ولكن 一百一十)، و零 واحدة لكل سلسلة أصفار داخل العدد (一百零一، 一千零五十)، و万 أو 萬 للعشرة آلاف، و亿 أو 億 للمئة مليون (一万零一十). وحده cjk-decimal يستخدم 〇، خانةً خانة، على طريقة كتابة السنوات (二〇二六، GB/T 15835—2011). تتوقف السيقان عند 10، والفروع عند 12، والأرقام داخل الدوائر عند 50؛ وبعدها يُطبع العدد بالأرقام. ويتبع 一 و壹 نظامَ كتابة locale الخاص بالمستند: التقليدي لـ zh-Hant أو zh-TW أو zh-HK، والمبسّط في غير ذلك. بعض الطبعات القديمة تكتب 101 على هيئة 一百一 دون 零؛ ولا يطبع Postext هذه الصيغة.
تحتفظ الأنماط العربية بأسماء CSS حيث يوجد لها اسم في CSS (arabic-indic وpersian؛ ويُقرأ urdu وmaghrebi-abjad المأخوذان من مذكرة W3C Ready-made Counter Styles على أنهما persian وarabic-abjad-maghrebi). ويحتفظ arabic بمعناه القديم، أي الأرقام الأوروبية. يكتب arabic-abjad الأعداد الأبجدية الجمعية في العربية التراثية وفي ترقيم أوراق المخطوطات، بدءًا بالقيمة الأعلى: 11 هي يا، و1446 هي غتمو، وعدد الآلاف يأتي قبل غ (2000 بغ، 1002 غب)؛ وبعد 999 999 يُطبع العدد بالأرقام. تستخدم مذكرة W3C هذا الاسم لسلسلة من 28 حرفًا تكون فيها 11 هي ك؛ ويسمّي Postext تلك السلسلة abjad، أي ترقيم عناصر القوائم بالحروف بترتيب أبجد (أ، ب، ج، د، هـ)، ويسمّي hijai الترتيب الهجائي (أ، ب، ت، ث). كلاهما يكتب الحرف الأول بهمزته (أ)، والهاء التي تقف منفردة على هيئة هـ بالتطويل، فلا يُقرأ أي منهما على أنه الرقمان ١ و٥؛ وبعد 28 تتضاعف الحروف، كما في lower-alpha (أأ, أب). وتتبع القيم المغربية العبارة التذكيرية صعفض قرست ثخذ ظغش (ص 60، ض 90)؛ وتبدّل قائمة W3C بين ص وض. ولا يكفي حرف أ وحده للتمييز بين السلسلتين، فرمزاهما هما أحرفهما الأربعة الأولى: {1:أبجد} و{1:أبتث}. أما أرقام المستند نفسه فتُضبط بالإعداد numerals.
الصيغ الأخرى مخصّصة للإعدادات التي لا يتحقق أحد من أنواعها: الإعدادات المسبقة بصيغة JSON وJavaScript العادية. ما تزال أنواع TypeScript لا تسمّي إلا الصيغة الخاصة بكل إعداد، أي التي يكتبها Sandbox ويعيدها resolveAllConfig، بما فيها أسماء شرق آسيا، فيلتزم بها PostextConfig المحدّد النوع، وتحتاج أي صيغة أخرى إلى تحويل نوع (cast).
// JavaScript or a JSON preset (in TypeScript, each setting's own spelling)
orderedLists: { numberFormat: 'decimal' }, // same as 'arabic'
page: { pageNumbering: { format: 'roman-lower' } }, // same as 'lower-roman'
resourceTypes: [{ id: 'plate', counterFormat: 'upper-roman', … }], // same as 'roman-upper'يحوّل resolveAllConfig تنسيقات القوائم والصفحات إلى صيغة الإعداد الخاص بكل منها، فتُحسم 'decimal' إلى 'arabic' في orderedLists، و'roman-lower' إلى 'lower-roman' في page.pageNumbering، ويُسقط stripConfigDefaults أي صيغة تساوي القيمة الافتراضية. تعرض لوحات Sandbox كل إعداد من الإعدادات الثلاثة بصيغته الخاصة، أيًا كانت الصيغة المستخدمة في الإعدادات. وأي قيمة أخرى، مثل roman أو 01 أو خطأ إملائي، ترقّم بالأرقام العشرية بدلًا من طباعة undefined، ويُبلَّغ عنها تحذيرًا في الإعدادات. وتقبل قوالب ترقيم العناوين الأسماء نفسها بعد النقطتين ({1:roman-upper} هو {1:I})، إلى جانب صيغتها الخاصة ذات الأصفار البادئة {1:01}.
#التخطيط
تتحكم الخاصية layout في ترتيب الأعمدة داخل مساحة المحتوى.
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
layoutType | 'single' | 'double' | 'oneAndHalf' | 'double' | ترتيب الأعمدة. انظر أدناه لتفاصيل كل نوع. |
gutterWidth | Dimension | 0.75 cm | المسافة الأفقية بين الأعمدة. لا تنطبق إلا على التخطيطات متعددة الأعمدة. |
sideColumnPercent | number | 33 | عرض العمود الجانبي نسبةً مئوية من مساحة المحتوى. تُستخدم كما كُتبت أي قيمة تترك لكلا العمودين شيئًا من العرض؛ أما القيمة التي لا تفعل ذلك فتُقيَّد، ويبلّغ البناء عنها (انظر أنواع التخطيط). لا تنطبق إلا على التخطيط 'oneAndHalf'. |
sideColumnRole | 'text' | 'floats' | 'text' | ما يحمله العمود الجانبي: نص المتن (يتدفق إليه بعد العمود الرئيسي)، أو الموارد والإطارات الموضوعة بـ span: 'side' فقط، أي عمود هامشي للعناصر العائمة وحدها. لـ 'oneAndHalf' فقط. مع 'text'، تُعاد قسمة أسطر الفقرة التي تمتد من عمود إلى آخر على عرض العمود الذي تتابع فيه؛ وحتى postext 1.4 كانت تحتفظ بأسطر العمود الذي بدأت فيه، فكان السطر المنضّد لعرض العمود الرئيسي يتجاوز العمود الجانبي، فيُقصّ. |
sideColumnSide | 'right' | 'left' | 'outer' | 'inner' | 'right' | حافة مساحة المحتوى التي يقع عندها العمود الجانبي. تتبع 'outer' / 'inner' زوجية الصفحة حين تكون الهوامش متناظرة (الحافة الخارجية لوجه الورقة هي حافته اليمنى، ولظهرها حافته اليسرى). لـ 'oneAndHalf' فقط. |
columnRule | ColumnRuleConfig | معطّل | خط مرئي اختياري يُرسم بين الأعمدة. انظر أدناه. |
fitFiguresToPage | boolean | false | يصغّر الشكل (صورة نقطية أو SVG) الذي تكون صورته وتعليقه وملاحظته معًا أطول من مساحة المحتوى حتى يتسع لها، ويصغّر الشكل المدرج في سياق النص الذي يزيد قليلًا على المساحة الباقية في عموده (حتى نصف عرضه، مع احتفاظ التعليق بعرض العمود) فيبقى مع نصه. وتقع الصورة المصغّرة في خانتها وفق placement.align. يفعّله عارض HTML، لأن صفحاته لا تزيد طولًا على الشاشة؛ أما الصفحات المطبوعة فمقاسها يناسب أشكالها. |
hugClosingFloats | boolean | true | في الصفحة الختامية من الفصل (ومن المستند)، ترتفع الأشكال والجداول الممتدة بعرض الصفحة الموضوعة تحت آخر شريط من النص لتستقر على مسافة فجوة واحدة من فجوات العناصر العائمة تحته، مكدّسة بترتيبها: لا شيء يليها هناك. تتركها false حيث وضعها موضعها المحدّد، فينتهي العنصر العائم ذو position: 'bottom' عند أسفل الصفحة في الصفحة الختامية كما في كل صفحة أخرى، كما في نشرة بيانات ينتهي مخططها عند الارتفاع نفسه في كل صفحة مثلًا. والصفحات التي فيها عمود جانبي لا تحرّكها أبدًا. |
inlineResourceGap | 'around' | 'above' | 'around' | الموضع الذي يحتفظ فيه المورد المدرج في سياق النص (placement.position: 'here'، المضمَّن بـ ::resource) بفجوة العناصر العائمة، وهي سطر واحد. تحتفظ 'around' بها فوق المورد وتحته، ويعود النص الذي يليه إلى شبكة خطوط الأساس تحت تلك الفجوة؛ والعنوان أو القائمة أو الإطار أو المورد المدرج الآخر الذي يليه مباشرة يتقاسم الفجوة السفلية مع مسافته العلوية الخاصة، فتُطبَّق الكبرى بينهما. تحتفظ 'above' بها فوقه فقط: يستأنف النص الذي يلي المورد عند خط الشبكة التالي مهما كان قريبًا، بين لا شيء وسطر كامل، كما كان حتى postext 1.4. والإعدادات التي خزّنتها إصدارات سابقة وتضمّن فصولُها موردًا تُقرأ بالقيمة 'above'، فلا تتحرك صفحاتها (انظر الحزم المكتوبة بالإصدار postext 1.4 أو أقدم). والإعدادات المكتوبة في الشيفرة للإصدار 1.4 تحتفظ بالتباعد القديم بضبط 'above' بنفسها، أو عبر pinLegacyInlineGap من postext/bundle. |
inlineResourceGapInBoxes | boolean | true | هل يحتفظ المورد المدرج داخل إطار (:::callout) بالفجوة التي يضبطها inlineResourceGap، وهي سطر من نص الإطار نفسه: فوق المورد، وتحته أيضًا مع 'around'، فتُطبَّق الكبرى بينها وبين المسافة الخاصة بالكتلة التالية. في أعلى الإطار أو أسفله، أو في أعلى جزء من إطار مقسوم أو أسفله، تتولى الحشوة فصل المورد ولا تُضاف فجوة. تضع false المورد مباشرة تحت النص الذي قبله، والنص الذي بعده مباشرة تحت المورد، كما كان حتى postext 1.4. والإعدادات التي خزّنتها إصدارات سابقة وتضمّن فصولها موردًا داخل إطار تُقرأ بالقيمة false (انظر الحزم المكتوبة بالإصدار postext 1.4 أو أقدم)؛ وفي الشيفرة، يفعل pinLegacyBoxResourceGap من postext/bundle الشيء نفسه. |
boxChildSplitMinLines | number | 2 | أقل عدد من أسطر الفقرة أو عنصر القائمة يتركه القطع داخلها على كل جانب حين ينقسم الإطار (أما splitMinLines، في أنماط الإطارات، فما يزال يعدّ كل أسطر الإطار على كل جانب من القطع). عدد صحيح، 1 على الأقل. مع القيمة الافتراضية لا يترك القطع أبدًا سطرًا منفردًا من فقرة أو عنصر في أسفل عمود أو في رأس العمود التالي؛ ونمط الإطار الذي تكون فيه splitMinLines أقل يحدّد الحدّ بدلًا من ذلك. تسمح 1 للقطع بأن يترك سطرًا واحدًا من الفقرة أو العنصر على أحد الجانبين، كما كان حتى postext 1.4. والإعدادات التي خزّنتها إصدارات سابقة وتحتوي فصولها على :::callout تُقرأ بالقيمة 1 (انظر الحزم المكتوبة بالإصدار postext 1.4 أو أقدم)؛ وفي الشيفرة، يفعل pinLegacyBoxChildCut من postext/bundle الشيء نفسه. |
writingMode | 'horizontal-tb' | 'vertical-rl' | 'horizontal-tb' | اتجاه جريان الأسطر. تنضّد 'vertical-rl' النص الصيني والياباني عموديًا: الحروف من الأعلى إلى الأسفل، وكل سطر على يسار السطر الذي قبله. يرثها layout الخاص بنمط العنوان ما لم يضبط قيمته الخاصة، فيمكن أن يلي ملحقٌ أفقي كتابًا عموديًا. انظر الكتابة العمودية. |
#الكتابة العمودية
مع writingMode: 'vertical-rl' تُخرَج الصفحة كأنها صفحة أفقية أُديرت ربع دورة باتجاه عقارب الساعة. يُنضَّد التدفق في إطار عرضه يساوي طول الورقة؛ وأسطره هي أعمدة النص العمودي، تُقرأ من اليمين، وكل ما يفعله المحرّك بالأسطر (الكسر وضبط الأسطر والعناصر العائمة والحواشي السفلية وقواعد إبقاء العناصر معًا) يعمل في ذلك الإطار. وعليه، على الورقة:
- عمود التخطيط هو طبقة (tier، 栏): يعطي
layoutType: 'double'طبقتين مكدّستين من الأعلى إلى الأسفل، تُملآن بدءًا من أعلى اليمين؛ وgutterWidthهو الفجوة بينهما، والخط الفاصل بين الأعمدة خطٌّ أفقي بينهما. لا تُوازَن الطبقات في نهاية الفصل (clreq §7.1.3.4): موازنة الأعمدة معطّلة في المستند العمودي ما لم يُضبطheadings.balancing.enabled. - ما يسمّيه التدفق «الأعلى» هو الحافة اليمنى للورقة، حيث تبدأ القراءة: العنصر العائم في الأعلى يقع على يمين الصفحة، والعائم في الأسفل على يسارها، وشريط صفحة الافتتاح الممتد على الصفحة شريطٌ ينزل على الحافة اليمنى، والحواشي السفلية تقع عند الطرف الأيسر لكل طبقة. وعنصر التصميم المثبّت إلى أعلى الصفحة (تصميم عنوان، إطار ثابت) مثبّت إلى الحافة اليمنى. القيمة
'left'للإعدادsideColumnSideهي الطبقة العليا، و'right'الطبقة السفلى؛ وتُقرأ'outer'و'inner'على أنهما'right'و'left'. - هوامش التدفق هي هوامش الورقة مُدارة: الهامش الأيمن هو أعلى التدفق، والهامش العلوي يساره. وتحتفظ
page.marginsبأسمائها على الورقة. - تبقى الترويسات وأرقام الصفحات وعلامات القص وخلفية الصفحة على الورقة، منضّدة أفقيًا، كما تصف clreq للكتب العمودية.
- تقف الأشكال والجداول قائمة. يملأ الشكل ارتفاع طبقته بقدر ما يسمح تعليقه، على ألا يزيد عرضه على عرض الصفحة؛ والعرض الذي يأخذه هو المساحة التي يشغلها في التدفق. يُنضَّد تعليقه أفقيًا تحته، وكذلك خلايا الجدول: كلاهما يُقاس نصًا أفقيًا. يُنضَّد الجدول قائمًا عبر الصفحة، وتُقطع صفوفه على قدر الطبقة حين يكون أطول منها. يضع
placement.alignالشكل في أعلى طبقته ('left') أو وسطها أو أسفلها. لا يُطبَّقplacement.rotateحيث يُشار إلى الشكل أول مرة في نص عمودي: يبلّغ البناء عن تحذير المحتوىrotateIgnoredVertical. أما القسم الأفقي من الكتاب (نمط عنوان يضبطlayoutفيه'horizontal-tb') فيدير أشكاله كما طُلب. - صورة التصميم (رسم صفحة افتتاح، أو رسم صفحة جزء) تقف قائمة أيضًا. يُحسب مقاس صندوقها في التدفق بعد تبديل عرض الصورة وارتفاعها، فيكون
size.widthهو المسافة التي تمتد فيها الصورة نزولًا في العمود، ويُستنتج عرضها على الورقة من تناسب أبعادها. - الحروف: تقف حروف الهان والكانا والصيغ كاملة العرض قائمة، كلٌّ منها بعرض em واحد؛ وتُدار الكلمات اللاتينية والأرقام جانبيًا بعروضها الأفقية؛ وتأخذ علامات الترقيم الصيغةَ العمودية من الخط. لا تُدار علامات الوقف والتوقف أبدًا: يضع خط البر الرئيسي 、。,. في الزاوية العليا اليمنى من الخانة و!?:; في نصفها الأيمن، ويوسّطها خطُّ تايوان أو هونغ كونغ (
cjk.region). تأخذ الأقواس صيغها العمودية، وتُقرأ “ ” ‘ ’ في نصوص البر الرئيسي على هيئة 『』「」. وتأخذ الشرطات وعلامات الحذف وشرطة الموجة الصيغةَ العمودية من الخط حيث يملكها (يربط Noto CJK صيغة — بـvertمعfwid: خطٌّ ينزل في وسط الخانة)، وإلا فتُدار مع توسيط حبرها على محور العمود. والـ 破折号 (——) في النص الصيني خطٌّ واحد ينزل في العمود: صيغه العمودية تترك فراغًا عند طرفي كل خانة، فتُدار كل شرطة مع السطر وتُمدّ كما في النص الأفقي (انظر عروض علامات الترقيم). والعدد الذي لا يزيد على رقمين يقف في خانة قائمة واحدة، ما لم يقع في جملة لاتينية فيتبع كلماتها (انظر الأرقام في النص العمودي). وتأخذ النقطة الوسطى (·) نصف خانة في نصوص البر الرئيسي وخانة كاملة في نصوص تايوان وهونغ كونغ. والعلامات التي يجعلها Unicode قائمةً (× © ± § ℃ ① وما شابهها) تقف في خانة خاصة بها، حتى داخل عدد:3×4هو 3 و4 مُداران جانبيًا وبينهما × قائمة. والفاصلة العليا أو النقطة الوسطى بين حرفين من كلمة لاتينية (don’t،l·l) تبقى في الكلمة، مُدارة جانبيًا. وتتبع الفقرة اللاتينية في تدفق عمودي القواعد نفسها. أما الصيغ الرياضية المدرجة في السطر والشارات (chips) وعيّنات الألوان (swatches) فتُدار جانبيًا مع السطر. - يُقاس كل حرف كما يُرسم: الخانة تتقدّم بمقدار خانتها على امتداد السطر، والمقطع المُدار جانبيًا بعرضه الأفقي. والنص الذي يبقى أفقيًا على الورقة (الترويسات وأرقام الصفحات والتعليقات وخلايا الجداول) يُقاس أفقيًا.
- تنطبق عروض علامات الترقيم وتعليق علامات الترقيم والمسافة بين حروف الهان والحروف اللاتينية على امتداد السطر نزولًا كما تنطبق عليه أفقيًا. الفراغ الذي يسبق الحرف يكون فوقه والفراغ الذي يليه تحته: تأخذ 、 بنمط Kaiming نصف خانة، وتنضغط
」「إلى خانة ونصف، ويبدأ القوس الافتتاحي المقصوص في رأس السطر أعلى بنصف خانة، وتقع 。 المعلّقة تحت أسفل سطرها، والمسافة بين الهان واللاتينية ربع em من العمود فوق الكلمة المُدارة جانبيًا وتحتها. وتحتفظ:;?!بخانة كاملة في النص العمودي في كل المناطق. - تَعُدّ شبكة الحروف الحروفَ نزولًا على السطر والأسطرَ عبر الصفحة: يحدّد
charsPerLineطول الطبقة، وlinesPerPageعدد الأسطر التي تتسع لها الصفحة، ويعطيlayoutType: 'double'طبقتين من حروف كاملة بينهما فاصل من وحدات em كاملة.
وللمضيفين الذين يقرؤون الإخراج: تحمل الصفحة العمودية VDTPage.flow. تكون contentArea والأعمدة والكتل والأسطر والعناصر العائمة ومساحات الحواشي وشريط صفحة الافتتاح وطبقات تصميم الكتل بإحداثيات التدفق؛ أما width وheight وheader وfooter فعلى الورقة. تحوّل flowToPage وpageToFlow وflowRectToPage وpageRectToFlow بين النظامين، وتخبر verticalOrientation(char, region) كيف يقف الحرف. يعطي flow.centralBaselines، لكل عائلة خط، المحور الذي وسّط عليه الإخراجُ الحروفَ القائمة: مركز حبر 中، الذي يمتد شُقّه الطويل على ارتفاع صندوق em كله (0.38 em فوق خط الأساس في Noto Serif وNoto Sans، بنسختيهما SC وTC على السواء)، والموجود في كل خط صيني وياباني وكوري. يُوسَّط كل سطر على ذلك المحور: يقع خط أساس السطر العمودي تحت أعلى صندوق سطره بمقدار نصف ارتفاع سطره مضافًا إليه خط الأساس المركزي للعائلة (أما خط أساس السطر الأفقي فيقع على 0.8 من ارتفاع سطره نزولًا)، فيقف عمود الحروف في منتصف خطوته (pitch)، ويقع الخط المرسوم بين عمودين على مسافة خطوة كاملة في منتصف المسافة بينهما. ويُنضَّد نص التصميم العمودي بالطريقة نفسها في أسطره الخاصة. يرسم Canvas الصفحة العمودية بنفسه؛ ولرسم علامات الترقيم بالصيغ العمودية للخط نفسه، يحمّل المضيف في المتصفح، مرة واحدة لكل عائلة، وجهًا توأمًا تكون فيه تلك الصيغ مفعّلة: loadVerticalAlternates(family, faces)، حيث faces هي مصادر العائلة (عناوين URL أو بايتات) وواصفاتها. ولا يُحتفظ بالتوأم إلا حيث يطبّق المتصفح الخاصية على نص Canvas (Chrome 140 وما بعده): يرسم 「(《 بالتوأم وبنسخة من الوجوه نفسها محمّلة دون الخاصية، بوزن الوجوه المعطاة وأسلوبها، ويحتفظ بالتوأم حين يختلف حبرهما. والاستدعاء اللاحق لوجوه أخرى من عائلة توأمها قيد الاستخدام (العريض بعد العادي) يضيفها إليه. يفعل Sandbox ذلك لكل عائلة في مستند عمودي. ودون توأم، تُدار الأقواس حول صندوق em الخاص بها، وتُنقل علامات الوقف والتوقف في نصوص البر الرئيسي داخل خانتها إلى حيث تضعها الصيغ العمودية للخط: 、。,. إلى الزاوية العليا اليمنى (في حدود 0.07 em من الصيغ العمودية في Noto Serif SC)، و!?:; نصف em إلى اليمين وقليلًا إلى الأعلى (في حدود 0.02 em). ويُنضّد PDF وHTML الصفحة نفسها: انظر النص العمودي في PDF والجدول في كيف يختلف إخراج HTML عن Canvas وPDF. وبالمقارنة خانةً خانة مع HarfBuzz (تخطيط عمودي مع vert) في Noto Serif TC وSC، يقف كل حرف من 「賈雨村」云云,宜乎?故曰!;:、。“引”‘單’…… في حدود 0.02 em منه في Canvas وفي PDF، وفي حدود 0.05 em في HTML (يوسّط Chrome الحرف المُدار على منتصف الصعود والنزول في الخط، وهو في Noto أعلى من مركز صندوق em بمقدار 0.05 em). يعطي flow.dashAdvances، لكل عائلة، التقدّم الأفقي بوحدات em لكل شرطة تمدّها الصفحة لتملأ خانتها (— – ― ⸺ ⸻ -)، للمُخرِج الذي لا يملك مقاييس خطوط خاصة به: يمدّ HTML الشرطة بها، كما يفعل Canvas وPDF من مقاييسهما. ويمكن تنضيد الترويسات وأرقام الصفحات عموديًا أيضًا: انظر عناصر النص العمودي.
#الخط الفاصل بين الأعمدة
يرسم خطًا عموديًا رفيعًا في الفاصل بين الأعمدة للفصل بينها بصريًا.
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
enabled | boolean | false | هل يُرسم الخط الفاصل بين الأعمدة. |
color | ColorValue | #cccccc | لون الخط. |
lineWidth | Dimension | 0.5 pt | سماكة الخط. |
تحت عنوان ممتد على الصفحة (span: 'page') يبدأ الخط حيث يبدأ نص الأعمدة، تحت شريط العنوان، سواء رسم الشريطَ تصميمُ الافتتاح الافتراضي أم تصميمٌ خاص بالعنوان. وحتى postext 1.4 كان يمتد من أعلى كتلة النص، عبر الشريط.
يمكن لنمط العنوان أن يضبط خطًا فاصلًا خاصًا به في layout الخاص به (انظر أنماط العناوين)، فترسمه صفحات قسمه. والحقل الذي يتركه النمط دون ضبط يأخذ قيمة المستند، فالقسم الذي لا يغيّر إلا أعمدته يحتفظ بالخط الفاصل الخاص بالمستند. وحتى postext 1.4 لم يكن خط النمط يُرسم أبدًا: كانت كل صفحة ترسم خط المستند.
#أنواع التخطيط
-
'single'— عمود واحد يمتد على كامل عرض المحتوى. الأنسب للصفحات الضيقة أو للمحتوى الغالب عليه النص ذي الفقرات الطويلة. -
'double'— عمودان متساويان في العرض. التخطيط التحريري الكلاسيكي، يُبقي طول السطر ضمن المدى الأمثل من 40 إلى 50 حرفًا لقراءة مريحة. -
'oneAndHalf'— تخطيط غير متماثل فيه عمود رئيسي وعمود جانبي أضيق. العمود الجانبي (يتحكم فيهsideColumnPercent) مناسب لملاحظات الهامش والأشكال الصغيرة والمحتوى المساند. تعمل القيم بين 25 و40% جيدًا، ويأخذ مسارٌ ضيق لأرقام الأسطر أو العلامات الهامشية نحو 10–15%. يساوي العمود الجانبيsideColumnPercent% من عرض المحتوى، والعمود الرئيسي هو ما يتبقى بعد الفاصل، فعند 50% يكون العمود الجانبي أعرض من الرئيسي بمقدار فاصل واحد. تُخرَج أي قيمة كما كُتبت ما دام كل عمود يحتفظ بـ 1% على الأقل من عرض المحتوى؛ أما القيمة التي تترك أيًا منهما أضيق من ذلك، أي 0 أو أقل، أو قيمة كبيرة إلى حدّ أن يختفي العمود الرئيسي خلف الفاصل، فتُقيَّد إلى أقرب قيمة تحفظ العمودين، والقيمة التي ليست عددًا تأخذ القيمة الافتراضية، 33. وعندئذ تحملconfigWarningsالخاصة بالمستند{ kind: 'sideColumnPercentClamped', path: 'layout.sideColumnPercent', value, used }، ويسمّي مسارُlayoutالخاص بنمط عنوان ذلك النمطَ، كما فيheadingStyles[2].layout.sideColumnPercent، ويُقاس على هوامش ذلك النمط، ويعرض Sandbox هذه التحذيرات في لوحة الفحوص (Checks)؛ وتعيدcollectConfigWarnings(config)القائمة نفسها دون إخراج أي شيء. والتخطيط الذي ليس'oneAndHalf'لا يقرأ القيمة أبدًا، ولا يبلّغ عنها أبدًا. معsideColumnRole: 'floats'لا يدخل نص المتن العمود الجانبي أبدًا: يصبح مسارًا للأشكال والجداول والإطارات الموضوعة بـspan: 'side'. يتكدّس الشكل أو الجدول الجانبي بدءًا من رأس المسار في الصفحة التي تستشهد به أولًا، فالشكل الهامشي في الكتاب المدرسي يقع في أعلى صفحته حتى حين يستشهد به النص في موضع أدنى؛ والذي لا يتسع له باقي المسار ينتظر مسار الصفحة التالية. ويتكدّس الإطار الجانبي بجوار النص الذي يقطعه، وحين لا يتسع له باقي المسار هناك ينزلق إلى الأعلى إلى أدنى موضع ما يزال يتسع له (أسفله على أسفل المسار)، أو ينتظر الصفحة التالية. وحين يستمر النص الذي يلي علامة إغلاق الإطار (fence) في الصفحة التالية (العمود ممتلئ، أو قواعد الكسر تنقل ذلك النص)، يبقى الإطار عند علامة إغلاقه، بجوار النص الذي قبله؛ أما النمط الذي فيهsideAtColumnEnd: 'after'فيضعه بمحاذاة السطر الأول من النص الذي يلي علامة الإغلاق بدلًا من ذلك، في مسار الصفحة التالية، كما تحتاج أرقام الأسطر والعناوين الهامشية المكتوبة قبل سطرها. وعنصر تصميم العنوان الذي يقف في المسار، كرقم فصل مثبّت في الهامش الخارجي، يُبعَد عنه التكدّس أيضًا (انظر الارتفاع المحجوز). وما تزال العناصر العائمة والإطارات ذاتspan: 'page'تعبر العمودين، والعنصر العائم في العمود ذوplacement.captionSideيضع تعليقه في المسار، بمحاذاة الشكل. ومع الهوامش المتناظرة وsideColumnSide: 'outer'، يقع المسار عند الحافة الخارجية لكل صفحة: العمود الهامشي في الكتاب المدرسي.
#رؤوس الصفحات وتذييلاتها
تتحكّم الخاصيتان header وfooter في خانتَي رأس الصفحة وتذييلها في كل صفحة. يُرسم رأس الصفحة وتذييلها داخل هوامش الصفحة القائمة: فلا يحجزان مساحة إضافية ولا يُقلّصان مساحة المحتوى.
إطار الحاوية. يوضع العنصر المثبّت إلى 'container' في شريط الهامش الواقع بين المتن وحدّ القص، ممتدًا بعرض مساحة المحتوى. تمتد حاوية رأس الصفحة من حدّ القص العلوي نزولًا إلى أعلى المتن، وحاوية التذييل من أسفل المتن نزولًا إلى حدّ القص السفلي. لذلك تُقاس مراسي top-* في رأس الصفحة ومراسي bottom-* في التذييل من حدّ القص، بينما تُقاس مراسي bottom-* في رأس الصفحة ومراسي top-* في التذييل من حافة المتن. لا تشمل الحاوية النزف ولا شريط علامات القص أبدًا، فيقع رأس الصفحة أو التذييل في الموضع نفسه من الصفحة المقصوصة سواء كان page.cutLines مفعّلًا أم معطّلًا. ثبّت العنصر إلى 'page' (صندوق القص) أو إلى 'bleed' ليتجاوز عرض مساحة المحتوى أو يبلغ النزف.
تستخدم الخانات نموذج خانة التصميم الموحّد: لكل عنصر placement فيه anchor (مرساة إلى الحاوية أو إلى عنصر آخر عبر #id)، وoffset اختياري، وsize اختياري. لا تزال الحقول المسطّحة القديمة align وmarginFromBody وmarginFromEdge وwidth: 'full' مقبولة في الإدخال، وتُرحَّل إلى الشكل الجديد تلقائيًا؛ والوصف المكافئ بالشكل الجديد موثّق أدناه.
تحمل كل خانة قائمة من عناصر النص والخط الفاصل والصندوق. ترتيب المصفوفة هو ترتيب الرسم (يُرسم العنصر الأول أولًا، ويُرسم الأخير فوق الجميع). ويصحّ هذا أيًّا كانت المراسي: قد يُثبَّت عنصر إلى عنصر يرد بعده في القائمة (anchor.to: '#ttl')، فيمكن أن يأتي صندوق الخلفية أولًا ويظل موضعه محسوبًا بالنسبة إلى النص الذي يقع خلفه.
القيم الافتراضية المدمجة. حين يكون header أو footer بقيمة undefined، يطبّق postext قيمة افتراضية مدمجة معقولة بدلًا من خانة فارغة:
- رأس الصفحة الافتراضي:
{title}بمحاذاة اليمين في الصفحات الفردية، و{chapterTitle}بمحاذاة اليسار في الصفحات الزوجية، وخط فاصل بالعرض الكامل، وكلها باللون الرئيسي للوحة الألوان، بخط Open Sans 8pt/600، وmarginFromBodyبقيمة16pt(للنص) /13pt(للخط الفاصل). - التذييل الافتراضي:
{pageNumber}في الوسط في كل صفحة باللون الرئيسي للوحة الألوان، بخط Open Sans 8pt/600، وmarginFromBodyبقيمة16pt.
للاستغناء عن القيم الافتراضية المدمجة، اضبط header: { elements: [] } (أو footer: { elements: [] }). تُحفظ المصفوفة elements الفارغة الصريحة على أنها «بلا عناصر»؛ ووحدها القيمة undefined تستدعي القيم الافتراضية.
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
elements | HeaderFooterElement[] | القيم الافتراضية المدمجة حين تكون undefined؛ و[] يعطّلها | قائمة مرتّبة من عناصر النص والخطوط الفاصلة. |
#عناصر النص
ترسم عناصر النص سلسلة قالب بعد استبدال العناصر النائبة (placeholders) فيها. تُكتب العناصر النائبة بالصيغة {name}؛ ويُخرج {{ و}} قوسين معقوفين حرفيين.
القيم الافتراضية في الجدول أدناه هي قيم عنصر نص تضيفه بنفسك. أما رأس الصفحة والتذييل المدمجان الموصوفان تحت القيم الافتراضية المدمجة أعلاه فهما عنصران جاهزان لهما قيمهما الخاصة (Open Sans 8pt/600 باللون الرئيسي للوحة الألوان)، وليسا القيم الافتراضية للعنصر.
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
kind | 'text' | — | المميِّز (discriminator). |
id | string | — | معرّف ثابت، فريد داخل الخانة. تُثبَّت إليه العناصر الأخرى بـanchor.to: '#id'. يعيّن Sandbox معرّفًا عند الإنشاء. |
content | string | '' | سلسلة القالب. تدعم العناصر النائبة المدرجة أدناه، إضافة إلى : سمة مكتوبة في سطر H1 للفصل الحالي (# Title ). تتحوّل السمة الغائبة إلى سلسلة فارغة دون تحذير. السطر الجديد، أو الحرفان \n مكتوبَين في القالب أو في قيمة سمة، يبدأ دائمًا سطرًا جديدًا أيًّا كانت قيمة overflow. أما النص الذي ينسخه عنصر نائب من المستند (عنوان، أو حقل من البيانات التمهيدية) فيُطبع كما كُتب: لا يبدأ فيه سطرًا جديدًا إلا فاصل أسطر حقيقي، مثل فاصل داخل عنوان. |
align | 'left' | 'center' | 'right' | 'justify' | 'start' | 'end' | 'center' | المحاذاة الأفقية للأسطر داخل صندوق العنصر. تمدّ 'justify' المسافات بين الكلمات في كل سطر ملفوف عدا الأخير من كل فقرة حتى يملأ السطر الصندوق؛ وتملأ الأسطر المجاورة لحرف استهلالي المساحة المجاورة له. ومع hyphenate، تُقطع أيضًا الكلمة التي لا تتسع لبقية سطر مضبوط عند حدّ مقطع لملئه. يُصفّ بمحاذاة اليسار السطرُ الأخير من الفقرة، والسطر الذي لا مسافة فيه تُمدّ، والنص الذي لا يلتف. يُخرَج النص المضبوط فقرةً فقرة، فتُعدّ الأسطر الفارغة بين الفقرات سطرًا واحدًا. تضع Canvas وPDF كل كلمة حيث وضعها الإخراج؛ أما HTML فيوسّع المسافات بـword-spacing. تتبع 'start' و'end' قيمة direction للنص: فهما يمين النص ويساره إذا كان من اليمين إلى اليسار. أما 'left' و'right' فهما جانبا الصندوق نفسه؛ وفي تصميم مُخرَج ضمن تدفّق صفحة من اليمين إلى اليسار (شريط صفحة افتتاح، تصميم عنوان) يكون التدفق معكوسًا، فيصيران بدايته ونهايته، كما في نص المتن. |
direction | 'ltr' | 'rtl' | 'auto' | قيمة المستند | الاتجاه الأساسي للنص: أين تذهب محارفه المحايدة، وترتيب مقاطعه في السطر، وما يعنيه الجانبان 'start' و'end'. تقرأ 'auto' أول حرف قوي الاتجاه في النص المحسوم، وتعود عند غيابه إلى direction المستند. تُقرأ مقاطع العربية والعبرية من اليمين إلى اليسار أيًّا كان الاتجاه الأساسي. النص الذي يحوي حرفًا عربيًا لا يُطبَّق عليه تباعد الحروف أبدًا (يُتجاهل letterSpacing للنص كله)، ولا تُقطع كلماته أبدًا عند اللف أو الاقتطاع؛ والكلمة الأعرض من الصندوق تفيض ويُبلَّغ عنها بـunbreakableWordOverflow. |
parity | 'all' | 'odd' | 'even' | 'all' | الصفحات التي يظهر فيها العنصر (بحسب زوجية رقم الصفحة أو فرديته: الصفحة 1 فردية). |
pages | 'all' | 'body' | 'opener' | 'part' | 'blank' | 'all' | أدوار الصفحات التي يظهر فيها العنصر، مقترنةً بـparity. بعد التوزيع تُصنَّف كل صفحة على أنها 'blank' (حشو للزوجية أو للفاصل، أو بلا محتوى)، أو 'part' (صفحة فاصلة لجزء)، أو 'opener' (أول كتلة فيها عنوان يمتد مستواه على الصفحة أو يفرض فاصل صفحة قبله: الصفحة الأولى من فصل)، أو 'body' (كل ما عدا ذلك). يُخفي pages: 'body' الترويسة في صفحات افتتاح الفصول؛ ويُظهر pages: 'opener' رقم الصفحة هناك فقط. |
fontFamily | string | 'EB Garamond' | عائلة الخط. |
fontSize | Dimension | 8 pt | حجم الخط. |
fontWeight | number | 400 | وزن الخط (100–900). |
italic | boolean | false | هل يُرسم النص بخط مائل. |
color | ColorValue | #000000 | لون النص. |
overflow | 'wrap' | 'ellipsis-start' | 'ellipsis-middle' | 'ellipsis-end' | 'clip' | 'ellipsis-end' | كيف يعالج المحرّك النص الذي يتجاوز العرض المتاح للعنصر. تكسر 'wrap' السطر إلى أسطر عدة؛ وتُبقي صيغ علامة الحذف كل سطر في سطر واحد وتقتطعه بـ… في بدايته أو وسطه أو نهايته؛ وتقصّ 'clip' النص قصًّا حادًّا عند الصندوق المحيط بالعنصر دون إدراج أي محرف. تنطبق فواصل الأسطر الموجودة في المحتوى في كل الأنماط: تقتطع أنماط الحذف والقص كل سطر على حدة. العنصر الذي يُغفل overflow يأخذ 'ellipsis-end'، فاضبط 'wrap' لكل ما قد يمتد إلى أسطر عدة (عنوان بريدي، أو عنوان طويل). النص الذي له dropCap يلتف أيًّا كانت هذه القيمة؛ ومع 'clip' تظل أسطره تُقص عند حافة الصندوق الثابت الارتفاع. تقطع 'ellipsis-end' و'ellipsis-start' عند حدّ كلمة، The history of… لا The history of th…، ولا تلاصق علامةَ الحذف أيُّ مسافة أو علامة ترقيم رابطة (فاصلة، نقطتان، شرطة، شرطة مائلة، قوس افتتاحي). ولا تُقطع كلمة حيث يلزم إلا حين يُبقي حدّ الكلمة، بعد إسقاط تلك العلامة، أقل من نصف ما يتسع: كلمة واحدة طويلة، أو عنوان URL (http://exampl…، لا http…). المسافة غير القاطعة والواصلة غير القاطعة (U+2011، كما في MS‑DOS) ليستا حدّ كلمة؛ أما 'ellipsis-middle' فتقطع في أي موضع لكنها تُسقط المسافات المجاورة لها. حتى postext 1.4 كانت كل الأنماط تقطع عند آخر محرف يتسع، بما في ذلك المسافات. |
verticalAlign | 'top' | 'middle' | 'bottom' | 'middle' | موضع النص داخل صندوق أطول من أسطره: placement.size.height ثابت، أو صندوق مدّه عنصر مجاور مثبّت. حين تكون الأسطر أطول من الصندوق (رقم كبير في صندوق ثابت الارتفاع، أو lineHeight ضيّق)، تفيض من الجهة التي تتركها المحاذاة حرّة، كما تفعل محاذاة CSS flex: تُبقي 'bottom' أسفل صندوق السطر الأخير على أسفل الصندوق وتفيض من الأعلى، وتفيض 'middle' بالتساوي من الطرفين، و'top' من الأسفل. حتى postext 1.4 كانت هذه الأسطر تتدلى دائمًا من الأعلى أيًّا كانت المحاذاة. يتحرك dropCap مع أسطره (حتى postext 1.4 كان يبقى في أعلى صندوق أنزلت 'middle' أو 'bottom' أسطره فيه). |
lineHeight | number | Dimension | 1.2 | تباعد أسطر العنصر. العدد مضاعف لـfontSize. ويُقبل Dimension أيضًا، على النحو الذي يُكتب به كل تباعد أسطر آخر في الإعدادات: em / rem هو المضاعف نفسه، والطول المطلق (pt، mm، px…) هو المسافة بين خطوط الأساس: يضبط تباعدًا قدره 15.5 pt أيًّا كان الحجم. وأي قيمة أخرى (صفر، أو عدد سالب، أو بُعد مشوّه) تأخذ القيمة الافتراضية. حتى postext 1.4 كان Dimension هنا يجعل ارتفاع التصميم غير قابل للقياس: فلم تكن صفحة الافتتاح تحجز أي مساحة، ولا حتى minHeight، وكان المتن يجري تحت العنوان. |
letterSpacing | Dimension | 0 | التتبّع (tracking): مسافة إضافية بعد كل محرف، بما في ذلك المسافات، تمامًا كما يفعل letter-spacing في CSS. تزداد العروض المقيسة معه، فيبقى الصندوق التلقائي العرض محكمًا. يُستبعد التتبّع الذي يلي آخر محرف في السطر من محاذاته ومن الصندوق التلقائي العرض، فيتوسّط العنوانُ المتتبَّع الموسَّط على حروفه، وينتهي العنوان المحاذى لليمين عند الحافة، ويبلغ آخر حرف في السطر المضبوط الحافة (حتى postext 1.4 كانت تقع على بعد نصف وحدة تتبّع، أو وحدة كاملة، إلى اليسار، وكان العنصر المثبّت إلى يمين عنصر متتبَّع يبتعد عنه وحدة تتبّع إضافية). القيمة السالبة تُقرّب الحروف، فعنوان العرض بحجم 36 pt يأخذ غالبًا { value: -0.3, unit: 'pt' }، وتنكمش العروض بالطريقة نفسها؛ وترسمه Canvas وHTML وPDF على النحو نفسه (حتى postext 1.4 كانت القيمة السالبة تُضبط على 0 دون تحذير). |
textTransform | 'none' | 'uppercase' | 'none' | تحويل حالة الحروف المطبّق على النص المحسوم، بما فيه العناصر النائبة: عنوان جزء مكتوب بالحروف الكبيرة في جدول المحتويات مثلًا. |
box | ElementBoxStyle | — | خلفية وحدّ اختياريان يُرسمان خلف النص: backgroundColor وborderColor وborderWidth وborderRadius، وpadding لكل جانب يوسّع الصندوق أبعد من النص (انظر عناصر الصندوق للاطلاع على الحقول). |
dropCap | { lines, fontFamily, fontWeight, fontSize, color, gap } | — | الحرف الاستهلالي: الحرف الأول مكتوبًا بحجم كبير بجوار أول lines أسطر (الافتراضي 2)، بخطه ووزنه ولونه الخاصة به، وعلى مسافة gap من النص. يقف الحرف على خط أساس آخر سطر يمتد عليه، ويكون fontSize افتراضيًا الحجمَ الذي يجعل أعلاه في مستوى الحروف الكبيرة للسطر الأول: حجم النص مضافًا إليه lines − 1 من تباعدات الأسطر، مع اعتبار الحروف الكبيرة 0.72 من حجم الحرف (والخط الذي تكون حروفه الكبيرة أطول من ذلك أو أقصر كثيرًا يحتاج إلى fontSize خاص به). النص الذي له حرف استهلالي يلتف أيًّا كانت قيمة overflow؛ ومع 'clip' تظل أسطره تُقص عند حافة الصندوق الثابت الارتفاع. يتبع color المرتبط بلوحة الألوان لوحاتِ ألوان الأجزاء والأقسام كبقية التصميم. وفي تصميم العنوان لا يحجز الحرف مساحة تحت النص: فالجزء من صندوق سطره الواقع تحت خط أساسه لا يدفع المتن إلى الأسفل. لذا قد يمتد الحرف الذي ينزل تحت خط أساسه (Q أو J في كثير من الخطوط) إلى المساحة الواقعة تحت التصميم: أعطِ العنوان marginBottom لذلك. حتى postext 1.4 كان الحجم الافتراضي يجعل الحرف بطول كل صناديق الأسطر التي يمتد عليها، فيعلو أعلاه فوق السطر الأول؛ وكانت أي قيمة لـoverflow غير 'wrap' تُسقط الحرف دون تحذير؛ وكانت لوحة ألوان القسم أو الجزء تترك لونه كما هو؛ وكان الحرف الذي يبلغ عمقُه عمقَ النص المجاور له قد يدفع المتن خط شبكة إلى الأسفل. الإعدادات المحفوظة قبل ذلك تحتفظ بحجم الإصدار 1.4، مكتوبًا في fontSize (انظر الحزم التي كتبها postext 1.4 أو أقدم). |
paragraphIndent | Dimension | 0 | إزاحة السطر الأول لكل فقرة بعد الأولى. يفصل بين الفقرات سطرٌ جديد في المحتوى، أو الحرفان \n في النص الآتي من قيمة سمة؛ وتُعدّ الأسطر الجديدة المتتالية سطرًا واحدًا. |
hyphenate | boolean | false | حين تكون true ويلتف النص (overflow: 'wrap'، أو dropCap الذي يلتف دائمًا)، تُقسَّم الكلمات الطويلة التي تظل تفيض بعد كسر السطر العادي عند حدود المقاطع (باستخدام لغة تقسيم الكلمات النشطة في المستند) مع واصلة مرنة عند موضع القطع. وفي النص المضبوط (align: 'justify') تُقطع أيضًا الكلمة التي لا تتسع لبقية السطر عند آخر حدّ مقطع يتسع، لملء السطر. |
inlineMarks | boolean | false | يقرأ النص المحسوم، بما فيه قيم العناصر النائبة، على أنه Markdown مضمّن: bold وitalic و^superscript^ و~subscript~. وحين يكون معطّلًا تُطبع العلامات كما كُتبت. انظر العلامات المضمّنة والخطوط المحيطية. |
stroke | { width, color?, hollow? } | — | خط محيطي يُرسم حول الحروف: width (قيمة Dimension متمركزة على حواف الحروف)، وcolor (الافتراضي: لون النص؛ والحرف الاستهلالي يأخذ لونه الخاص)، وhollow (true يرسم الخط المحيطي وحده). انظر العلامات المضمّنة والخطوط المحيطية. |
writingMode | 'horizontal-tb' | 'vertical-rl' | 'horizontal-tb' | يكتب 'vertical-rl' النص من الأعلى إلى الأسفل، والأسطر من اليمين إلى اليسار، والمحارف قائمة: ترويسة تنزل على الحافة الأمامية، أو عنوان عمودي بجوار فصل أفقي. انظر عناصر النص العمودي. |
reserve | boolean | true | لتصاميم العناوين فقط: هل يُحتسب العنصر ضمن الارتفاع الذي يحجزه العنوان في تدفق النص. false للزخرفة التي قد تقع تحت النص (ختم في أسفل الصفحة، أو إطار، أو شريط جانبي). انظر الارتفاع المحجوز. تتجاهله تصاميم رأس الصفحة والتذييل والأجزاء. |
marginFromBody | Dimension | 6 pt | المسافة المطلقة بين حافة العنصر المواجهة للمتن وحافة المتن. مستقلة عن العناصر الأخرى. تُرحَّل إلى placement.offset.y. |
marginFromEdge | Dimension | 0 pt | إزاحة أفقية من حافة المحتوى التي يحاذى إليها العنصر. لا تنطبق إلا حين يكون align بقيمة 'left' أو 'right'. تُرحَّل إلى placement.offset.x. |
placement | ElementPlacement | مشتقة من align + marginFromBody + marginFromEdge | تموضع متقدم (انظر أدناه). حين يُضبط، تكون له الأولوية على الحقول المسطّحة القديمة. |
العناصر النائبة المتاحة:
{pageNumber}: رقم الصفحة الحالية، بدءًا من 1.{totalPages}: العدد الإجمالي لصفحات المستند. في كتاب مُخرَج فصلًا فصلًا (Sandbox، وbuildBundle) يكون كل فصل مستندًا، فيكون هذا عدد صفحات الفصل نفسه.{bookTotalPages}: العدد الإجمالي لصفحات الكتاب كله: كل الفصول، بما فيها الصفحات الفارغة. وفي مستند مُخرَج وحده يساوي{totalPages}. انظر عدد صفحات الكتاب أدناه.{title}،{subtitle}،{author}،{publishDate}: قيم تُقرأ منcontent.metadata. البيانات الوصفية المجهولة أو الفارغة تُرسم سلسلةً فارغة (وتثير تحذيرًا في Sandbox).{chapterTitle}: نص آخر H1 في الصفحة الحالية أو قبلها. يُتخطّى H1 الذي يضبط نمط عنوانهrunningChapter: false(لوحة مصوّرة، أو خريطة).{chapterTitleAtTop}،{chapterNumberAtTop}: عنوان الفصل الساري في أعلى الصفحة ورقمه، ويختلفان عن{chapterTitle}و{chapterNumber}في صفحة يبدأ فيها فصل جديد تحت نص آخر. انظر الفصل في أعلى الصفحة أدناه.{partTitle}،{partNumber}: عنوان الجزء الحالي ورقمه (آخر صفحة:::partفي الصفحة الحالية أو قبلها؛ والصفحات الفارغة المضافة للزوجية قبل صفحة الجزء مباشرةً تنتمي إليه). فارغان قبل الجزء الأول.{firstMark.<key>}،{lastMark.<key>}: الكلمة الدليلية الأولى والأخيرة في الصفحة: عنوان من مستوى ما (h1–h6) أو مدخل من نمط فقرة. انظر الكلمات الدليلية أدناه.
عدد صفحات الكتاب
يطبع {bookTotalPages} عدد صفحات الكتاب كله، وهو العدد الذي يراه القارئ في «الصفحة 12 من 348». يعدّ الصفحات المادية، بما فيها الفارغة، مثل {totalPages}، لكن عبر كل الفصول:
- المستند المُخرَج وحده (
buildDocumentبلاcontinuation) هو الكتاب كله:{bookTotalPages}يساوي{totalPages}. buildBundleيُخرج الكتاب، ويجمع صفحات كل الفصول، ثم يُخرجه مرة أخرى بذلك المجموع، فيطبع كل فصل العدد نفسه. لا يحرّك العدد أي فاصل صفحة أبدًا، فتكفي جولة إضافية واحدة لاستقراره؛ والإعدادات التي لا تطبع{bookTotalPages}لا تكلّف شيئًا.- Sandbox يسلّم كل فصل المجموعَ حالما تُعرف صفحات كل الفصول. وحتى ذلك الحين يطبع الفصل عدد الصفحات حتى نهايته هو. ويعدّ تصدير الكتاب كاملًا في تبويب PDF الصفحاتِ التي يُخرجها: فإذا طبع فصلٌ عددًا آخر، لأن صفحاته لم تكن معروفة بعد، أخرج الكتاب مرة أخرى بالعدد الذي بلغته الفصول.
- المضيف الذي يُخرج الفصول بنفسه يمرّر المجموع في
continuation.bookPageCount(للفصل الأول أيضًا). ومن دونه يعدّ{bookTotalPages}الصفحات حتى نهاية المستند (continuation.pageIndexOffsetمضافًا إليه صفحاته)، وهذا صحيح للفصل الأخير فقط.
footer: {
elements: [{
kind: 'text', id: 'folio', content: '{pageNumber} / {bookTotalPages}',
fontSize: { value: 8, unit: 'pt' },
placement: { anchor: { to: 'container', edge: 'top' }, size: { width: 'auto', height: 'auto' } },
}],
}تخبر configUsesPlaceholder(config, 'bookTotalPages') المضيفَ هل يستحق العدد أن يُحسب.
الكلمات الدليلية: العلامة الأولى والأخيرة
يطبع القاموس في ترويسة كل صفحة أول مدخل فيها وآخر مدخل («Aback – Anchor»)؛ ويطبع الكتاب المرجعي أول قسم وآخره. يطبعهما {firstMark.<key>} و{lastMark.<key>}. ويحدّد المفتاح ما يُعلِّم الصفحة:
- من
h1إلىh6: عنوان من ذلك المستوى. العلامة نصّه، دون رقمه. - معرّف نمط فقرة (
entry): فقرة في حاوية:::paragraphs{style="entry"}. العلامة هي المقطع الغامق الذي تبدأ به الفقرة، أي المدخل، دون علامة الترقيم الختامية:**Aback.** Said of…تُعلِّمAback. والفقرة التي لا تبدأ بنص غامق لا تضع علامة.
{firstMark.<key>} هي أول علامة تبدأ في الصفحة، و{lastMark.<key>} آخرها. الصفحة التي لا تبدأ فيها أي علامة (مدخل طويل مستمر) تطبع العلامة السارية، أي آخر علامة قبلها، في الحالتين. والصفحات التي تسبق أول علامة لا تطبع شيئًا؛ وفي كتاب مُخرَج فصلًا فصلًا يعني ذلك ما قبل أول علامة في الفصل، إذ لا تنتقل العلامات من فصل إلى الذي يليه. العنوان أو الفقرة المقسومة بين صفحات تُعلِّم الصفحة التي تبدأ فيها فقط. يُكتب المفتاح بعد النقطة من حروف وأرقام و_ و-، ويبدأ بحرف أو بـ_؛ والمفتاح المجهول لا يطبع شيئًا.
:::paragraphs{style="entry"}
**Aback.** Said of the sails when pressed back against the mast.
**Abaft.** Towards the stern, or behind a given point.
:::header: {
elements: [{
kind: 'text', id: 'guide', content: '{firstMark.entry} – {lastMark.entry}',
fontSize: { value: 8, unit: 'pt' },
placement: { anchor: { to: 'container', edge: 'bottom' }, size: { width: 'auto', height: 'auto' } },
}],
}الكلمات الدليلية ترويسات: تُحسم في خانات رأس الصفحة والتذييل (بما فيها header وfooter لنمط العنوان) ولا تطبع شيئًا في تصاميم العناوين والأجزاء وجدول المحتويات؛ وفي تصاميم العناوين والأجزاء تشير إليها لوحة الفحوص بوصفها عناصر نائبة مجهولة. لإظهار أول مدخل في الصفحات الزوجية وآخر مدخل في الفردية، أعطِ عنصرين parity: 'even' وparity: 'odd'. وعنوان H1 الذي يضبط نمطُ عنوانه runningChapter: false لا يضع علامة h1.
الفصل في أعلى الصفحة
يسمّي {chapterTitle} و{chapterNumber} آخر فصل بدأ في الصفحة أو قبلها. وفي كتاب تتوالى فصوله دون فاصل صفحة بينها، تحمل الصفحة التي تُنهي فصلًا وتبدأ التالي قرب أسفلها عنوانَ الفصل الجديد فوق نص لا يزال ينتمي إلى القديم. أما {chapterTitleAtTop} و{chapterNumberAtTop} فيسمّيان الفصل الساري في أعلى الصفحة، كما تفعل الروايات ذات الفصول المتوالية وكثير من الكتب المرجعية:
- الصفحة التي أول كتلة فيها عنوان H1 لفصل تسمّي ذلك الفصل؛
- أي صفحة أخرى تسمّي الفصل الذي تستمر منه، حتى حين يبدأ فصل جديد في أسفلها؛
- الصفحة الفارغة المضافة للزوجية تتبع الصفحة التي بعدها، وفاصل أنماط
always-*يتبع الصفحة التي قبله (انظر ملكية الصفحات الفارغة)؛ وحين تبدأ الصفحة التي تلي صفحة فارغة للزوجية بنهاية فصل، لا بعنوان H1 له، تحتفظ الصفحة الفارغة بذلك الفصل أيضًا؛ - يُتخطّى عنوان H1 الذي له
runningChapter: false.
header: {
elements: [{
kind: 'text', id: 'chapter', content: '{chapterTitleAtTop}', parity: 'odd', pages: 'body',
fontSize: { value: 8, unit: 'pt' },
placement: { anchor: { to: 'container', edge: 'bottom-right' }, size: { width: 'auto', height: 'auto' } },
}],
}وكالكلمات الدليلية، كلاهما ترويسة: يُحسمان في خانات رأس الصفحة والتذييل ولا يطبعان شيئًا في تصاميم العناوين والأجزاء وجدول المحتويات. أما {attr.<key>} فيقرأ دائمًا آخر فصل بدأ.
المحاذاة المستنتجة من الحافة
حين يشير placement.anchor.to لعنصر نص إلى عنصر آخر عبر #id، تستتبع حافة المرساة محاذاةً افتراضية للنص في الأسطر الملفوفة:
right-ofوalign-leftتستتبعان محاذاة النصalign: 'left': تتدفق الأسطر الملفوفة نحو اليمين بدءًا من المرساة.left-ofوalign-rightتستتبعانalign: 'right': تلتصق الأسطر الملفوفة بالجانب الأقرب إلى العنصر المستهدف.
يطبّق محرّر العناوين في Sandbox هذه المحاذاة المستنتجة تلقائيًا حين تغيّر حافة المرساة أو هدفها. وهي تُبقي النص الملفوف على أسطر عدة مثبّتًا بصريًا إلى العنصر الذي يتصل به (فمثلًا يصطفّ حرف «P» من كلمة «Postext» الملفوفة عموديًا تحت حرف «I» من «Introduction»).
العلامات المضمّنة والخطوط المحيطية
يكتب عنصر النص نصّه بخط واحد افتراضيًا: تُطبع ** و^ وسائر علامات Markdown كما كُتبت. ومع inlineMarks: true يُقرأ النص المحسوم على أنه Markdown مضمّن، بالعلامات نفسها التي يقبلها نص المتن (انظر صيغة المستند › التنسيق المضمّن):
- يضبط
**bold**الوزن 700 (أوfontWeightالخاص بالعنصر إذا كان أثقل)؛ ويعكس*italic*ميل العنصر، فيخرج التوكيد داخل عنصر مائل قائمًا؛ ويفعل***both***الأمرين. وتعمل صيغ الشرطة السفلية (__bold__،_italic_) أيضًا. - يُكتب
^superscript^و~subscript~بنسبة 58% من الحجم، مع رفع النص العلوي بمقدار ثلثه وخفض النص السفلي بمقدار 0.15 منه؛ والنص السفلي والعلوي المتلاصقان (T~0~^2^) يُرصّان أحدهما فوق الآخر، كما في المتن. - تكتب الشرطة المائلة العكسية محرف العلامة نفسه (
\*،\_،\^،\~). يحتفظ الرابط بنصه؛ وتُحذف علامات الشيفرة (backticks).
تُقرأ العلامات بعد ملء العناصر النائبة، فيمكن أن تحملها القيمة نفسها: سطر المؤلفين في سمة عنوان يحصل على أرقام الانتساب بوصفها نصوصًا علوية. يعمل اللف والضبط وأنماط الحذف وdropCap وparagraphIndent كلها على النص المعلَّم؛ ويحتفظ كل مقطع بلون العنصر.
{ "kind": "text", "id": "authors", "content": "{attr.authors}", "inlineMarks": true, "overflow": "wrap",
"fontSize": { "value": 11, "unit": "pt" },
"placement": { "anchor": { "to": "#title", "edge": "below" }, "offset": { "y": { "value": 6, "unit": "pt" } } } }# Snow cover and river flow {authors="Ana Ruiz^1^, Luis Gil^2^ and Marta Sanz^1,3^"}يرسم stroke خطًا محيطيًا حول الحروف: width هو عرض الخط، متمركزًا على حواف الحروف (نصفه داخل الحروف ونصفه خارجها؛ ولا يتغير عرض النص المقيس)، وcolor قيمته الافتراضية لون النص، وhollow: true يترك الحروف بلا تعبئة فلا يظهر إلا الخط المحيطي: رقم عرض مفرّغ، أو عنوان يبرز فوق صورة فوتوغرافية. يُرسم الخط المحيطي فوق التعبئة، بالطريقة نفسها في Canvas، وفي HTML (-webkit-text-stroke)، وفي PDF (نمط رسم النص 2، أو 1 عند التفريغ).
{ "kind": "text", "id": "year", "content": "1863", "fontFamily": "Bitter", "fontSize": { "value": 120, "unit": "pt" }, "fontWeight": 700,
"color": { "hex": "#1d3557", "model": "hex" },
"stroke": { "width": { "value": 1.5, "unit": "pt" }, "hollow": true },
"placement": { "anchor": { "to": "page", "edge": "bottom-right" }, "offset": { "x": { "value": -15, "unit": "mm" }, "y": { "value": -20, "unit": "mm" } } } }في PDF، تُضمِّن مقاطعُ الغامق والمائل الخطوطَ المطابقة من عائلة خط العنصر، فيجب أن يوفّرها مزوّد الخطوط.
القيم الافتراضية للنص ومزالق التثبيت
يبدأ عنصر النص الذي تكتبه بنفسك من هذه القيم، وبعضها مفاجئ:
overflowقيمته'ellipsis-end'. النص الأعرض من مساحته يُقتطع إلى سطر واحد بـ…. اضبطoverflow: 'wrap'لعنوان بريدي أو سطر مؤلف أو أي عنوان قد يطول. فاصل الأسطر في المحتوى (سطر جديد، أو\nفي القالب أو في قيمة سمة) يبدأ سطرًا جديدًا في كل الأنماط؛ وتقتطع أنماط الحذف كل سطر على حدة.alignقيمته'center'وverticalAlignقيمته'middle'. العنصر التلقائي العرض يأخذ عرض أطول أسطره، فتتوسط أسطره بعضها بالنسبة إلى بعض؛ اضبطalign: 'left'لكتلة بمحاذاة اليسار (العنصر التلقائي العرض المثبّت إلى عنصر آخر يحاذي أسطره من جهة مرساته من تلقاء نفسه، انظر أعلاه).- الخط EB Garamond بحجم 8 pt، أسود، و
lineHeightقيمته 1.2، أيًّا كان ما يستخدمه نص المتن. وخلافًا لتباعدات الأسطر الأخرى في الإعدادات، يكونlineHeightلنص التصميم عادةً مضاعفًا بسيطًا (1.2)؛ ويعملDimensionأيضًا (انظر أعلاه).
العنصر الذي ليس له placement.size.width (أو قيمته 'auto') يحدّد حجمه وفق نصه، لكن فقط ضمن المساحة الواقعة بين نقطة مرساته وحافة الحاوية التي ينمو نحوها، وهي في مرساة top أو bottom ضعف المسافة إلى الحافة الأقرب. ويُحتسب offset الخاص به. الإزاحة بعيدًا عن تلك الحافة لا تكلّف شيئًا: ترويسة مثبّتة عند top-left مع x سالبة (متدلّية في الهامش الأيسر) لا تخسر أي مساحة، لأنها تنمو نحو اليمين. أما الإزاحة نحو تلك الحافة فتقلّص المساحة بالمقدار نفسه (وبضعفه في مرساة top أو bottom)، والإزاحة التي تدفع المرساة إلى ما بعد الحافة لا تُبقي شيئًا: مرساة top-right تقل قيمة x فيها عن سالب عرض الحاوية، أو مرساة top أُزيحت جانبيًا بأكثر من نصف ذلك العرض. وحين لا تبقى مساحة، لا يطبع نمط الحذف شيئًا ويرصّ 'wrap' محرفًا واحدًا في كل سطر. ثلاثة مخارج:
- أعطِ العنصر
size.widthثابتًا، فالعروض الثابتة لا تُقيَّد أبدًا؛ - ثبّته إلى
'page'أو'bleed'، فتصير الصفحة (أو النزف) إطار مساحته؛ - ثبّته إلى الحافة المقابلة من الحاوية.
تمتد حاوية رأس الصفحة من حدّ القص العلوي نزولًا إلى المتن، وحاوية التذييل من المتن نزولًا إلى حدّ القص السفلي: في رأس الصفحة تُقاس مراسي top-* من حدّ القص ومراسي bottom-* من المتن، والعكس في التذييل (انظر إطار الحاوية أعلاه). لا يحرّك رأس الصفحة أو التذييل نص المتن أبدًا، ويُرسم فوقه، فالعنصر المدفوع إلى منطقة المتن يغطي النص. أما شريط صفحة الافتتاح فيُرسم على العكس تحت نص المتن؛ والمساحة التي يأخذها في التدفق موصوفة تحت الارتفاع المحجوز.
#عناصر النص العمودي
عنصر النص الذي له writingMode: 'vertical-rl' يُكتب عموديًا في خانة نصها أفقي: ترويسات أي كتاب وأرقام صفحاته، وهي تبقى على الورقة، وكل تصميم في صفحة أفقية. ويُخرَج نصًّا أفقيًا في إطاره الخاص المُدار ربع دورة باتجاه عقارب الساعة، ثم يُعاد تدويره إلى الصفحة:
- يبقى صندوقه حيث يضعه التموضع. ارتفاعه هو طول السطر: يضبطه
size.height(أو'auto'، أي طول النص؛ أو'fill'، أي حتى حافة الحاوية)، ويضبطsize.widthعدد الأسطر التي تتسع عرضًا، ويحدّsize.maxWidthطول السطر. - يضع
alignالأسطر على امتداد الصندوق ('left'في الأعلى)، وverticalAlignعبره ('top'في اليمين، حيث يقف السطر الأول)، وتبقى حشوة الصندوق في الجانب الذي كُتبت له. - تُقاس المحارف وتُرسم كما في صفحة عمودية: محارف Han قائمة بعرض em واحد لكل منها، وعلامات الترقيم بصيغتها العمودية، والكلمات اللاتينية مُدارة جانبيًا، والأعداد القصيرة في خلية واحدة (
cjk.uprightDigits). وتضعه Canvas وPDF وHTML في المستطيل نفسه. - مع
inlineMarks: trueتميّز علامات الاتجاه مقطعًا كما في المتن:第:tcy[3.0]回يضع 3.0 في خلية واحدة، و:upright[GDP]يُقيم الحروف واحدًا تحت الآخر، و:sideways[…]يُدير مقطعًا؛ ولا ينكسر السطر داخل أيٍّ منها أبدًا. والعنصر الأفقي يتجاهلها. - لا يُضبط حرف استهلالي في عنصر عمودي.
- في تدفق صفحة عمودية (صفحة افتتاح، أو صفحة جزء، أو عنوان إطار) يجري النص نزولًا أصلًا، ولا يغيّر
writingModeشيئًا هناك.
تضع الكتب الصينية العمودية ترويساتها وأرقام صفحاتها في واحد من ثلاثة مواضع (clreq §7.2؛ وJLREQ §2.6 لليابانية):
| العُرف | الموضع | طريقة ضبطه |
|---|---|---|
| رأس وتذييل أفقيان | فوق منطقة النص وتحتها، كما في الكتب الأفقية؛ وهو الأكثر شيوعًا. | رأس الصفحة والتذييل كما هما. |
| الحافة الأمامية (أسلوب 中缝، وفي تايوان 邊峰) | نزولًا على الهامش الخارجي: عنوان الفصل أو الكتاب بدءًا من نحو أربعة محارف تحت رأس منطقة النص، ورقم الصفحة منتهيًا نحو خمسة محارف فوق أسفلها، بالأرقام الصينية، وبنحو 80 % من حجم المتن. | عنصران عموديان مثبّتان إلى 'outer'، كما أدناه. |
| الزاوية الخارجية السفلى | رقم الصفحة في أسفل الصفحة، في الزاوية الخارجية (قواعد تايوان لكتب 中式). | عنصر أفقي في التذييل عند 'bottom-left' مع parity: 'odd' وآخر عند 'bottom-right' مع parity: 'even' في كتاب مجلّد من اليمين (والعكس في كتاب مجلّد من اليسار). |
ترويسات الحافة الأمامية كما يضيفها Sandbox (الترويسة › ترويسات الحافة الخارجية (عمودية))، هنا لمتن بحجم 10 pt (يضبطها Sandbox بنسبة 80 % من حجم المتن):
{
"page": { "pageNumbering": { "format": "trad-chinese-informal" } },
"header": { "elements": [
{ "kind": "text", "id": "head", "content": "{chapterTitle}", "writingMode": "vertical-rl",
"fontSize": { "value": 8, "unit": "pt" }, "overflow": "clip", "align": "left",
"placement": { "anchor": { "to": "outer", "edge": "top" }, "offset": { "y": { "value": 4, "unit": "em" } } } },
{ "kind": "text", "id": "folio", "content": "{pageNumber}", "writingMode": "vertical-rl",
"fontSize": { "value": 8, "unit": "pt" }, "overflow": "clip", "align": "left",
"placement": { "anchor": { "to": "outer", "edge": "bottom" }, "offset": { "y": { "value": -5, "unit": "em" } } } }
] }
}anchor.to: 'outer' (انظر تموضع العناصر) هو الهامش الخارجي لكل صفحة، فيجري العنصران نزولًا على الحافة اليسرى للصفحة الفردية والحافة اليمنى للصفحة الزوجية في كتاب مجلّد من اليمين (page.binding). ويُطبع {pageNumber} بصيغة ترقيم الصفحات: trad-chinese-informal يعطي 一百零三 في الصفحة 103، وcjk-decimal يعطي 一〇三. وتظل قواعد كل خانة سارية: pages: 'body' يُبعد الترويسة عن صفحات افتتاح الفصول. والزخرفة القصيرة بين الترويسة ورقم الصفحة (ذيل سمكة ︻، أو خط فاصل) عنصر عادي مثبّت إلى الإطار نفسه.
في VDT تحمل الكتلة العمودية vertical (VDTDesignTextBlock.vertical: المنطقة، والأرقام القائمة، والمحور المركزي لكل عائلة خط)؛ وأسطرها في إطار الكتلة المُدار الخاص بها، xOffset نزولًا من أعلى الصندوق، وbaselineY نحو اليسار من حافته اليمنى. ويحمل مقطع السطر المعلَّم tcy أو orientation، كما يحملهما مقطع في المتن.
#عناصر الخط الفاصل
ترسم عناصر الخط الفاصل خطًا: أفقيًا عبر الخانة، أو عموديًا نزولًا فيها.
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
kind | 'rule' | — | المميِّز. |
id | string | — | معرّف ثابت، فريد داخل الخانة، للإشارات anchor.to: '#id'. |
direction | 'horizontal' | 'vertical' | 'horizontal' | يمتد الخط الأفقي على طول placement.size.width ('fill' = حتى حافة الحاوية) ويكون ارتفاعه thickness. ويمتد الخط العمودي نزولًا على طول placement.size.height ('fill' أو بلا قيمة = حتى حافة الحاوية) ويكون عرضه thickness: فاصل بين ترويسة ورقم صفحة مثلًا. |
color | ColorValue | #000000 | لون الخط. |
thickness | Dimension | 0.5 pt | سماكة الخط. الخط الفاصل الذي يُغفلها يُرسم بالقيمة الافتراضية (حتى postext 1.4 لم يكن يرسم شيئًا). |
width | Dimension | 'full' | 'full' | 'full' يمتد على مساحة المحتوى؛ وقيمة Dimension تقيّد الخط بطول ثابت يحدّد align موضعه. |
align | 'left' | 'center' | 'right' | 'center' | المحاذاة حين لا يكون width بقيمة 'full'. |
marginFromBody | Dimension | 6 pt | المسافة المطلقة بين حافة الخط المواجهة للمتن وحافة المتن. مستقلة عن العناصر الأخرى. |
marginFromEdge | Dimension | 0 pt | إزاحة أفقية من حافة المحتوى التي يحاذى إليها الخط. لا تنطبق إلا حين يكون width قيمة Dimension ثابتة وalign بقيمة 'left' أو 'right'. |
parity | 'all' | 'odd' | 'even' | 'all' | الصفحات التي يظهر فيها الخط. |
pages | 'all' | 'body' | 'opener' | 'part' | 'blank' | 'all' | أدوار الصفحات التي يظهر فيها الخط (انظر الحقل pages لعنصر النص). |
reserve | boolean | true | لتصاميم العناوين فقط: هل يُحتسب الخط ضمن الارتفاع الذي يحجزه العنوان في تدفق النص (انظر الارتفاع المحجوز). |
placement | ElementPlacement | مشتقة من align + marginFromBody + marginFromEdge | تموضع متقدم (انظر تموضع العناصر). يضبط size.width / size.height طول الخط؛ وwidth: 'fill' هو 'full' القديم. |
#عناصر الصندوق
ترسم عناصر الصندوق مستطيلًا مستدير الزوايا داخل الخانة، وهو مفيد خلفيةً للنص في صفحات افتتاح الفصول أو الأشرطة الجانبية أو التذييلات. تُموضَع عناصر الصندوق حصرًا عبر الحقل placement؛ وليس لها اختصار مسطّح قديم. وتقع التعبئة والحدّ ونصف قطر الزوايا في الكائن المتداخل style (ElementBoxStyle)، كما في مثال JSON تحت «تموضع العناصر».
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
kind | 'box' | — | المميِّز. |
id | string | — | معرّف ثابت، فريد داخل الخانة. تُثبَّت إليه العناصر الشقيقة بـanchor.to: '#id'. يعيّن Sandbox معرّفًا عند الإنشاء. |
style.backgroundColor | ColorValue | transparent | لون التعبئة. اضبطه على transparent لصندوق بحدّ فقط. |
style.borderColor | ColorValue | transparent | لون الحدّ. |
style.borderWidth | Dimension | 0 pt | عرض الحدّ. يُرسم الحدّ داخل المستطيل المحيط بالصندوق فتبقى الأبعاد الخارجية ثابتة: تجري الحافة الخارجية للحدّ على حافة الصندوق، ويحتفظ الصندوق المستدير بنصف قطره الخارجي. والحدّ الذي يساوي عرض الصندوق يملؤه. ترسمه Canvas وHTML وPDF على النحو نفسه (حتى postext 1.4 كانت Canvas وPDF تُمركزان الحدّ على الحافة، فيقع نصفه خارج الصندوق). |
style.borderRadius | Dimension | 0 pt | نصف قطر الزاوية. يُقيَّد عند الرسم بنصف الضلع الأقصر. |
placement | ElementPlacement | — | إلزامي. انظر «تموضع العناصر» أدناه. |
parity | 'all' | 'odd' | 'even' | 'all' | الصفحات التي يظهر فيها الصندوق. |
pages | 'all' | 'body' | 'opener' | 'part' | 'blank' | 'all' | أدوار الصفحات التي يظهر فيها الصندوق (انظر الحقل pages لعنصر النص). |
reserve | boolean | true | لتصاميم العناوين فقط: هل يُحتسب الصندوق ضمن الارتفاع الذي يحجزه العنوان في تدفق النص (انظر الارتفاع المحجوز). |
#عناصر الصورة
يرسم عنصر image موردًا نقطيًا (bitmap) أو SVG من المستند: شعار ناشر في صفحة العنوان، أو علامة في ترويسة. يتحدد حجمه بـplacement.size: إذا تُرك أحد البعدين width / height بقيمة 'auto' (الافتراضية) تبع البعد الآخر نسبة أبعاد الصورة؛ وإذا ضُبط الاثنان، تُلاءم الصورة داخل الصندوق وتُوسَّط. والمورد الغائب أو الذي ليس صورة لا يرسم شيئًا.
{
kind: 'image', id: 'logo', resourceId: 'logo-publisher',
placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: { value: 64, unit: 'mm' }, y: { value: 233, unit: 'mm' } }, size: { width: { value: 83, unit: 'mm' }, height: 'auto' } },
}يقبل resourceId العناصر النائبة التي يقبلها content لعنصر النص، فيرسم تصميم واحد صورة مختلفة لكل عنوان. مع resourceId: '{attr.vignette}' في نمط عنوان، يرسم # Chapter I {style="opener" vignette="log"} المورد log ويرسم # Chapter II {style="opener" vignette="wig"} المورد wig: فتتشارك الفصول النمط بدلًا من نسخة منه لكل صورة. يقرأ رأس الصفحة أو التذييل سمات فصل الصفحة، كما يفعل {attr.<key>} في الترويسة، وتعمل العناصر النائبة الأخرى أيضًا ('map-{chapterNumber}'). والمعرّف الذي يخرج فارغًا، كما في عنوان بلا السمة، لا يرسم شيئًا. متاح منذ postext 1.8.
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
id | string | — | معرّف ثابت؛ يمكن أن تُثبَّت إليه العناصر الأخرى بـ#id. |
resourceId | string | — | معرّف Resource نقطي أو SVG في المستند. قد يحوي عناصر نائبة، منها {attr.<key>}، تُملأ لكل عنوان أو جزء أو صفحة. |
decorative | boolean | false | الصورة زخرفية فقط (حلية، أو شريط): لا تعطي المخرجاتِ نصًا بديلًا حتى حين يكون لموردها نص بديل (انظر أدناه). |
placement | ElementPlacement | — | المرساة والإزاحة والحجم (انظر تموضع العناصر). الجانب الذي قيمته 'fill' يمتد حتى حافة الحاوية. |
parity, pages | كما سبق | 'all' | الصفحات التي تظهر فيها الصورة. |
reserve | boolean | true | لتصاميم العناوين فقط: هل تُحتسب الصورة ضمن الارتفاع الذي يحجزه العنوان في تدفق النص (انظر الارتفاع المحجوز). |
يُضمّن مُخرِج PDF المورد كما يُضمّن الشكل (ويستخدم SVG الذي له نسخة طباعة رئيسية تلك النسخة)؛ ويحسمه عارض HTML عبر resourceImageUrl.
الصورة التي يرسمها تصميم تُعدّ محتوى حين يصفها موردها: يدخل altText المورد، وإلا فتعليقه نصًّا عاديًا (الشارة تُقرأ بتسميتها، و:ref بنصه text إن كان له)، إلى VDT (VDTDesignImageBlock.altText)، ويصير alt لعنصر <img> الخاص بها في HTML، وFigure مع /Alt في PDF موسوم، ويُقرأ مباشرةً بعد نص تصميمها (لوحة الفصل بعد عنوان الفصل). أما الصورة التي ليس لموردها أيٌّ منهما، والصورة المعلَّمة decorative، فزخرفة: alt="" مع role="presentation"، وعنصر زخرفي (artifact) في PDF. وصورة الترويسة أو التذييل تتكرر في كل صفحة، فهي من أثاث الصفحة أيًّا كان ما يقوله موردها: لا altText في VDT، وalt="" مع role="presentation" في HTML، وعنصر زخرفي للصفحة في PDF.
#تموضع العناصر
ElementPlacement هو نموذج التموضع الموحّد الذي يستخدمه كل نوع من العناصر (نص، وخط فاصل، وصندوق) داخل أي خانة تصميم: رأس الصفحة، أو تذييلها، أو خانة التصميم المتقدم لمستوى عنوان. وتصف التموضعَ ثلاثُ قطع من الحالة:
interface ElementPlacement {
/** What this element anchors to and which edge of that target. */
anchor: {
to: 'container' | 'page' | 'bleed' | 'outer' | `#${string}`; // container = the slot; page = trim box; bleed = trim box + bleed; outer = the outer margin (header, footer); #id = another element
edge: AnchorEdge;
};
/** Distance from the anchor point. */
offset?: { x?: Dimension; y?: Dimension };
/** Optional fixed width / height. Width also accepts 'fill' (span the slot).
* `maxWidth` caps an 'auto' width (text): the element still shrink-wraps its
* content, so elements anchored to it stay attached, but a long text wraps or
* ellipsizes there — a running head can reserve room for the label hanging
* off it instead of squeezing that label out. */
size?: { width?: Dimension | 'fill' | 'auto'; height?: Dimension | 'fill' | 'auto'; maxWidth?: Dimension };
}يقبل AnchorEdge:
- حواف الحاوية (حين يكون
anchor.toبقيمة'container'أو'page'أو'bleed'):top،top-left،top-right،bottom،bottom-left،bottom-right،left،right. - الحواف النسبية إلى عنصر (حين يكون
anchor.to === '#someId'):right-of،left-of،below،above،align-top،align-bottom،align-left،align-right.
تضع كل حافة نسبية زاويةً من زوايا العنصر على زاوية من زوايا العنصر الذي يُثبَّت إليه، ثم يحرّكه offset من هناك:
right-of: زاويته العليا اليسرى على الزاوية العليا اليمنى للهدف (بجواره، وأعلاهما في مستوى واحد)؛left-of: زاويته العليا اليمنى على الزاوية العليا اليسرى للهدف؛below: زاويته العليا اليسرى على الزاوية السفلى اليسرى للهدف (تحته، وحافتاهما اليسريان في مستوى واحد)؛above: زاويته السفلى اليسرى على الزاوية العليا اليسرى للهدف؛align-topوalign-left: زاويته العليا اليسرى على الزاوية العليا اليسرى للهدف. يعطي الاسمان التموضع نفسه: تصطف الحافتان العليا واليسرى كلتاهما؛align-bottom: زاويته السفلى اليسرى على الزاوية السفلى اليسرى للهدف؛align-right: زاويته العليا اليمنى على الزاوية العليا اليمنى للهدف.
الحافة النسبية إلى عنصر المستخدمة مع 'container' أو 'page' أو 'bleed'، وحافة الحاوية المستخدمة مع '#id'، تُقرآن على أنهما الزاوية العليا اليسرى.
يثبّت anchor.to: 'page' العنصر إلى صندوق القص (الصفحة المادية بعد القص)، ويثبّته 'bleed' إلى صندوق القص موسّعًا بمقدار cutLines.bleed من كل جانب (وهو مطابق لصندوق القص ما دامت خطوط القص معطّلة). ويصير الإطاران أيضًا المرجع لـsize: 'fill' وللتقييد التلقائي للعرض، فيمكن أن يمتد شريط ملوّن من حافة إلى حافة بصرف النظر عن هوامش الصفحة:
{ "kind": "box", "id": "band", "placement": { "anchor": { "to": "bleed", "edge": "top-left" }, "size": { "width": "fill", "height": { "value": 6, "unit": "cm" } } }, "style": { "backgroundColor": { "hex": "#1d3557", "model": "hex" } } }مع تفعيل خطوط القص، يُقص كل ما يرسمه عنصر خارج صندوق النزف (انظر خطوط القص).
يثبّت anchor.to: 'outer' (في خانتي رأس الصفحة والتذييل) العنصر إلى الهامش الخارجي للصفحة: من حافة منطقة النص إلى حدّ القص في الجهة البعيدة عن الكعب، ومن رأس منطقة النص إلى أسفلها. ويقع على يمين الصفحة الفردية ويسار الزوجية في كتاب مجلّد من اليسار، والعكس في كتاب مجلّد من اليمين (page.binding)، فيخدم عنصر واحد الصفحتين المتقابلتين كلتيهما: ترويسة تنزل على الحافة الأمامية (انظر عناصر النص العمودي). وفي أي خانة أخرى يُقرأ على أنه 'container'. ويمكن أن يُكتب offset لعنصر نص بوحدة em، أي بوحدات em من fontSize الخاص به: أربعة محارف تحت رأس منطقة النص هي { "y": { "value": 4, "unit": "em" } }.
داخل خانة التصميم المتقدم لعنوان، لا تزيد العناصر المثبّتة إلى الصفحة أو النزف الارتفاعَ المحجوز للعنوان إلا إذا امتدت تحت الحافة العليا للعنوان (الشريط الممتد عبر أعلى الصفحة يقع خلف صفحة الافتتاح؛ والشريط الذي يبلغ ما تحت العنوان يدفع نص المتن إلى الأسفل). استخدم advancedDesign.minHeight لحجز ارتفاع ثابت لصفحة الافتتاح في كل الأحوال، وreserve: false في العنصر الذي لا ينبغي أن يدفع النص أبدًا. القواعد الكاملة تحت الارتفاع المحجوز.
لكل عنصر id ثابت (يعيّنه Sandbox تلقائيًا؛ ويمكنك أيضًا ضبطه يدويًا). وتشكّل العناصر المثبّتة إلى عناصر أخرى رسمًا بيانيًا صغيرًا للاعتماديات يحسمه المحرّك قبل القياس، فيمكن أن يتسلسل عنصر من آخر دون إحداثيات يدوية.
يُحلَّل الشكل القديم align + marginFromBody + marginFromEdge عند الإدخال ويُعاد كتابته تموضعًا وقت حسم الإعدادات، فتظل الإعدادات القائمة تعمل دون تغيير.
#نص المتن
تتحكم الخاصية bodyText في طباعة نص الفقرات كله.
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
fontFamily | string | 'EB Garamond' | عائلة الخط لنص المتن. أي خط من Google Fonts، أو خط من خطوط النظام، أو عائلة مخصّصة معرّفة في customFonts. عائلة واحدة، لا سلسلة خطوط CSS (انظر أدناه). |
fontSize | Dimension | 8 pt | حجم الخط الأساسي لنص المتن. |
lineHeight | Dimension | 1.5 em | المسافة العمودية بين الأسطر. الوحدات النسبية (em و rem) تتغيّر مع حجم الخط. |
paragraphSpacing | boolean | false | عند التفعيل، يُدرج سطرًا فارغًا (يساوي lineHeight) بين الفقرات المتتالية، للفصل بينها على طريقة دور النشر. |
color | ColorValue | #000000 | لون النص. |
boldColor | ColorValue | اللون الرئيسي (#295AA3) | اللون المطبّق على المقاطع العريضة (bold/strong). يُحسم وفق المدخل main-color في لوحة الألوان الافتراضية، فتغيير لون اللوحة يعيد تلوين كل المقاطع العريضة في المستند. |
italicColor | ColorValue | اللون الرئيسي (#295AA3) | اللون المطبّق على المقاطع المائلة والمؤكَّدة (italic/emphasis). القيمة الافتراضية المرتبطة باللوحة نفسها كما في boldColor. |
referenceColor | ColorValue | اللون الرئيسي (#295AA3) | اللون المطبّق على تسميات :ref المضمَّنة في السطر (الإحالات إلى الموارد). القيمة الافتراضية المرتبطة باللوحة نفسها كما في boldColor. يتبع اللوحة منذ postext 1.5؛ وحتى 1.4 كان يبقى #295AA3 أيًّا كان اللون الرئيسي. |
referenceBold | boolean | true | ارسم تسميات :ref المضمَّنة بالخط العريض. |
referenceItalic | boolean | false | ارسم تسميات :ref المضمَّنة بالخط المائل. |
emphasis | 'auto' | 'italic' | 'bold' | 'color' | 'overline' | 'auto' | كيف يُنضَّد …: بالمائل، أو بالوجه العريض وبلون boldColor، أو قائمًا بلون italicColor، أو قائمًا مع خط فوق الكلمات. القيمة 'auto' تعني 'bold' في مستند مكتوب بالحرف العربي، و'italic' في أي مستند آخر. انظر النص العربي. |
tashkil | 'keep' | 'strip' | 'strip-vowels' | 'keep' | علامات التشكيل العربية: تُنضَّد كما كُتبت، أو تُحذف كلها، أو تُحذف ما عدا الشدّة. انظر النص العربي. |
textAlign | 'left' | 'justify' | 'start' | 'end' | 'justify' | محاذاة النص. 'left' (أو 'start') هو الجانب الذي يبدأ منه السطر: يمين الفقرة المكتوبة من اليمين إلى اليسار (انظر اتجاه النص). النص المضبوط يوزّع المسافات على كل سطر لتستوي الحافتان. وتُرسم الأسطر الأخيرة من الفقرات المضبوطة غير مضبوطة بعرضها الطبيعي — إلا حين تقبل خوارزمية Knuth-Plass سطرًا أخيرًا زائد الامتلاء يعتمد على انكماش المسافات المرنة (glue)، فتنضغط حينئذ المسافات بين الكلمات ليملأ السطر عرضه تمامًا (دلالة ضبط المسافات المرنة في TeX، مطبّقة بالطريقة نفسها في مُخرِجات Canvas وHTML وPDF). |
fontWeight | number | 400 | وزن النص العادي (100–900). |
boldFontWeight | number | 700 | وزن النص العريض (100–900). |
hyphenation | HyphenationConfig | مفعّل، 'en-us' | إعدادات التقسيم الآلي للكلمات بالواصلة. انظر أدناه. |
firstLineIndent | Dimension | 1.5em | المسافة البادئة المطبّقة على السطر الأول من كل فقرة (أو على كل الأسطر عدا الأول حين تُفعَّل المسافة البادئة المعلّقة). |
hangingIndent | boolean | false | عند التفعيل، تُطبَّق المسافة البادئة على كل الأسطر عدا الأول (المسافة البادئة الفرنسية أو المعلّقة). |
indentAfterHeading | boolean | true | حين يُضبط على false، تُرسم الفقرة الأولى التالية مباشرة لعنوان بلا مسافة بادئة للسطر الأول — وهو عُرف طباعي شائع في المنشورات العلمية وفي كثير من أساليب الكتب. وينطبق الأمر نفسه على الفقرة التي تلي مباشرة سطر :::space. ويُتجاوَز الإطار المنضَّد خارج النص الواقع بينهما — في العمود الجانبي (span: 'side')، أو العائم إلى رأس الصفحة أو ذيلها، أو الثابت — وكذلك الشكل العائم: ففي عمودها تظل الفقرة تالية للعنوان، وتُنضَّد بلا مسافة بادئة. أما الإطار المنضَّد داخل النص (placement: 'here') فيُحتسب، وتُزاح الفقرة التي تليه. لا أثر له حين تكون hangingIndent مفعّلة. |
maxWordSpacing | number | 2 | الحد الأعلى لتباعد الكلمات في النص المضبوط، معبَّرًا عنه بمضاعِف لعرض المسافة العادية. تُبقي Knuth-Plass ضمنه كل سطر تسمح به الفقرة، فتقسم كلمة بالواصلة أو توزّع الفراغ الزائد على الأسطر المجاورة أولًا؛ والسطر الذي لا تُبقيه ضمنه أي مجموعة من نقاط الكسر يتمدّد متجاوزًا إياه، والسطر الذي يتجاوز 3× المسافة العادية يُنضَّد غير مضبوط. الأسطر التي تتجاوز هذه النسبة تُعدّ «متخلخلة»: انظر الأسطر التي يعجز الكاسر عن ملئها، وmaxJustifyTracking للسماح لها بقليل من التتبّع بدلًا من ذلك. |
minWordSpacing | number | 0.6 | الحد الأدنى لتباعد الكلمات في النص المضبوط، بمضاعِف لعرض المسافة العادية. |
maxJustifyTracking | number | 0 | أقصى تتبّع يجوز أن يأخذه السطر المضبوط، بأجزاء الألف من em في الاتجاهين (وحدة InDesign: 10 = 0.01 em لكل حرف)، حين تتمدّد مسافات كلماته وحدها إلى ما بعد maxWordSpacing أو تنكمش إلى ما دون minWordSpacing. ما يتجاوز الحدّ من التعديل يذهب إلى الحروف، فتعود مسافات السطر المتخلخل إلى maxWordSpacing ويستقر السطر المضغوط عند minWordSpacing. تزنه Knuth-Plass حين تختار نقاط الكسر، وللأسطر التي يتجاوز بها تباعدُ الكلمات وحده الحدودَ فقط: أما الأسطر الأخرى، والسطر الأخير من الفقرة (ما لم يتجاوز عرضه)، والسطر المكوّن من كلمة واحدة، والسطر الذي يحوي شارة (chip)، فلا تأخذ منه شيئًا. يسجّله السطر في letterSpacing، وترسمه مخرجات Canvas وHTML وPDF. يتطلب optimalLineBreaking. القيمة 0 توقفه. انظر التتبّع ملاذًا أخيرًا. |
kashida | 'auto' | 'none' | 'auto' في مستند مكتوب بالحرف العربي، وإلا 'none' | ضبط الأسطر بالكشيدة (kashida): يأخذ السطر المضبوط من نص مكتوب بالحرف العربي فراغه الزائد في المسافات بين كلماته (حتى ربع عرضها) ثم في الكشيدات، وهي محارف تطويل (tatweel) كاملة (U+0640) تُدرج بين حرفين متصلين، ولا يأخذه أبدًا تباعدًا بين الحروف. تحتسب Knuth-Plass إطالة كل كلمة تمدّدًا. لا كشيدة أبدًا في الكلمات اللاتينية، ولا في الأرقام، ولا في العناوين، ولا في الأسطر غير المضبوطة، ولا في السطر الأخير من الفقرة. تُرسم محارف التطويل لكنها تُستبعد من النص المجرّد والنص المنسوخ. انظر الكشيدة في النص العربي. |
kashidaPatterns | 'auto' | 'naskh' | 'simple' | 'nastaliq' | 'auto' | أيّ مواضع الاتصال تأخذ كشيدة، وبأي ترتيب: قواعد النسخ الكلاسيكية، أو أولويات Microsoft، أو قواعد النسخ المعدّلة للنستعليق (على طريقة raqim-kashida). القيمة 'auto' تقرأ خط المتن: لا كشيدة في خط رقعة أو ديواني (Aref Ruqaa)، وقواعد النستعليق في خط نستعليق، والنسخ فيما عدا ذلك. |
kashidaPerWord | number | 1 | أقصى عدد من الإطالات في الكلمة الواحدة. |
kashidaMaxLength | number | 0.6 | أطول إطالة عند موضع اتصال واحد، بوحدة em؛ وتأخذ من محارف التطويل الكاملة ما يتّسع له. |
optimalLineBreaking | boolean | true | استخدم كسر الأسطر الأمثل بخوارزمية Knuth-Plass بدلًا من الملء الجشع للسطر تلو الآخر (greedy first-fit). ينتج تباعدًا أكثر انتظامًا بين الكلمات عبر الفقرة. الفقرة الصينية أو اليابانية أو الكورية (انظر طباعة شرق آسيا)، أو الفقرة التي فيها كلمة أعرض من العمود، تظل تُنضَّد سطرًا سطرًا؛ أما الفقرة اللاتينية التي تقتبس بضع كلمات CJK فتحتفظ به. ويأخذه النص غير المضبوط أيضًا مع optimalRagged. انظر تقسيم الكلمات وضبط الأسطر. |
optimalRagged | boolean | true | اكسر النص الجاري غير المضبوط بخوارزمية Knuth-Plass أيضًا: نص المتن والاقتباسات الكتلية وبنود القوائم المحاذاة إلى اليسار أو اليمين أو الوسط، وأنماط الفقرات غير المضبوطة، ومتون الأطر، ومتون الأجزاء وأنماط الأقسام. تحتفظ المسافات بين الكلمات بعرضها. يزن الكاسر مقدار قصور كل سطر عن عرض السطر الكامل (السطر الذي ينقصه 3 em يكلّف ما يكلّفه سطر مضبوط عند maxWordSpacing)، فيسوّي الحافة بدلًا من ملء كل سطر قبل الذي يليه، وتعمل قواعد الكلمة المعزولة (avoidRunts وtightenRunts) وhyphenateAcrossColumns على النص غير المضبوط كما تعمل على المضبوط. ومع hyphenation.ragged تظل المنطقة هي التي تقرّر أيّ المقاطع يجوز أن ينتهي بها السطر (انظر النص غير المضبوط). أما العناوين وتعليقات الأشكال والملاحظات وخلايا الجداول وجدول المحتويات غير المضبوطة فتظل تُنضَّد سطرًا سطرًا. يتطلب optimalLineBreaking. القيمة false تنضّد النص غير المضبوط سطرًا سطرًا، كما حتى postext 1.4؛ والإعدادات المخزَّنة قبل ذلك التي تجعل بعض النص الجاري غير مضبوط تُقرأ بها (انظر الحزم المكتوبة بـ postext 1.4 أو ما قبله). |
breakAfterDashes | boolean | true | اسمح للسطر بأن ينتهي بعد شرطة طويلة (em) أو متوسطة (en) منضَّدة ملتصقة بين كلمتين: say—that’s، riddles.—I، Hamburg–Berlin، وكذلك حين تُنضَّد الكلمة التالية للشرطة بنمط آخر (see—and). تأخذه Knuth-Plass كما تأخذ المسافة بين الكلمات، وينتهي السطر بالشرطة دون إضافة شيء. ولا يكون أبدًا بعد شرطة تفتح جملة اعتراضية أو سطر حوار (—dijo، said "—Hola، sagte »—Ich: مسافة، أو مسافة وعلامة تنصيص، قبل الشرطة؛ أما بعد علامة تنصيص تُغلق كلمة، كما في "no"—and أو الألمانية „nein“—und أو الفرنسية « non »—et، فيجوز أن ينتهي السطر)، ولا قبل علامة ترقيم (él—,)، ولا قبل علامة تنصيص أو قوس (thinking—" and، says—“no”، says—(no): علامة التنصيص بعد الشرطة كثيرًا ما تُغلق الكلام الذي قطعته الشرطة)، ولا داخل سلسلة من الشرطات، ولا داخل مدى أرقام منضَّد بشرطة متوسطة (1914–1918). القيمة false تُبقي نقاط الكسر كما في 1.4: لا تكسر Knuth-Plass أبدًا بعد شرطة، ولا يكسر الكاسرُ الذي ينضّد سطرًا سطرًا النصَّ المنسَّق أو غير المضبوط المقسَّم بالواصلة إلا بين حرفين؛ والإعدادات المخزَّنة قبل ذلك التي يحوي نصها مثل هذه الشرطة تُقرأ بها (انظر الحزم المكتوبة بـ postext 1.4 أو ما قبله). ينطبق على النص الجاري والعناوين والقوائم والاقتباسات الكتلية والأطر. أما تعليقات الأشكال والملاحظات وخلايا الجداول وجدول المحتويات فتحتفظ بنقاط كسر 1.4، والفقرة غير المضبوطة البسيطة المنضَّدة سطرًا سطرًا تتبع قواعد pretext الخاصة في الحالتين. |
breakAfterHyphens | boolean | true | اسمح للسطر بأن ينتهي بعد واصلة الكلمة المركّبة، أي الواصلة الواقعة بين حرفين (well- · known، vencer- · se)، في كل فقرة تكسرها Knuth-Plass. ينتهي السطر بالواصلة ولا يُضاف شيء؛ ويُسعَّر الكسر كما يُسعَّر المقطع. ولا يكون أبدًا بعد واصلة مجاورة لرقم أو رمز (COVID-19، -5 °C). الفقرة المضبوطة الخالية من التنسيق المضمَّن لا تنكسر هناك إلا إذا كان على كل جانب من الواصلة حرفان، فلا ينتهي أي سطر بـ e- من e-mail. القيمة false تُبقي نقاط كسر 1.4: الفقرة المضبوطة الخالية من التنسيق المضمَّن لا تنكسر هناك أبدًا، في حين تنكسر هناك الفقرة نفسها إذا احتوت كلمة مائلة واحدة في أي موضع، والفقرة غير المضبوطة، والفقرة المنضَّدة سطرًا سطرًا؛ والإعدادات المخزَّنة قبل ذلك التي يحوي نصها كلمة مركّبة تُقرأ بها (انظر الحزم المكتوبة بـ postext 1.4 أو ما قبله). ينطبق على النص الجاري والعناوين والقوائم والاقتباسات الكتلية والأطر. انظر الكلمات المركّبة. |
repeatHyphen | boolean | false | ابدأ السطر التالي لكسرٍ عند واصلة كلمة مركّبة بواصلة أيضًا: vencer- · -se، كما يطلب الإملاء البرتغالي، وléxico- · -semántico، كما تطلب قواعد الأكاديمية الملكية الإسبانية منذ 2010. تُقاس الواصلة المكرَّرة وتُرسم مع سطرها، الذي يسجّلها في repeatedHyphen؛ ويشير plainStart وsourceStart إلى ما بعدها، فتقرأ الروابط والترويسات و Sandbox الكلمة كما كُتبت. ويرسمها ملف PDF تحت /ActualText يستبعدها، فيقرأ النص المنسوخ أو المستخرَج من PDF الكلمة مرة واحدة. ولا يأخذها عنوان الويب أبدًا. ينطبق على النص الجاري والعناوين والقوائم والاقتباسات الكتلية والأطر؛ والفقرة الخالية من التنسيق التي تحوي كلمة مركّبة يكسرها حينئذ الكاسر الذي ينضّد النص المنسَّق. |
blockquote | BlockquoteConfig | انظر أدناه | كيف تُنضَّد الاقتباسات الكتلية في Markdown (> …): اللون والمائل والمسافات البادئة. انظر الاقتباسات الكتلية. |
#الاقتباسات الكتلية
يأخذ الاقتباس الكتلي (الأسطر التي تبدأ بـ >) عائلة خط المتن وحجمه وتباعد أسطره وأوزانه ومحاذاته وتقسيمه بالواصلة. ويضبط bodyText.blockquote الباقي؛ وإن لم يُضبط، يبدو الاقتباس الكتلي كما كان حتى postext 1.4: رماديًا مائلًا، بالمسافة البادئة للسطر الأول في المتن وبلا إزاحة جانبية.
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
color | ColorValue | #666666 | لون النص. اللون المرتبط بمدخل في لوحة الألوان (paletteId) يتبع ذلك المدخل، كما في كل موضع؛ ولا تنطبق ألوان العريض والمائل والإحالات الخاصة بالمتن داخل الاقتباس الكتلي. |
italic | boolean | true | نضّد النص بالمائل. المقطع … داخله يعود قائمًا؛ ومع false يكون مائلًا كما في الفقرة. |
indent | Dimension | 0 | إزاحة كل سطر عن الحافة اليسرى للعمود أو الإطار. يضيق عرض السطر بمقدارها، فتنتهي الأسطر المضبوطة عند الحافة اليمنى. وحدة em هي حجم خط المتن. |
firstLineIndent | Dimension | قيمة المتن | المسافة البادئة للسطر الأول من كل فقرة مقتبسة، محسوبة من indent (ومع hangingIndent الخاصة بالمتن، المسافة البادئة لكل سطر عدا الأول). إن لم تُضبط: bodyText.firstLineIndent. |
bodyText: {
firstLineIndent: { value: 1.5, unit: 'em' },
// Upright verse in the body colour, set in by 2 em, no first-line indent.
blockquote: { color: { hex: '#241f26', model: 'hex' }, italic: false, indent: { value: 2, unit: 'em' }, firstLineIndent: { value: 0, unit: 'em' } },
}في Sandbox، هذه هي مجموعة الاقتباسات الكتلية في قسم نص المتن.
#عائلة واحدة لكل fontFamily
fontFamily — هنا وفي كل حقل آخر لعائلة الخط (headings.fontFamily، وtableStyle.bodyFontFamily، وseparatorFontFamily، وfontFamily لنمط شارة أو لعنصر تصميم…) — يسمّي عائلة واحدة. فمخرجات Canvas وHTML وPDF يجب أن تنضّد الوجه نفسه، وPDF يضمّن خطًا واحدًا لكل عائلة بلا سلسلة بدائل، فلا يوجد ما ترجع إليه سلسلة خطوط CSS. السلسلة تُنضَّد بعائلتها الأولى ويُبلَّغ عنها تحذيرَ إعدادات:
bodyText: { fontFamily: "'EB Garamond', Georgia, serif" } // set in EB Garamondحمّل تلك العائلة قبل الإخراج (انظر الخطوط المخصّصة ومزوّد الخطوط في إنتاج ملفات PDF)؛ وإن كانت غائبة، يقيس المتصفح بخطه الافتراضي، أيًّا كان ما تقوله بقية السلسلة. الفاصلة داخل علامات التنصيص جزء من الاسم ('"Foo, Bar"' عائلة واحدة).
#تقسيم الكلمات بالواصلة
حين تُضبط محاذاة النص على 'justify'، يمنع التقسيم بالواصلة الإفراط في تباعد الكلمات بكسر الكلمات الطويلة عند حدود المقاطع. يستخدم المحرّك أنماط TeX/Liang لإيجاد نقاط الكسر الطبيعية عند حدود المقاطع. انظر تقسيم الكلمات وضبط الأسطر لشرح مفصّل. ولا يُقسَّم النص غير المضبوط إلا إذا طلبت ذلك: انظر النص غير المضبوط أدناه.
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
enabled | boolean | true | ما إذا كان التقسيم بالواصلة مسموحًا. |
locale | LocaleTag | locale في المستوى الأعلى، وإلا 'en-us' | قواعد اللغة لحدود المقاطع: إحدى اللغات المدعومة أدناه، أو أي وسم BCP 47 ('es-ES'، 'pt-BR'). |
ragged | boolean | false | قسّم بالواصلة النص غير المضبوط أيضًا (المحاذى إلى اليسار أو اليمين أو الوسط)، ضمن zone. انظر النص غير المضبوط. |
zone | Dimension | 3em | منطقة التقسيم بالواصلة في النص غير المضبوط: لا تُقسَّم الكلمة التي لا يتّسع لها السطر إلا حين يترك إرسالها كاملة إلى السطر التالي فراغًا أعرض من هذا. وحدة em نسبية إلى حجم خط النص نفسه. تُتجاهَل في النص المضبوط. |
compounds | boolean | true | اسمح للقاموس بتقسيم أجزاء الكلمة المركّبة، وهي كلمة فيها واصلة بين حرفين (af-ter-dinner). القيمة false تُبقي هذه الكلمة كاملة إلا عند واصلتها الخاصة، حيث يظل جائزًا أن ينتهي السطر (after- · dinner)، كما يفعل TeX. الواصلة الاختيارية (soft hyphen) المكتوبة في الكلمة تظل تكسرها، والكلمة المركّبة الأعرض من السطر كله تظل تُقسَّم. ينطبق على النص الجاري والعناوين والقوائم والاقتباسات الكتلية والأطر؛ أما تعليقات الأشكال والملاحظات وخلايا الجداول وجدول المحتويات فتظل تقسّم الكلمات المركّبة. انظر الكلمات المركّبة. |
اللغات المدعومة: 'en-us' (الإنجليزية)، 'es' (الإسبانية)، 'fr' (الفرنسية)، 'de' (الألمانية)، 'it' (الإيطالية)، 'pt' (البرتغالية)، 'ca' (الكتالونية)، 'nl' (الهولندية).
تُتجاهَل الوسوم الفرعية للمنطقة ونظام الكتابة والمتغيّر عند اختيار الأنماط، وكذلك حالة الأحرف والفاصل _: فـ 'es-ES' و'es-MX' و'es_419' تُقسَّم بأنماط 'es'، و'pt-BR' بأنماط 'pt'، وكل وسم إنجليزي ('en'، 'en-GB') بأنماط 'en-us' — وهي الأنماط الإنجليزية الوحيدة المضمَّنة، فينال النص البريطاني نقاط كسر أمريكية. واللغة التي لا أنماط مضمَّنة لها ('sv'، 'pl'، 'fi'…) تُقسَّم بأنماط 'en-us'، وهذا يعطي نقاط كسر خاطئة لا انعدامها؛ ويبلّغ المحرّك عن ذلك مرة واحدة لكل وسم عبر console.warn، ويُدرجه Sandbox في لوحة «الفحوص». اضبط enabled: false لمثل هذا المستند؛ فذلك يُسكت التحذير أيضًا. أما الصينية واليابانية والكورية (zh، ja، ko، أيًّا كانت المنطقة أو نظام الكتابة) فلا تحتاج إلى أنماط ولا تطبع تحذيرًا: المستند المكتوب بإحداها يُنضَّد بلا تقسيم بالواصلة. ولتقسيم الكلمات اللاتينية المقتبسة فيه، اضبط enabled: true وسمِّ لغتها في locale ('en-us' للإنجليزية). أما enabled: true بلا locale، أو مع لغة صينية أو يابانية أو كورية، فيترك التقسيم معطّلًا ويقول ذلك مرة واحدة في وحدة التحكم (console). تعيد matchHyphenationLocale(tag) اللغة المضمَّنة التي يُطابقها الوسم (undefined حين لا توجد)، وتُدرج HYPHENATION_LOCALES اللغات المضمَّنة. وتسمّي الإعدادات المحلولة (doc.config.bodyText.hyphenation) الأنماط المستخدمة فعلًا في locale، وتحتفظ بالوسم الذي أعطيته في tag حين يختلف. ويعلن مُخرِج PDF لغة المستند من locale في المستوى الأعلى، ومن هذا الوسم فقط حين لا يكون locale مضبوطًا (انظر لغة المستند).
import { matchHyphenationLocale } from 'postext';
matchHyphenationLocale('es-MX'); // 'es'
matchHyphenationLocale('en-GB'); // 'en-us'
matchHyphenationLocale('sv'); // undefined: hyphenated with 'en-us', with a console warningلا تُقسَّم أبدًا الكلمات الأقصر من 5 أحرف. ويشترط المحرّك حرفين على الأقل قبل نقطة الكسر و3 أحرف بعدها.
النص غير المضبوط
افتراضيًا لا يُقسَّم بالواصلة إلا النص المضبوط: مع textAlign: 'left'، وفي أنماط الفقرات المحاذاة إلى اليسار أو الوسط أو اليمين، تبقى كل كلمة كاملة مهما ازدادت الحافة تعرّجًا، إلا عند واصلة اختيارية (U+00AD) مكتوبة في النص. اضبط hyphenation.ragged: true لتقسيم النص غير المضبوط أيضًا.
لا يُمدَّد السطر غير المضبوط أبدًا، فتقسيم كل كلمة لا يتّسع لها السطر سيملأ الحافة بالواصلات. وتحدّ من ذلك منطقة التقسيم بالواصلة، كما يفعل مُنضِّد السطر الواحد في برامج النشر المكتبي. حين لا تتّسع كلمة في آخر السطر، ينظر المحرّك إلى الفراغ الذي سيتركه إرسالها كاملة إلى السطر التالي. فإن كان ذلك الفراغ أعرض من zone، تُقسَّم الكلمة عند آخر مقطع يتّسع؛ وإلا نزلت كاملة. المنطقة المقيسة بـ em نسبية إلى حجم خط النص نفسه. القيمة الافتراضية 3em لا تقسّم إلا الكلمات التي ستترك سطرًا قصيرًا على نحو ملحوظ. والمنطقة الأعرض تعطي واصلات أقل وحافة أكثر تعرّجًا؛ والقيمة 0 تقسّم كل كلمة لا تتّسع. وفي التنضيد سطرًا سطرًا، لا ينتهي أكثر من سطرين متتاليين بمقطع. أما الواصلات الصلبة (enseñanza-aprendizaje) ومفاصل عناوين URL والواصلات الاختيارية المكتوبة في النص والكلمات الأعرض من السطر كله فتنكسر كما تفعل دائمًا: المنطقة وحدّ السطرين لا يحكمان إلا مقاطع القاموس.
الإعداد على مستوى المستند كله. ينطبق على نص المتن والاقتباسات الكتلية حين تكون غير مضبوطة، وعلى كل نمط فقرة غير مضبوط ومتن إطار يكون hyphenation الخاص به مفعّلًا (وقيمته الافتراضية هي hyphenation.enabled الخاصة بالمتن)، فالقيمة hyphenation: false تُبقي كلمات نمطٍ واحد كاملة. لا تُقسَّم العناوين وتعليقات الأشكال والملاحظات وخلايا الجداول وجدول المحتويات؛ وهناك، كما في كل موضع، لا تُقسَّم إلا الكلمة الأعرض من عرض السطر كله. أما نص التصميم — الترويسات وصفحات الافتتاح وصفحات الأجزاء — فيتبع علامة hyphenate الخاصة بكل عنصر نصي، التي تضبطها صفحات الافتتاح الكاملة وصفحات الأجزاء المضمَّنة، ويستخدم قاموس المستند أيضًا. ويتجاهل النص المضبوط ragged وzone.
const config: PostextConfig = {
locale: 'es',
bodyText: {
textAlign: 'left',
// A little more hyphenation than the 3 em default.
hyphenation: { ragged: true, zone: { value: 2, unit: 'em' } },
},
};مع optimalRagged (القيمة الافتراضية) يُكسر النص الجاري غير المضبوط بخوارزمية Knuth-Plass، وتحتفظ المنطقة بمعناها هناك: لا تُقسَّم الكلمة إلا حين لا يتّسع لها باقي السطر ويترك إنزالها كاملة أكثر من المنطقة فارغًا. ولا يُرفض مقطعان متتاليان، لكنهما يكلّفان ما تكلّفه واصلتان متتاليتان في النص المضبوط، فيندر الثالث. ومع optimalRagged: false، أو optimalLineBreaking: false، يُملأ كل سطر قبل الذي يليه، كما حتى postext 1.4.
يُخرج التقسيمُ بالواصلة للنص غير المضبوط الفقراتِ بكاسر الأسطر الذي تستخدمه دائمًا الفقرات ذات التنسيق المضمَّن (العريض والمائل والروابط والرياضيات)، فتفعيله قد يحرّك بضع نقاط كسر غير الواصلات. في السطر غير المضبوط، وإلى جانب المسافات ومقاطع القاموس، يكسر ذلك الكاسر بعد واصلة صلبة أو شرطة بين كلمتين (largas—separadas؛ ومع breakAfterDashes، بعد أي شرطة منضَّدة ملتصقة بين كلمتين، وriddles.—I وriddles—*and* أيضًا)، وعند مفاصل عناوين URL وبين الرموز الإيديوغرافية، ويُبقي معًا الكلمة المنضَّدة في عدة مقاطع (**Nota**:، (*véase*). ومن دون التقسيم بالواصلة للنص غير المضبوط، تكسر Knuth-Plass الفقرة غير المضبوطة الخالية من التنسيق حين يكون optimalRagged مفعّلًا (القيمة الافتراضية): عند المسافات بين الكلمات، وبعد واصلة صلبة بين حرفين (meta- · analyses)، كما يفعل الكاسر الآخر، ومع breakAfterDashes، بعد الشرطات الملتصقة. وفي التنضيد سطرًا سطرًا، تمرّ الفقرة عبر كاسر pretext، الذي يختلف في ثلاثة أمور: قد يكسر أيضًا قبل شرطة تُغلق جملة اعتراضية أو بعد شرطة تفتحها (él · — y)، وبعد الشرطة المائلة عند التقسيم بالواصلة (km/ · h)، ويقطع الكلمة الأعرض من السطر عند أي حرف بلا واصلة، حيث يقسّمها الكاسر الآخر عند مقطع أولًا.
في Sandbox، المفتاح هو قسّم النص غير المضبوط بالواصلة، تحت محاذاة الفقرة في قسم نص المتن، والمنطقة تحته. وتحت متنٍ مضبوط يقع المفتاح نفسه مع إعدادات ضبط الأسطر، لأنماط الفقرات والأطر غير المضبوطة.
#لغة المستند
locale في المستوى الأعلى هو لغة المستند ككل. يأخذ القيم نفسها التي يأخذها hyphenation.locale، وهو البديل حين لا يكون ذلك الحقل مضبوطًا، فلا يحتاج كتاب إسباني إلا إلى locale: 'es' ليُقسَّم بالواصلة بالإسبانية. ويختار أيضًا لغة عبارات الاستمرار المضمَّنة في الجداول والأطر المقسومة ((cont.) / Continued مقابل Continúa، انظر الجداول الأطول من الصفحة والعلامات على الإطار المقسوم) ولغة أرقام العناوين المكتوبة بالحروف (Chapter One مقابل Capítulo uno، انظر الأرقام المكتوبة بالحروف)، وهو اللغة التي يُوسَم بها ملف PDF الميسَّر الوصول. إن لم يُضبط، يفترض المحرّك 'en-us'؛ ويرجع Sandbox إلى لغة الواجهة ويضبط الحقل تحت التصميم › نظام الكتابة.
ويختار أنواع الموارد المضمَّنة أيضًا. حين لا يكون resourceTypes مضبوطًا، يرقّم buildDocument ويكتب التعليقات باستخدام defaultResourceTypes(locale)، فيعطي locale: 'de' وحده Abbildung 1.1 وTabelle 1.1. وحين لا يكون locale مضبوطًا، تحلّ محلّه لغة التقسيم بالواصلة، لأنواع الموارد ولعبارات الجداول على السواء. وقائمة resourceTypes الصريحة تغلب دائمًا. كانت الإصدارات السابقة تستخدم الأنواع الإنجليزية أيًّا كان locale ما لم تمرّر defaultResourceTypes(locale) بنفسك؛ والمستند الذي يضبط locale ويريد الاحتفاظ بالتسميات الإنجليزية يمرّر resourceTypes: defaultResourceTypes('en'). كتب Sandbox والحزم تحمل قائمتها الخاصة، فلا تتأثر.
أي وسم BCP 47 يصلح هنا كما في hyphenation.locale: فـ 'de-AT' ينال العبارات الألمانية. العبارات المضمَّنة موجودة باللغات الثماني التي يدعمها التقسيم بالواصلة وبالصينية، بالحروف المبسّطة والتقليدية؛ وأي لغة أخرى تنال العبارات الإنجليزية.
تقبل الصينية zh وzh-Hans وzh-Hant وzh-CN وzh-SG وzh-TW وzh-HK وzh-MO والصيغ الطويلة (zh-Hant-TW)، بأي حالة أحرف ومع - أو _. تتبع العبارات نظامَ الكتابة، مقروءًا بـ Intl.Locale(tag).maximize(): zh وzh-CN وzh-SG مبسّطة، وzh-TW وzh-HK وzh-MO تقليدية. أما الإعدادات الطباعية الافتراضية التي تعتمد على اللغة فتتبع المنطقة بدلًا من ذلك، كما يوصي clreq §1.2: تُعدّ CN و SG و MY البرَّ الرئيسي، و TW تايوان، و HK و MO هونغ كونغ، والوسم الذي لا منطقة فيه يُؤخذ بنظام كتابته (zh-Hant تايوان، وzh وzh-Hans البر الرئيسي). تعطي localeScript(tag) وcjkRegionOf(tag) وstringsKeyOf(tag) وsameContentLocale(a, b) هذه القراءات، وتُدرج DOCUMENT_LANGUAGES اللغات ذات العبارات المضمَّنة، كلٌّ منها باسمها في لغتها، كما تعرضها قائمة «لغة المستند» في Sandbox.
| اللغة | الشكل: الاسم، الجمع، التسمية المختصرة | الجدول: الاسم، الجمع، التسمية المختصرة | استمرار الجدول: continuedSuffix، continuesMarker |
|---|---|---|---|
الإنجليزية (en) | Figure, Figures, Fig. | Table, Tables, Tab. | (cont.), Continued |
الإسبانية (es) | Figura, Figuras, Fig. | Tabla, Tablas, Tabla | (cont.), Continúa |
الفرنسية (fr) | Figure, Figures, Fig. | Tableau, Tableaux, Tabl. | (suite), À suivre |
الألمانية (de) | Abbildung, Abbildungen, Abb. | Tabelle, Tabellen, Tab. | (Forts.), Wird fortgesetzt |
الإيطالية (it) | Figura, Figure, Fig. | Tabella, Tabelle, Tab. | (segue), Continua |
البرتغالية (pt) | Figura, Figuras, Fig. | Tabela, Tabelas, Tab. | (cont.), Continua |
الكتالونية (ca) | Figura, Figures, Fig. | Taula, Taules, Taula | (cont.), Continua |
الهولندية (nl) | Figuur, Figuren, Fig. | Tabel, Tabellen, Tab. | (vervolg), Wordt vervolgd |
الصينية المبسّطة (zh-Hans، zh، zh-CN) | 图, 图, 图 | 表, 表, 表 | (续), 接下页 |
الصينية التقليدية (zh-Hant، zh-TW، zh-HK) | 圖, 圖, 圖 | 表, 表, 表 | (續), 接下頁 |
العربية (ar، ar-EG، ar-MA…) | شكل, أشكال, شكل | جدول, جداول, جدول | (تابع), يتبع |
بادئة التعليق هي اسم النوع (Figure 1.1.). ترقّم الأنواع الصينية داخل الفصل بواصلة، {h1}-{n} (图 1-1)؛ ومع إعدادَي التعليق labelNumberGap: '' وlabelSeparator: ' ' يصبح التعليق 图1-1 标题 (انظر نمط التعليق). ويتبع الفهرس الأبجدي في آخر الكتاب اللغةَ أيضًا: 见 و另见 قبل الإحالة، و符号 و数字 فوق الرموز والأرقام في المبسّطة؛ و見 و另見 و符號 و數字 في التقليدية. وترقّم العربية أنواعها بالطريقة نفسها، {h1}-{n} (شكل 2-3)، وتكتب الإحالات على هيئة «الفصل 3» و«القسم 2-1» و«ص 12»، وتعنون قائمة المراجع بـ «المراجع»، ويطبع فهرسها «انظر» / «انظر أيضًا» و«رموز» و«أرقام»، بالفاصلة والفاصلة المنقوطة العربيتين (، ؛).
يعلن المستند الصيني أو الياباني أو الكوري لغته في مخرجات HTML (lang على الجذر .pt-doc، مع الإبقاء على zh-Hant-TW كاملًا) وعلى اللوحة التي يرسمها (ctx.lang، في Chrome 136 وما بعده)، فيرسم المتصفح أشكال الحروف الخاصة بالمنطقة: يوحّد Unicode محارف الهان، ونقطة الترميز الواحدة تبدو مختلفة في خط تايواني وفي خط ياباني. ولا تحمل المستندات الأخرى lang، كما كان من قبل. ويعلن ملف PDF القيمة /Lang من locale لكل مستند، موسومًا كان أو غير موسوم، مع نظام كتابته ومنطقته. والمستند المكتوب بلغة تُكتب من اليمين إلى اليسار (العربية والفارسية والأردية والعبرية…) يعلن لغته أيضًا: أشكال الحروف الخاصة باللغة في الخط (locl) والخط البديل يتبعانها.
const config: PostextConfig = {
locale: 'es',
bodyText: { textAlign: 'justify', hyphenation: { enabled: true } }, // hyphenates in Spanish
};أرقام المستند
يضبط numerals في المستوى الأعلى أرقامَ كل عدد يكتبه المحرّك: أرقام الصفحات وتسميات الصفحات في جدول المحتويات والفهرس الأبجدي والإحالات إلى الصفحات؛ وأرقام القوائم المرقّمة والحواشي السفلية؛ وعدّادات العناوين والفصول، و{chapterNumber}؛ و{h1} و{n} في رقم الشكل وفي الإحالة إليه؛ و{totalPages} و{bookTotalPages} و{numberDecimal}. القيمة 'latn' تكتب 0–9، و'arab' الأرقام الهندية العربية ٠–٩، و'arabext' الأرقام الفارسية ۰–۹. لا يتغيّر إلا التنسيق العشري — decimal، أو arabic في القوائم، أو إعداد متروك على قيمته الافتراضية — فالتنسيق الذي يسمّيه المؤلف يُطبع كما سُمّي: lower-roman يبقى i, ii, iii، وarabic-indic في مستند لاتيني يظل يكتب ١, ٢, ٣. ولا يُعاد أبدًا كتابة نص المستند.
القيمة الافتراضية، 'auto'، تأخذ أرقام locale (defaultNumeralsFor(tag)): 'arab' للعربية بلا منطقة أو مع أي منطقة خارج المغرب العربي (ar، ar-EG، ar-SA، ar-AE…)، و'latn' لـ ar-MA وar-DZ وar-TN وar-LY وar-MR وar-EH، و'arabext' للفارسية (fa) والبشتوية (ps) وأردية الهند (ur-IN)، و'latn' لكل ما سوى ذلك، ومنه أردية باكستان. تعطي CLDR القيمة latn للوسم ar المجرّد وللوسم ar-AE؛ لكن الكتب العربية في المشرق والخليج تطبع ٠–٩، وهذا ما يتبعه Postext. والوسم الذي يسمّي أرقامه يحتفظ بها: ar-MA-u-nu-arab. والصفحة المرقّمة بأرقام المستند تسجّل arabic-indic أو persian في pageNumberFormat الخاص بها، فتعرض تسميات صفحات PDF الأرقام نفسها. والقيمة غير المعروفة تتبع اللغة ويُبلَّغ عنها بـ unknownNumerals.
الأعداد التي يكتبها المؤلف تُقرأ بأي من الأنظمة الثلاثة: بند القائمة ٣. يبدأ من 3، و{startAt=٥} و:::numbering{startAt=٥} و:::space{lines=٢} و:::part{number="٣"} (من أجل {numberDecimal}) تقرأ القيمة.
const config: PostextConfig = { locale: 'ar' }; // ١، ٢، ٣
const maghreb: PostextConfig = { locale: 'ar-MA' }; // 1, 2, 3
const forced: PostextConfig = { locale: 'ar', numerals: 'latn' }; // 1, 2, 3اتجاه النص
يضبط direction في المستوى الأعلى الاتجاه الأساسي للمستند: 'ltr' أو 'rtl' أو 'auto' (القيمة الافتراضية)، وهي 'rtl' حين يكون نظام الكتابة في locale مكتوبًا من اليمين إلى اليسار (العربية والفارسية والأردية والعبرية والسريانية والثانا وإنكو وأدلام…، ويقرؤه directionOf(tag))، و'ltr' في غير ذلك. يُخرَج المستند المكتوب من اليمين إلى اليسار في إطار معكوس: تبدأ أسطره من اليمين، وعموده الأول هو الأيمن، وتقف مسافاته البادئة وعلامات قوائمه وعناصره العائمة وحواشيه السفلية وأطره على اليمين، وpage.binding: 'auto' يجلّده من اليمين. ولا تحمل الإعدادات المحلولة direction: 'rtl' إلا لمثل هذا المستند، فيُحَلّ المستند المكتوب من اليسار إلى اليمين كما كان من قبل. والقيمة غير المعروفة تُقرأ 'auto' ويُبلَّغ عنها بـ unknownConfigValue. وترتّب خوارزمية Unicode ثنائية الاتجاه (UAX #9) مقاطع كل سطر في أي من الاتجاهين: فالاقتباس العربي في كتاب إنجليزي يُقرأ من اليمين إلى اليسار في موضعه.
داخل المستند، يأخذ العنوان أو الحاوية ::: القيمة {dir=ltr} أو {dir=rtl}، ويعزل :ltr[…] و:rtl[…] المضمَّنان مقطعًا من النص (انظر اتجاه النص في الترميز)؛ ويأخذ مورد الجدول table.direction. والكتلة المضبوطة بعكس اتجاه المستند تحتفظ بجانب بدايتها الخاص: تنتقل مسافتها البادئة وعلامات قوائمها والطرف المحاذى لسطرها الأخير إلى الجانب الذي يبدأ منه نصها.
الإعداد الذي يسمّي جانبًا يعني جانبًا من النص أو من تدفّق المتن، لا من الورقة أبدًا، فالتصميم المُعدّ لكتاب إنجليزي يعمل حين يُحوَّل الكتاب إلى العربية. وتُقبل 'start' و'end' اسمين صريحين:
| الإعداد | 'left' / 'right' | 'start' / 'end' |
|---|---|---|
textAlign للمتن والعناوين وأنماط الفقرات والأجزاء والحواشي السفلية ومتون الأطر؛ وalign للتعليق ولملاحظة التعليق | جانبا النص: 'left' هو الجانب الذي يبدأ منه السطر، أي يمين الفقرة العربية، حيث يذهب السطر الأخير من الفقرة المضبوطة. | مرادفات لـ 'left' و'right'. تحمل الإعدادات المحلولة 'left' / 'right'؛ وتحتفظ الإعدادات المحفوظة بما كُتب. |
align لخلية الجدول | جانبا نص الخلية، مقروءين باتجاه الجدول (table.direction). | مرادفات، مقروءة باتجاه الجدول. |
placement.align (العناصر العائمة، الأشكال الضيقة) | جانبا تدفّق المتن: في كتاب مكتوب من اليمين إلى اليسار، 'left' هو يمين الورقة. | مرادفات. |
stripe.side وicon.cornerSide وlabelTab.position للإطار ('top-start'، 'top-end') | جانبا تدفّق المتن، وهما نفسهما لكل إطار في الصفحة. | اتجاه الإطار الخاص (:::callout{dir=ltr} في كتاب عربي يبدأ من يسار الورقة). |
| خانات رأس الصفحة وتذييلها؛ وعناصر التصميم المثبّتة إلى الورقة | جانبا الورقة. | للعناصر النصية فقط: بداية direction الخاص بالعنصر ونهايته. |
يحتفظ placement.rotate بمعناه المادي في الصفحة المعكوسة: الشكل المُدار باتجاه عقارب الساعة يُدار باتجاه عقارب الساعة على الورقة. والمضيف الذي يقرأ الإخراج يجد الإطار المعكوس في كل صفحة (VDTPage.flow مع direction: 'rtl'، وpageIsMirrored(page)) والترتيب البصري لمقاطع كل سطر في VDTLine.order؛ وتحوّل flowToPage وpageToFlow بين التدفّق والورقة. انظر الإخراج العربي.
const arabic: PostextConfig = { locale: 'ar' }; // right to left, bound on the right
const english: PostextConfig = { locale: 'en', direction: 'rtl' }; // forced; rarely what you want#اللغات وأنظمة الكتابة
ينضّد Postext أنظمة الكتابة الأبجدية المكتوبة من اليسار إلى اليمين، والصينية أفقيًا عبر الصفحة وعموديًا نزولًا فيها. يشرح الإخراج الصيني كيف تُنضَّد الصينية وأيّ الإعدادات تتحكم فيها؛ والمفاتيح تحت طباعة شرق آسيا والكتابة العمودية والتجليد. ما يناله كل نظام كتابة:
- الصينية يُنضّدها مُنضِّد CJK حين تحوي الفقرة محارف CJK أكثر من المسافات بين الكلمات: تنكسر أسطرها بين المحارف وفق قواعد بداية السطر ونهايته في
cjk.lineBreak(لا سطر يبدأ بـ 。、」 أو ー، ولا سطر ينتهي بـ 「 أو ()، وتُبقي —— و…… والعدد مع علاماته والكلمة اللاتينية كاملةً، والسطر المضبوط تُوزَّع مسافاته بين محارفه حتى يبلغ عرض السطر. وتتبع عروضُ علامات الترقيم، وعلامات الترقيم المعلّقة، والمسافة بين الهان واللاتينية، وشبكة المحارف، ونقاط التوكيد، وعلامات أسماء الأعلام وعناوين الكتب، وحواشي الروبي (ruby) والواريتشو (warichu) منطقةَlocale، أفقيًا أو عموديًا (layout.writingMode: 'vertical-rl'). والفقرة اللاتينية التي تقتبس بضع كلمات CJK تحتفظ بكسر الأسطر الأمثل وقد تنكسر بجوارها؛ أما قوس CJK أو النقطة الوسطى أو العلامة كاملة العرض المقتبسة في نص لاتيني (〈h〉، %) فلا تغيّر شيئًا. - اليابانية والكورية تمرّان عبر المُنضِّد نفسه وتُنضَّدان بلا تقسيم بالواصلة، لكن بالإعدادات الافتراضية لصينية البر الرئيسي: قواعدهما الخاصة (JLREQ و KLREQ) غير مطبّقة. والنص الكوري ينكسر بين المقاطع كما ينكسر عند مسافاته.
- العربية وأنظمة الكتابة الأخرى من اليمين إلى اليسار (الفارسية والأردية والعبرية…) تُنضَّد من اليمين إلى اليسار: يشرح الإخراج العربي الكيفية. يأتي
directionالخاص بالمستند من نظام الكتابة فيlocale، وترتّب خوارزمية Unicode ثنائية الاتجاه الكلماتِ اللاتينية والأعداد داخل كل سطر، ويُجلَّد الكتاب من اليمين وعموده الأول هو الأيمن، وكل عدد يكتبه المحرّك يأخذ أرقام المنطقة. والكلمة التي تحوي حرفًا من الحرف العربي لا تُقسَّم بالواصلة ولا تُباعَد حروفها ولا تُقطع أبدًا، والسطر العربي المضبوط يُمدَّد عند مسافاته وبالكشيدات. علامات التشكيل والتوكيد والحواشي السفلية وعبارات العربية مشروحة تحت النص العربي. وتنال الفارسية والأردية والعبرية الاتجاه والأرقام وقواعد الكلمة الكاملة، لكن بلا عبارات مضمَّنة خاصة بها.
تُشحن أنماط التقسيم بالواصلة لثماني لغات (en-us، es، fr، de، it، pt، ca، nl)؛ وأنواع الموارد وعبارات الاستمرار المضمَّنة موجودة بهذه الثماني وبالصينية والعربية. تُنضَّد الصينية واليابانية والكورية واللغات المكتوبة من اليمين إلى اليسار بلا تقسيم بالواصلة. وأي لغة أخرى تُقسَّم بأنماط الإنجليزية الأمريكية، مع تحذير في وحدة التحكم، وتنال العبارات الإنجليزية. لمثل هذا المستند، اضبط hyphenation.enabled: false ومرّر resourceTypes وعبارات الاستمرار في tableStyle بلغته.
#النص العربي
تخدم هذه الإعدادات النص المكتوب بالحرف العربي؛ ولا يغيّر أيٌّ منها مستندًا مكتوبًا بنظام كتابة آخر. يشرحها الإخراج العربي مع سائر ما في الكتاب العربي: الاتجاه والتجليد والأرقام والشعر وجدول المحتويات والفهرس.
- علامات التشكيل وتباعد الأسطر. علامات التشكيل في النص المشكول (الفتحة والكسرة والشدة والتنوين والألف الخنجرية والعلامات القرآنية) تتراكب فوق الحروف وتحتها، داخل تباعد الأسطر، الذي لا يزداد من أجلها أبدًا. كل سطر يحوي علامات يسجّل مدى وصول حبرها (
VDTLine.markInk)، ويشمل قصّ العمود في المُخرِجات علاماتِ السطرين الأول والأخير من العمود. وحين تلتقي علامة فوق كلمة بالحروف أو العلامات المتدلّية تحت الكلمة التي فوقها، يبلّغ البناء عن الفقرة (arabicMarksExceedLeading، في لوحة «الفحوص» في Sandbox). ولا تُقارَن إلا الكلمات الواقعة بعضها فوق بعض. يحتاج النص المشكول جزئيًا إلى نحو 1.7–1.85 em منlineHeight، والشعر المشكول كاملًا إلى 1.9–2.1 em. - التوكيد. لا مائل في الحرف العربي، ففي المستند الذي تُكتب لغته
localeبالحرف العربي يُنضَّد*…*بالعريض افتراضيًا (bodyText.emphasis: 'auto'). القيمة'color'تنضّده قائمًا بلونitalicColor، و'overline'ترسم خطًا فوق الكلمات، وهو الخط الفوقي في الكتب العربية. يصل الإعداد إلى كل نص منضَّد بوجوه خط المتن: الفقرات والقوائم والاقتباسات الكتلية وأنماط الفقرات ومتون الأطر والملاحظات والعناوين. أما تعليقات الأشكال وخلايا الجداول وجدول المحتويات والفهرس فتحتفظ بإعدادات المائل الخاصة بها. ومهما كان الاختيار، لا يُميل المحرّك الحروف العربية أبدًا: الكلمات العربية في مقطع منضَّد بالمائل تقف قائمة، وكلماته اللاتينية تحتفظ بميلها. والاقتباس الكتلي قائم افتراضيًا في مثل هذا المستند. - التشكيل.
bodyText.tashkil: 'strip'يحذف علامات التشكيل والعلامات القرآنية من النص الذي ينضّده الإخراج، لإصدار غير مشكول يُصنع من مصدر مشكول: الفتحة والضمة والكسرة وتنوينها، والسكون، والشدة، والألف الخنجرية (هٰذا تصبح هذا) والعلامات U+0656–U+065F و U+06D6–U+06ED. والقيمة'strip-vowels'تُبقي الشدة، كما تطبعها معظم الكتب الحديثة. وتبقى الهمزة والمدّة (أ إ آ حروف، حتى حين تُكتب بعلامات مركّبة). يحتفظ المصدر بعلاماته؛ وتُنضَّد الأسطر والعناوين وجدول المحتويات بدونها، ويظل كل محرف منضَّد مرتبطًا بموضعه في المصدر. - الحواشي السفلية.
footnotes.markerTemplate: '({n})'يكتب العلامات «(١)» بأرقام المستند، وnumbering: 'page'يعيد ترقيمها في كل صفحة، وnoteNumberPosition: 'inline'ينضّد رقم الحاشية نفسها على السطر. ويقف خط الفصل وأرقام الحواشي في بداية العمود، على اليمين في الكتاب المكتوب من اليمين إلى اليسار. - الكلمات الكاملة. الكلمة التي تحوي حرفًا من الحرف العربي لا تُقسَّم بالواصلة ولا تُقطع ولا تُباعَد حروفها أبدًا، في كتاب عربي أو مقتبسةً في كتاب آخر. والنمط الذي يضبط
letterSpacingعلى نص عربي يُبلَّغ عنه (joiningScriptLetterSpacing)، والكلمة الأعرض من سطرها تفيض عنه ويُبلَّغ عنها (unbreakableWordOverflow). انظر الإخراج العربي. - الكشيدة والشعر. يُمدَّد السطر العربي المضبوط بالكشيدات كما يُمدَّد عند مسافاته (
bodyText.kashida، انظر الكشيدة في النص العربي)، وتُنضَّد القصيدة العمودية بيتًا في كل سطر، في شطرين متساويين في العرض، مع:::verse(انظر:::verse). - الفهرس. الفهرس بالعربية يُرتَّب ترتيبًا ألفبائيًا ويتجاهل أداة التعريف «ال» (
index.ignoreArticle) وعلامات التشكيل وكراسي الهمزة؛ انظر الفهرس الأبجدي في آخر الكتاب.
#الأسطر اليتيمة والأرامل والكلمات المعزولة وقواعد الإبقاء معًا
انظر تقسيم الكلمات وضبط الأسطر لآلية نقاط الجزاء هذه. وهذا القسم هو المرجع لمفاتيح bodyText التي تتحكم فيها.
وإلى جانب حدود التقسيم بالواصلة والتباعد، تتيح إعدادات نص المتن القواعدَ المرنة التي تمنع الانكسارات المحرجة بنيويًا في الفقرات. وكلها تُغذّى في خوارزمية Knuth-Plass لكسر الأسطر على هيئة نقاط جزاء (demerits) — تميل بالإخراج نحو الانكسارات النظيفة دون أن تفرض قاعدة صارمة أبدًا. اضبط قيم *Penalty على 0 لتعطيل أيٍّ منها فعليًا.
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
avoidOrphans | boolean | true | اثنِ الفقرة عن أن تنتهي بأقل من orphanMinLines سطرًا في أعلى العمود التالي. |
orphanMinLines | number | 2 | أدنى عدد من الأسطر مطلوب في أعلى العمود التالي حين تنقسم الفقرة. لا يعمل إلا حين تكون avoidOrphans هي true. |
orphanPenalty | number | 1000 | نقاط الجزاء المضافة عند مخالفة قيد الأسطر اليتيمة. القيم الأعلى تميل بالخوارزمية أشدّ ضد الأسطر اليتيمة؛ و0 يعطّل الجزاء. |
avoidOrphansInLists | boolean | true | حين تكون true، تنال بنود القوائم أيضًا الحماية من الأسطر اليتيمة (لا الفقرات وحدها). لا يكون فعّالًا إلا حين تكون avoidOrphans هي true. |
avoidWidows | boolean | true | اثنِ الفقرة عن أن تبدأ بأقل من widowMinLines سطرًا في أسفل العمود الحالي. |
widowMinLines | number | 2 | أدنى عدد من الأسطر مطلوب في أسفل العمود الحالي حين تنقسم الفقرة. لا يعمل إلا حين تكون avoidWidows هي true. |
widowPenalty | number | 1000 | نقاط الجزاء المضافة عند مخالفة قيد الأسطر الأرامل. 0 يعطّل الجزاء. |
avoidWidowsInLists | boolean | true | حين تكون true، تنال بنود القوائم أيضًا الحماية من الأسطر الأرامل. لا يكون فعّالًا إلا حين تكون avoidWidows هي true. |
avoidRunts | boolean | true | اثنِ الفقرات عن الانتهاء بسطر أخير قصير جدًا — كلمة معزولة (runt)، كأن تقف كلمة قصيرة واحدة وحدها. والنص غير المضبوط أيضًا، مع optimalRagged. والفقرة الصينية أو اليابانية أو الكورية لا تنتهي بسطر يحوي محرفًا واحدًا، وحده أو مع علامات إغلاقه (孤字): يعطيه السطر الذي فوقه محرفه الأخير حين يظل ممكنًا ضبط ذلك السطر ضمن سقف التتبّع. |
runtMinCharacters | number | 20 | عتبة السطر الأخير من الفقرة، محسوبة بالمسافات بين الكلمات لا بالحروف: يكون السطر كلمة معزولة حين يكون أضيق من runtMinCharacters × normalSpaceWidth بكسل. المسافة بين الكلمات ربع إلى ثلث em في معظم خطوط النصوص، أي نحو نصف حرف صغير (lowercase)، فالقيمة الافتراضية 20 تلتقط الأسطر الأخيرة الأقل من 4 إلى 7 em — نحو 8 إلى 12 حرفًا. لالتقاط الأسطر الأخيرة الأقل من نحو N حرفًا، اضبطها قرب 2 × N. |
runtPenalty | number | 1000 | رداءة مكافئة تُحقن في صيغة نقاط الجزاء التربيعية لـ Knuth–Plass (بمقياس رداءة السطر (badness) نفسه، التي تتشبّع عند 10000). 0 يعطّل الجزاء. |
gradedRuntPenalty | boolean | false | اجعل جزاء الكلمة المعزولة متناسبًا مع مقدار قِصَر السطر الأخير: السطر الأخير الذي عرضه w تحت العتبة t يكلّف runtPenalty × (1 − w / t) بدلًا من الجزاء كله. فتكلّف النهاية المكوّنة من كلمتين أقل من النهاية المكوّنة من كلمة واحدة، ويُنزل الكاسر كلمة حين يستطيع سطر أعلى التخلّي عنها ('…sallies of' / 'our minds.' بدلًا من '…sallies of our' / 'minds.'). معطّل افتراضيًا: كل كلمة معزولة تكلّف القدر نفسه، ويُبقي الكاسر الأسطر الأضيق في الأعلى. |
avoidRuntsInLists | boolean | true | حين تكون true، تنال بنود القوائم أيضًا جزاء الكلمة المعزولة. لا يكون فعّالًا إلا حين تكون avoidRunts هي true. |
tightenRunts | boolean | true | حين يعجز الجزاء عن تجنّب كلمة معزولة، نضّد الفقرة بسطر أقل بدلًا من ذلك: تضيق المسافات بين الكلمات (دون تجاوز minWordSpacing أبدًا)، وإن لم يكفِ ذلك وحده لاستيعاب السطر، ينضمّ قليل من التتبّع السالب. ويُرفض التنضيد الأقصر، وتبقى الكلمة المعزولة، حين يمدّ سطرًا مضبوطًا إلى ما بعد maxWordSpacing أو، إذا كان في الفقرة أصلًا سطر مضبوط أكثر ارتخاءً، إلى ما بعد ذلك السطر، أو حين ينضّد أسطرًا غير مضبوطة (بعد 3× المسافة العادية) أكثر مما كان في الفقرة. والمسافات بين الكلمات في النص غير المضبوط تحتفظ بعرضها، فلا يشارك هناك إلا التتبّع. يتطلب optimalLineBreaking وavoidRunts (وoptimalRagged للنص غير المضبوط). |
maxRuntTracking | number | 10 | أقصى تتبّع يجوز أن يأخذه إصلاح الكلمة المعزولة، بأجزاء الألف من em (وحدة InDesign: 10 = 0.01 em لكل حرف)، ويُطبَّق تضييقًا. 0 يترك الإصلاح لتباعد الكلمات وحده. |
slackWeight | number | 10 | الوزن المطبّق على الكلفة التربيعية لـ«مساحة العمود غير المستخدمة». القيم الأعلى تجعل الإخراج يفضّل ملء الأعمدة بإحكام؛ و0 يعطّل ضغط الفراغ تمامًا. |
keepColonWithList | boolean | true | حين تنتهي فقرة بنقطتين تقدّمان قائمة مباشرة، أبقِ السطر الأخير الحامل للنقطتين متصلًا بالقائمة: إن كان وضع الفقرة لن يترك متّسعًا لبدء أول بند من القائمة في العمود أو الصفحة نفسها، يُنقل السطر الأخير (أو الفقرة كلها، إن كانت سطرًا واحدًا) إلى العمود التالي مع القائمة. ومقدار المتّسع الكافي يحدّده colonListRoom. وحين تدفع هذه القاعدة الفقرة كلها وتسبقها مباشرة في العمود سلسلة من العناوين، تُسحب تلك العناوين معها أيضًا ليظل headings.keepWithNext نافذًا. |
colonListRoom | 'item' | 'line' | 'item' | المتّسع الذي يطلبه keepColonWithList تحت سطر النقطتين. 'item': ما تتركه قواعد الأسطر اليتيمة والأرامل للقوائم من البند الأول في أسفل العمود، سطرًا حين يجوز أن ينقسم هناك، وكلّه حين تُبقيه كاملًا (بند من سطرين، مثلًا). 'line': سطر واحد، كما حتى postext 1.4؛ فالبند الأول الذي تُبقيه تلك القواعد كاملًا ينتقل حينئذ إلى العمود التالي وحده ويترك سطر النقطتين في أسفل العمود. والإعدادات المخزَّنة قبل configVersion 6، في كتاب يقدّم قائمة بنقطتين، تُقرأ بـ 'line' (انظر الحزم المكتوبة بـ postext 1.4 أو ما قبله). وأي قيمة أخرى تُقرأ 'item'. |
hyphenateAcrossColumns | boolean | true | اسمح للعمود أو الصفحة بأن ينتهي بكلمة مقسومة بالواصلة (Hyphenate Across Column في InDesign). القيمة false تعيد كسر الفقرة التي تعبر فاصل العمود، لينتهي سطرها الأخير في العمود بكلمة كاملة؛ وتمتصّ المسافاتُ بين كلمات الأسطر التي فوقه الفرقَ، ضمن maxWordSpacing وminWordSpacing. وهو تفضيل: حيث لا يتجنّبه أي كسر ضمن تلك الحدود، تبقى الواصلة. يجرّب كل فاصل عمود في الفقرة: الأول، واللاحقة التي تقع حيث ينتهي عمود ممتلئ، في إعادة كسر واحدة؛ والفاصل اللاحق الذي يقع في موضع آخر (سطر أرملة أُبقي، أو شريط قُطع عند مستوى واحد) في إعادة كسر أخرى، من ذلك العمود، تُبقي الأسطر المنضَّدة في الأعمدة السابقة عند نقاط كسرها ولا تكسر إلا الباقي. لا تتأثر متون الأطر. ومع optimalRagged، يُعاد كسر الفقرة غير المضبوطة بالطريقة نفسها: تحتفظ المسافات بين كلماتها بعرضها، فلا تتحرك إلا نهايات أسطرها. وكل إعادة كسر تقيس الفقرة مرة أخرى، فالكتاب الذي فيه كثير من الواصلات في نهايات الأعمدة يُخرَج أبطأ قليلًا. يتطلب optimalLineBreaking، وoptimalRagged للنص غير المضبوط. |
paragraphContainerSpacing | 'collapse' | 'add' | 'collapse' | المسافة تحت حاوية :::paragraphs التي تُغلق على فقرة، بين تلك الفقرة والكتلة التي تليها (انظر الحاوية :::paragraphs). 'collapse': الأكبر من spaceBetween وmarginBottom الخاصين بالنمط ومن تباعد الفقرات في النص المحيط بالحاوية (سطر مع paragraphSpacing؛ وفي الإطار، تباعد الإطار)، مدمجًا مع المسافة التي تُبقيها الكتلة التالية فوقها، كما بين فقرتين من النص الجاري: فالعنوان الواقع تحت قائمة مراجع يقع على بُعد marginTop الخاص به تحتها، لا ذلك الهامش مضافًا إليه تباعد المدخلات. 'add': كما حتى postext 1.4، تُوضع مسافة النمط وحدها تحت السطر الأخير قبل المحاذاة إلى الشبكة، وتُضاف تحتها المسافة التي تُبقيها الكتلة التالية فوقها، ويُستبعد تباعد الفقرات، فقد تقع الفقرة التالية للحاوية أقرب إليها من أي فقرة أخرى. والإعدادات المخزَّنة قبل configVersion 8 التي تعرّف نمط فقرة، في كتاب يحوي مثل هذه الحاوية، تُقرأ بـ 'add' (انظر الحزم المكتوبة بـ postext 1.4 أو ما قبله). والقيمة السالبة لـ marginBottom تسحب الكتلة التالية إلى الأعلى في الحالتين. |
عن الكلمات المعزولة. الكلمة المعزولة فقرةٌ سطرها الأخير أقصر من أن يبدو سطرًا حقيقيًا من النص — عادةً كلمة أو كلمتان قصيرتان منقطعتان في آخر الفقرة. ولأن الفحص يقوم على طول السطر بالبكسل نسبةً إلى عرض المسافة العادية، يتكيّف runtMinCharacters تلقائيًا مع حجم الخط الحالي. الكلمة القصيرة التي تكون بصريًا أعرض من runtMinCharacters × spaceWidth لا بأس بها؛ أما الكلمة الأضيق من ذلك (أو المنفردة فعلًا) فتستجلب جزاء الكلمة المعزولة. تحسب العتبة المسافات بين الكلمات، وعرضها نحو نصف عرض الحروف: فالقيمة الافتراضية 20 تعني سطرًا أخيرًا من نحو 8 إلى 12 حرفًا. كل كلمة معزولة تكلّف الجزاء كله، فبين نهايتين تحت العتبة يُبقي الكاسر الأسطر الأضيق في الأعلى؛ أما gradedRuntPenalty فيسعّر كلًّا منهما بمقدار قصورها، فتفوز النهاية الأطول. ولمن يهمّه الحساب: عند القيمة الافتراضية 1000 لـ runtPenalty، يغلب تجنّبُ الكلمة المعزولة أيَّ مجموعة نقاط كسر بديلة تتطلب تمدّدًا في تباعد الكلمات حتى نحو r≈2.15.
مرنة لا صارمة. لا تستطيع أيٌّ من هذه القواعد أن تمنع كسرًا — فالمحرّك سينتج إخراجًا دائمًا. إنها نقاط جزاء: تُوازن الخوارزمية بين الرداءة وكلفة التقسيم بالواصلة وسلاسة فئات الملاءمة وهذه الجزاءات البنيوية في عملية تحسين شاملة واحدة، وتختار مجموعة نقاط الكسر ذات الكلفة الإجمالية الأدنى. إن احتجت ضمانًا أشدّ، فارفع الجزاء؛ وإن كان مستند ما يُقرأ أفضل مع جزاء مخفّف، فاخفضه.
#العناوين
تتحكم الخاصية headings في طباعة كل مستويات العناوين (H1–H6). يمكنك ضبط قيم افتراضية عامة تسري على كل المستويات، ثم تجاوز خصائص بعينها لكل مستوى.
#القيم الافتراضية العامة
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
fontFamily | string | 'Open Sans' | عائلة الخط لكل العناوين. |
lineHeight | Dimension | 1.2 em | ارتفاع السطر في العناوين، وهو أضيق منه في النص الأساسي. |
color | ColorValue | اللون الرئيسي (#295AA3) | لون نص العنوان. مرتبط بالمدخل main-color في لوحة الألوان الافتراضية، فإذا غيّرت ذلك اللون في اللوحة تغيّر لون كل العناوين. |
textAlign | 'left' | 'justify' | 'center' | 'right' | 'start' | 'end' | 'left' | محاذاة كل مستويات العناوين (لا توجد قيمة خاصة بكل مستوى): إلى اليسار بحافة يمنى حرّة، أو مضبوطة، أو في الوسط، أو إلى اليمين بحافة يسرى حرّة. العنوان المضبوط يضع سطره الأخير ملاصقًا لليسار، كما تفعل الفقرة، فيبدو العنوان ذو السطر الواحد كأنه 'left'. ترسم Canvas وHTML وPDF الأسطر بالطريقة نفسها، بما في ذلك بادئة الرقم. وتتبعها أيضًا الافتتاحية الافتراضية لعنوان span: 'page' ليس له تصميم متقدم (القيمة المضبوطة تضعها ملاصقة لليسار)؛ أما التصميم المتقدم فيحاذي عناصره النصية بقيمة align الخاصة بكل منها. |
fontWeight | number | 700 | وزن خط العناوين (100–900). |
marginTop | Dimension | 1.5 em | المسافة فوق العناوين. |
marginBottom | Dimension | 0.5 em | المسافة تحت العناوين. |
keepWithNext | boolean | true | عند true لا يوضع العنوان أبدًا عنصرًا أخيرًا في عمود أو صفحة. إذا لم يبقَ للكتلة التالية بعد العنوان متسع لعدد من الأسطر لا يقل عن bodyText.widowMinLines (أو لسطر واحد حين تكون قيمة bodyText.avoidWidows هي false)، يُدفع العنوان إلى الأمام ليبقى ملتصقًا بنصه. ويتفاعل مع bodyText.keepColonWithList: إذا اضطرت تلك القاعدة إلى دفع الفقرة المنتهية بنقطتين كاملةً، انتقلت معها العناوين الأخيرة في العمود بدل أن تُترك معزولة. |
keepWithNextSplit | 'rules' | 'fill' | 'rules' | كيف تنقسم الفقرة التي تحت العنوان حين ينتهي العنوان في أسفل عمود ويؤدي دفع الفقرة كاملة إلى ترك العنوان خلفها. 'rules': أكبر عدد من الأسطر يتسع له المكان، بشرط أن يبقى تحت العنوان عدد من الأسطر لا يقل عن bodyText.widowMinLines وأن ينتقل إلى العمود التالي عدد لا يقل عن bodyText.orphanMinLines؛ وإذا لم يحقق أي تقسيم الشرطين معًا، انتقل العنوان مع فقرته، وملأت موازنة الأعمدة المكان الذي يتركه. 'fill': كل ما يتسع له المكان من أسطر مهما قلّ ما ينتقل منها، فالفقرة ذات الأسطر الأربعة التي يتسع المكان لثلاثة منها تنقسم 3 + 1. حتى الإصدار postext 1.4 كان كل عنوان ينقسم بهذه الطريقة، والإعدادات المخزّنة قبل configVersion 8 تحتفظ بها (انظر الحزم المكتوبة بالإصدار postext 1.4 أو أقدم). وعند تعطيل avoidWidows أو avoidOrphans يسقط ذلك الشق من القاعدة. |
snapToGrid | boolean | true | هل يعود التدفق إلى شبكة خطوط الأساس تحت العنوان. مع true تُقرَّب قيمة marginBottom للعنوان صعودًا إلى عدد صحيح من أسطر الشبكة؛ ومع false يُحتفظ بالهامش الدقيق، وقد يقع النص تحت العنوان خارج الشبكة حتى نقطة الالتقاط التالية (نهاية قائمة، أو ذيل حاوية :::paragraphs، أو معادلة منفصلة) — وهي الطريقة التي تضع بها كتب كثيرة سطرًا ونصف سطر تحت العنوان. يمكن لمستوى (levels[].snapToGrid) أو لنمط عنوان أن يحدد قيمته الخاصة؛ وهذه القيمة هي ما يرثانه. |
inlineMarks | boolean | true | هل يقرأ العنوان علاماته المضمّنة كما تقرؤها الفقرة: italic وbold و^superscript^ و~subscript~ و:smallcaps[…] والروابط. المقطع المائل يعكس ميل العنوان، فيظهر قائمًا في عنوان مائل؛ والمقطع العريض يأخذ bodyText.boldFontWeight، أو وزن العنوان نفسه إن كان أثقل. يعرض جدول المحتويات المقاطع العريضة والمائلة أيضًا؛ أما الترويسات وإشارات PDF المرجعية فتطبع النص وحده. مع false تُحذف العلامات وتُطبع الكلمات بنمط العنوان نفسه، كما كان الحال حتى postext 1.4. الافتتاحية الافتراضية لعنوان span: 'page' ليس له تصميم تضبط أيضًا المقاطع العريضة والمائلة والعلوية والسفلية؛ أما تصميم العنوان (شريط افتتاح مصمَّم أو advancedDesign داخل العمود) فيطبع نصًا عاديًا في الحالتين. الإعدادات المخزّنة سابقًا التي تحمل عناوينها علامات تُقرأ بقيمة false (انظر الحزم المكتوبة بالإصدار postext 1.4 أو أقدم). |
balancing | ColumnBalancingConfig | مفعّلة | موازنة الأعمدة عموديًا — مسافة إضافية فوق العناوين كي تنتهي الأعمدة محاذية لأسفل الصفحة. انظر أدناه. |
عناوين في الوسط لقصائد أو لفصول مسرحية، بلا خانة تصميم:
headings: {
textAlign: 'center',
levels: [{ level: 2, textTransform: 'uppercase' }],
}تمرير كائن headings يحتفظ بكل قيمة افتراضية لمستوى لا تعيد ذكرها، بما في ذلك فاصل الصفحة قبل H1 (انظر تجاوزات كل مستوى).
#موازنة الأعمدة
يتوقع الناشرون أن يبدأ كل عمود عند أعلى الصفحة وأن ينتهي محاذيًا لأسفلها. لكن قواعد الفصل (حماية الأسطر اليتيمة والأرامل، وإبقاء العناوين مع نصها، والأشكال التي لا تنقسم) تترك بطبيعتها أعمدة قصيرة — سطرًا فارغًا أو أكثر من شبكة خطوط الأساس في أسفلها. عند تفعيل الموازنة يفعل المحرّك ما يفعله المنضّد، فيطبّق أدواته (levers) بترتيب الأولوية التحريرية:
- إطار يختم العمود — الإطار الذي ينهي عمودًا قصيرًا يُدفع إلى الأسفل بمقدار المتسع الذي تحت قاعدته بالضبط، فتقع حافته السفلى على آخر خانة في شبكة الصفحة، بمحاذاة آخر سطر في العمود المجاور. ويأخذ ذلك المتسع حتى لو كان أقل من سطر، ما دام لا ينتقل شيء إلى عمود آخر. افتراضيًا يعمل قبل كل الأدوات الأخرى ويأخذ الفجوة كلها، فقد ينتهي الإطار الذي يعلّق على الفقرة التي فوقه على بعد عدة أسطر منها؛ والقيمة
closingBox: 'last'تترك للعناوين ونهايات القوائم وأدوات المسافات الأخرى أن تأخذ الأسطر الكاملة أولًا، ولا يأخذ الإطار إلا ما تتركه، والقيمةclosingBox: 'off'لا تحرّكه أبدًا. - العناوين — تُضاف أسطر كاملة من الشبكة إلى الهامش العلوي للعناوين داخل العمود القصير. وإذا احتيج إلى عدة أسطر وكان في العمود عدة عناوين، وُزّعت الأسطر عليها، مع إعطاء الحصة الكبرى دائمًا للعنوان الأهم (يتلقى
h2أكثر مما يتلقىh3). العناوين الواقعة في أعلى العمود تمامًا لا تتلقى مسافة إضافية أبدًا، فتظل الأعمدة تبدأ عند أعلى الصفحة — إلا العنوان الواقع مباشرة تحت شكل أو جدول يتصدّر عموده في صفحة يستمر تدفقها إلى التالية: هنا يذهب المتسع فوق ذلك العنوان، تحت الشكل. - نهايات القوائم — حين لا تستطيع العناوين استيعاب الفجوة كلها، يُضاف سطر من الشبكة حيث تنتهي قائمة أو تعداد (المسافة بعد القائمة تبدو طبيعية للقارئ)، مع حد أقصى لكل نهاية قائمة.
- الفقرات المرخاة — في آخر المطاف، يُعاد تقسيم فقرة واحدة من العمود إلى أسطر بحيث تزيد سطرًا واحدًا (
\looseness=+1في TeX)، ويُختار أطول فقرة كي تتوزع المسافة الإضافية بين الكلمات دون أن تُلحظ. لا يُقبل الحل المرخى إلا إذا بقي كل سطر دونbodyText.maxWordSpacing— فلا تتجاوز كثافة الطباعة (type colour) الحد الذي ضبطته أصلًا. يتطلبbodyText.optimalLineBreaking.
لا يُوازَن العمود الأخير من الصفحة إلا إذا كان تدفق الصفحة يستمر طبيعيًا إلى التالية — فمن الطبيعي أن تنتهي الصفحة الأخيرة من الفصل قصيرة. وهذه الصفحة، وكذلك الشريط الختامي الذي يقطعه trailing قطعًا متساويًا، تحافظ أيضًا على تساوي رؤوس أعمدتها: لا يُضاف سطر تحت شكل أو جدول يتصدّر أحد أعمدتها (أداة ما بعد العنصر العائم)، والعنوان أو الإطار الذي يفتتح عمودًا تحت مثل هذا الشكل يبقى في رأسه، مهما كانت قيمة stretchAfterFloats، فلا يبدأ عمود أدنى من العمود المجاور لمجرد مساواة الأسافل.
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
enabled | boolean | true | هل تُوازَن أسافل الأعمدة. |
maxLinesPerHeading | number | 4 | الحد الأقصى لأسطر الشبكة الإضافية التي يمكن إضافتها فوق عنوان واحد. |
stretchAfterLists | boolean | true | السماح بأسطر شبكة إضافية حيث تنتهي قائمة، حين لا تستطيع العناوين استيعاب الفجوة كلها. |
maxLinesAfterList | number | 1 | الحد الأقصى لأسطر الشبكة الإضافية بعد نهاية قائمة واحدة. |
stretchAfterFloats | boolean | true | السماح بأسطر شبكة إضافية تحت شكل أو جدول يتصدّر العمود القصير (عنصر عائم علوي)، بعد أداة نهاية القائمة، فينزل النص الذي تحته بدل أن ينتهي العمود قصيرًا. لا يعمل أبدًا في صفحة لا يستمر تدفقها (الصفحة الأخيرة من فصل) ولا في شريط ختامي يقطعه trailing قطعًا متساويًا: هناك تبقى رؤوس الأعمدة متساوية وقد ينتهي العمود الأخير أقصر بسطر. قد تكون الكتلة الأولى تحت الشكل بقيةَ فقرة بدأت في الصفحة السابقة: تنزل هي أيضًا، بمقدار السطر الذي تركته قواعد الفصل خاليًا في أسفل العمود (سطر أبقته قاعدة الأرملة فارغًا، أو مسافة فقرة لا متسع بعدها لنص). اضبطه على false لإبقاء النص مباشرة تحت الشكل. |
maxLinesAfterFloat | number | 1 | الحد الأقصى لأسطر الشبكة الإضافية تحت عنصر عائم علوي واحد. |
looseParagraphs | boolean | true | آخر الأدوات: إعادة تقسيم فقرات العمود القصير بإرخاء يزيدها سطرًا (سطر إضافي لكل منها)، ضمن حدود bodyText.maxWordSpacing. |
maxLooseParagraphs | number | 2 | عدد فقرات العمود القصير الواحد التي يجوز أن تطول سطرًا، بدءًا بالأطول. |
trackParagraphs | boolean | true | حين لا يكفي تباعد الكلمات وحده لكسب السطر، يجوز للفقرة المرخاة أن تأخذ أيضًا أصغر تتبّع موجب (تباعد الحروف) يحقق ذلك. |
maxTracking | number | 10 | الحد الأعلى لذلك التتبّع، بأجزاء الألف من em لكل حرف (10 = 0.01 em). |
trailing | boolean | true | مساواة الشريط الختامي للفصل وللمستند: حين ينتهي التدفق قبل امتلاء الصفحة (عند صفحة افتتاح فصل، أو :::part، أو إطار placement: 'fixed' يختم الفصل، أو نهاية المستند) وأعمدته غير متساوية، تُقطع قطعًا متساويًا — بحد أقصى للشريط قدره ceil(Σ used / N / grid) سطرًا، يُحسب بعد أن تستقر الأدوات السابقة على الصفحات الأسبق — فتنتهي قائمة مراجع قصيرة على الارتفاع نفسه في كل عمود بدل أن تملأ العمود الأول وتترك الأخير نصف فارغ. يحافظ القطع على القواعد التي يحافظ عليها شريط غير مقطوع: إذا كانت كتلة لا يمكن تقسيمها عبره (ذيل فقرة يبقيه الحدّان الأدنيان لليتيمة والأرملة كاملًا، أو إطار يبقى متماسكًا) ستتجاوزه — فتفقد أسطرها الأخيرة، لأن المُخرِجات تقصّ العمود عند حدود صندوقه — أو كان عنوان سيختم عمودًا بينما يفتتح نصه العمود التالي (مع headings.keepWithNext، وهو الافتراضي)، يؤخذ القطع أدنى بسطر، حتى ثلاث مرات، وإلا أُلغي. الكتلة التي لا تستطيع أن تبدأ في الأسطر التي يتركها القطع تحت شكل يتصدّر عمودًا (تحتاج الفقرة هناك إلى حدها الأدنى لليتيمة) تنتقل إلى العمود التالي من الشريط، كما تفعل من عمود غير مقطوع، ويقف الشكل وحده في عموده. الأعمدة التي تختلف أسافلها بسطر شبكة واحد أو أقل تُترك كما هي: الشريط الختامي الذي ينتهي عموده الأخير أقصر بسطر هو الخاتمة المعتادة، وقطعه لن يفعل سوى نقل سطر. يذهب القطع إلى الشريط الذي قيس فيه، وكذلك القطع الذي يطلبه إطار يمتد بعرض الصفحة في منتصفها: حتى postext 1.4، حين كان أول نص في الصفحة الختامية قد عُرض أولًا على شريط لا متسع فيه له (صفحة يشغلها عنصر عائم كلها، أو الشريط الرفيع الذي يتركه إطار يمتد بعرض الصفحة في أسفل الصفحة السابقة)، كان القطع يُستهلك في ذلك الشريط وتُبقي الصفحة الختامية كل سطر في عمودها الأول. الأعمدة المختلفة العرض (تخطيط عمود ونصف فيه نص في العمودين) تُقطع بحسب المساحة، فيُرجَّح ارتفاع كل عمود بعرضه، لأن سطر العمود الضيق يتسع لقدر أقل من النص. لا يعمل إلا حين تكون قيمة enabled هي true. |
beforeSpan | boolean | true | مساواة الشريط الذي تتركه كتلة تمتد بعرض الصفحة: حين لا يتسع إطار span: 'page' تحت الأعمدة الحالية حتى بعد قطع متساوٍ، ويجب أن ينتقل إلى الصفحة التالية أو ينقسم (calloutStyles[].keepTogether: false)، تُقطع الأعمدة التي يقاطعها قطعًا متساويًا — بالحد الختامي نفسه الذي يناله الشريط الختامي — بدل أن يملأ الأول الصفحة وينتهي الأخير قصيرًا. تُترك الصفحة فاصلًا صريحًا كي لا تمدّ الأدوات السابقة عمودها الأخير من جديد حتى أسفل الصفحة؛ ثم يستقر الإطار، أو الجزء الذي يتسع منه، تحت الأعمدة المتساوية. لا يعمل إلا حين تكون قيمة enabled هي true. |
closingBox | 'first' | 'last' | 'off' | 'first' | متى يأخذ الإطار الذي يختم عمودًا قصيرًا المتسعَ الذي تحت قاعدته (الأداة 1، وتُسجَّل باسم trailingCallout). 'first': قبل أي أداة أخرى، كما كان الحال حتى postext 1.4 — يأخذ الإطار الفجوة كلها، ولو كانت عدة أسطر، ولا تنال العناوين التي فوقه شيئًا. 'last': بعد أدوات العناوين ونهايات القوائم والمعادلات المنفصلة والعناصر العائمة، التي تأخذ الأسطر الكاملة أولًا؛ ينزل الإطار مع النص الذي فوقه ثم لا يأخذ إلا ما تركته، وهو عادة جزء من سطر، فتلتقي قاعدته بآخر خانة في الشبكة ويبقى قريبًا من النص الذي يعلّق عليه. 'off': أبدًا؛ يحتفظ الإطار بالمتسع الذي تحت قاعدته، وإن كانت الأدوات الأخرى تستطيع إنزاله بأسطر كاملة حين تضيف مسافة فوقه، ويبقى جزء السطر المتروك تحته. في الصفحة الختامية، حيث الإطار هو الأداة الوحيدة التي تساوي قاعدته بالعمود المجاور، تتركه 'off' حيث وضعه التدفق. أي قيمة أخرى تُقرأ 'first'. |
أيّ أداة عملت
يسجّل الإخراج كل أداة طبّقها، على الكتلة التي طبّقها عليها: block.balancing في VDT الذي تعيده buildDocument. يمكن لبروفة أو اختبار أو تقرير أن يبيّن لماذا ينتهي عمود محاذيًا للأسفل — وأي الأعمدة لم تستطع أي أداة إغلاقها. الكتل التي تركتها الموازنة على حالها لا تحمل balancing، والمستند المبني مع enabled: false لا يحمل شيئًا منها.
interface VDTBalancing {
levers: BalanceLever[]; // usually one: a paragraph after a list can take the list-end line and run a line long too
spaceAbove: number; // px the spacing levers added above the block (0 when only looseParagraph fired)
extraLines?: number; // looseParagraph: lines the paragraph gained
tracking?: number; // looseParagraph: the tracking that gained them, in thousandths of an em (0 = word spacing alone)
}
type BalanceLever = 'trailingCallout' | 'heading' | 'listEnd' | 'afterDisplay' | 'afterFloat' | 'looseParagraph';| الأداة | تُسجَّل على | ما فعلته |
|---|---|---|
trailingCallout | كتلة حدود الإطار | إطار يختم العمود نزل بمقدار المتسع الذي تحت قاعدته بالضبط (spaceAbove؛ ولا بأس بجزء من سطر). |
heading | العنوان | أسطر شبكة كاملة فوق العنوان، حتى maxLinesPerHeading. |
listEnd | أول كتلة بعد القائمة | سطر شبكة حيث تنتهي قائمة، حتى maxLinesAfterList. |
afterDisplay | الكتلة التي تلي المعادلة أو الإطار | سطر شبكة تحت معادلة منفصلة أو إطار. |
afterFloat | أول كتلة في العمود | سطر شبكة بين شريط عناصر عائمة يتصدّر العمود ونصه، حتى maxLinesAfterFloat. |
looseParagraph | الفقرة | الفقرة بعد إعادة تقسيمها أطول بمقدار extraLines، بأصغر تتبّع حقق ذلك (tracking؛ وقيمة block.letterSpacing هي القيمة نفسها بالبكسل). |
تُسجَّل القطوع المتساوية على الأعمدة التي قطعتها: قيمة column.bandCapped هي true على كل عمود قُطع قطعًا متساويًا (الشريط الذي يتركه إطار يمتد بعرض الصفحة، أو شريط ختامي)، وcolumn.trailingCap تعلّم أيضًا الشريط الختامي لفصل أو للمستند (trailing).
import { buildDocument } from 'postext';
const doc = buildDocument(content, config);
for (const page of doc.pages) {
page.columns.forEach((column, i) => {
const levers: string[] = column.blocks.flatMap((block) => block.balancing?.levers ?? []);
if (column.trailingCap) levers.push('closing band cut level');
else if (column.bandCapped) levers.push('band cut level');
if (levers.length > 0) console.log(`page ${page.index + 1}, column ${i + 1}: ${levers.join(', ')}`);
});
}
// page 3, column 1: heading, heading
// page 3, column 2: listEnd, looseParagraph#تجاوزات كل مستوى
يمكن لكل مستوى عناوين أن يتجاوز القيم الافتراضية العامة عبر المصفوفة levels. لا يختلف افتراضيًا إلا fontSize (إضافة إلى breakBefore في H1، المذكور أدناه) — وكل الخصائص الأخرى تُورث من إعدادات العناوين العامة.
| المستوى | حجم الخط الافتراضي | breakBefore الافتراضي |
|---|---|---|
| H1 | 18 pt | { enabled: true, parity: 'always-odd' } |
| H2 | 15 pt | { enabled: false, parity: 'any' } |
| H3 | 12 pt | { enabled: false, parity: 'any' } |
| H4 | 10 pt | { enabled: false, parity: 'any' } |
| H5 | 9 pt | { enabled: false, parity: 'any' } |
| H6 | 8 pt | { enabled: false, parity: 'any' } |
القيمة الافتراضية في H1 تحاكي إخراج الفصول في الكتب: كل عنوان من المستوى الأعلى يُفتتح في صفحة فردية جديدة (اليمنى في كتاب يُقرأ من اليسار إلى اليمين)، مع صفحة فاصلة فارغة إلزامية بعد الفصل السابق. تجاوزها في levels[0].breakBefore إذا كانت بنية مستندك أبسط من بنية الكتاب.
يُدمج breakBefore الخاص بمستوى حقلًا بحقل فوق تلك القيمة الافتراضية، وكائن headings الذي لا يذكره يحتفظ بها. القيمة { parity: 'odd' } في H1 تُبقي الفاصل وتغيّر الزوجية وحدها؛ والقيمة { enabled: false } تجعل الفصول تتوالى دون فاصل:
headings: {
fontFamily: 'Merriweather', // H1 still breaks to a fresh recto (always-odd)
levels: [{ level: 1, breakBefore: { parity: 'odd' } }], // …or: a recto, with no mandatory blank
}
headings: { levels: [{ level: 1, breakBefore: { enabled: false } }] } // chapters run onتغيّر في postext 1.5. حتى postext 1.4 كان أي كائن headings يعطّل فاصل H1 ما لم يُعَد ذكر levels[0].breakBefore، وكان breakBefore الجزئي يملأ حقله الناقص من القيمة الافتراضية التي لا فاصل فيها. لذلك فإن إعدادًا مكتوبًا في الشيفرة للإصدار 1.4 فيه كائن headings وليس فيه فاصل قبل H1 يفتتح الآن كل فصل في صفحة فردية جديدة، مع صفحات زوجية فارغة حيث يلزم. لإبقاء الفصول متوالية، أعطِ مدخل H1 في levels حقلًا واحدًا:
headings: { fontFamily: 'Merriweather', levels: [{ level: 1, breakBefore: { enabled: false } }] } // as 1.4 laid it outحين يكون الإعداد مخزّنًا، يستطيع المحرّك أن يعرف ذلك فيقوم بالعمل عنك: Sandbox للكتب والإعدادات التي حفظها آنذاك (انظر Sandbox › حفظ العمل)، وopenBundle / readBundle لحزمة .postext مكتوبة بالإصدار postext 1.4 أو أقدم (انظر الحزم المكتوبة بالإصدار postext 1.4 أو أقدم). أما الإعداد الذي خزّنته بنفسك فيمكن تمريره عبر migrateConfig(config) من postext/bundle، التي تكتب صراحةً الفواصل التي أخرجها الإصدار 1.4 (pinLegacyHeadingBreaks): enabled: false على H1 لم يكن له فاصل، وparity: 'any' بجانب enabled: true لم يذكر زوجية، في H1 وفي أنماط العناوين. وتثبّت أيضًا حجم الرياضيات، والمسافة حول الأشكال المضمّنة (في الإطارات أيضًا)، والعلامات المضمّنة في العناوين، وأحجام الحروف الاستهلالية، والمتسع تحت سطر النقطتين الذي يقدّم قائمة، والأسطر التي يتركها قطع الإطار من فقرة أو بند قائمة، وفواصل الأسطر عند الشرطات، والتقسيم سطرًا بسطر للنص غير المضبوط، وتقسيم الفقرة تحت العنوان، وفواصل الأسطر عند واصلة الكلمة المركبة، والمسافة تحت حاويات :::paragraphs، كما ضبطها الإصدار 1.4. مرّر نص Markdown للكتاب وسيطًا ثالثًا، { content }، فتُسقط تثبيت الرياضيات إذا لم يكن في النص $، وتثبيتات الفجوات إذا لم يضمّن أي سطر موردًا (وتثبيت فجوة الإطار إذا لم يفعل ذلك أي سطر داخل إطار)، وتثبيت علامات العناوين إذا لم يحمل أي عنوان علامة، وتثبيت سطر النقطتين إذا لم تلِ أي قائمة سطرًا ينتهي بنقطتين، وتثبيت قطع الإطار إذا لم يفتح النص أي :::callout، وتثبيت الشرطة إذا لم توضع أي شرطة ملاصقة بين كلمتين، وتثبيت تقسيم العنوان إذا لم يكن في النص عنوان، وتثبيت الحاوية إذا لم يفتح النص أي حاوية :::paragraphs، وتثبيت الكلمات المركبة إذا لم تقع أي واصلة بين حرفين؛ ولا يذهب تثبيت التقسيم غير المضبوط إلا إلى إعداد يجعل بعض النص الجاري غير مضبوط، ولا تثبيت الحاوية إلا إلى إعداد يعرّف نمط فقرة (انظر الحزم المكتوبة بالإصدار postext 1.4 أو أقدم). وللفواصل وحدها، استدعِ pinLegacyHeadingBreaks(config).
تدعم تجاوزات المستوى الخصائص نفسها التي تدعمها القيم الافتراضية العامة — fontSize وlineHeight وfontFamily وcolor وfontWeight وmarginTop وmarginBottom وsnapToGrid — إضافة إلى هذه الحقول الخاصة بالمستوى:
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
italic | boolean | false | يرسم العنوان بخط مائل. يُطبَّق فوق fontWeight. |
textTransform | 'none' | 'uppercase' | 'none' | يحوّل نص العنوان إلى أحرف كبيرة (تبقى بادئة الترقيم كما كُتبت، وكذلك الشارة (chip) وتسمية :ref فيه). يحافظ على الطول كي تبقى خريطة المصدر في المحرّر متطابقة 1:1: الحروف التي يطول شكلها الكبير (ß → SS) تُترك كما هي. والنص المحوَّل يغذي أيضًا العنصر النائب {titleText} في التصاميم المتقدمة وصفحات افتتاح الفصول. أما إشارات PDF المرجعية فتحتفظ بالعنوان كما كُتب — Author contributions لا AUTHOR CONTRIBUTIONS — كما تترك خاصية text-transform في CSS النص نفسه دون تغيير (حتى postext 1.4 كانت تأخذ الأحرف الكبيرة). |
letterSpacing | Dimension | 0 | التتبّع بعد كل رسم حرفي (glyph) في العنوان — بما في ذلك المسافات وبادئة الترقيم — كما في letter-spacing في CSS. القيمة الموجبة تباعد الحروف (الأحرف الكبيرة الناتجة عن textTransform: 'uppercase' تحتاج عادة إلى قليل منه: { value: 0.12, unit: 'em' })، والسالبة تضيّق حجمًا عرضيًا كبيرًا. قيمة em نسبية إلى fontSize الخاص بالمستوى. تُقاس أسطر العنوان معه، فتلتف حيث ينتهي النص المتتبَّع، وترسمه Canvas وHTML وPDF بالطريقة نفسها. السطر الموسَّط أو المحاذى إلى اليمين يوضع بحسب حروفه: يُستبعد التتبّع الذي بعد آخر رسم حرفي فيه، كما في نص التصميم، فيصطف العنوان الموسَّط مع الافتتاحية الافتراضية لعنوان span: 'page'. المستوى المرسوم من advancedDesign الخاص به يتجاهله، شأنه شأن حقول الطباعة الأخرى هنا: لكل عنصر نصي في التصميم letterSpacing خاص به. ويضبطه نمط العنوان أيضًا، للعناوين التي تستخدمه. حتى postext 1.4 لم يكن للعناوين تتبّع، وكان المفتاح يُحذف دون أي تنبيه. |
numberingTemplate | string | '' | قالب الرقم التلقائي للمستوى. الرمز … يطبع العدّاد الجاري لمستوى العنوان ذاك، منسَّقًا اختياريًا بلاحقة — أرقام رومانية كبيرة، و رومانية صغيرة، و / أبجدية، و مع أصفار بادئة، و بالحروف (twenty-one)، و عددًا ترتيبيًا (twenty-first)، و بالأرقام الصينية، و رقمًا رقمًا، و بالأرقام المالية الصينية، و داخل دوائر، أو أي اسم من تسميات صيغ الترقيم — وأي نص آخر يُطبع حرفيًا ('Chapter . '، '.'، '第回' لطباعة 第一百二十回؛ والشرطة المائلة العكسية تجعل القوس المعقوف حرفيًا). الرمز الذي ما زال عدّاده فارغًا يختفي مع الفاصل المجاور له. القيمة الفارغة (الافتراضية) تعني عدم وجود رقم تلقائي. يُضاف الرقم الناتج قبل العنوان في التدفق، ويغذي العنصر النائب في خانة التصميم المتقدم (حيث لا تُضاف البادئة نفسها)، ويُطبع في جدول المحتويات. انظر الأعداد المكتوبة بالحروف للكلمات، وأنماط العناوين لقالب خاص ببعض عناوين مستوى ما. |
numberSeparator | string | ' ' | ما يقع بين الرقم والعنوان: في العمود، وفي الافتتاحية الافتراضية لمستوى span: 'page'، وفي الترويسات التي تطبع سطر العنوان، وفي إشارات PDF المرجعية. وفاصل المستوى 1 يربط أيضًا رقم الجزء بعنوانه في صفحة الجزء الافتراضية وفي صف الجزء الافتراضي في جدول المحتويات. تأخذ عناوين الفصول الصينية مسافة إيديوغرافية أو لا شيء (' ': 第一回 甄士隱夢幻識通靈). يحتفظ جدول المحتويات بعمود أرقامه الخاص (toc.levels[].numberGap). ويجوز أن يضبط نمط العنوان فاصله الخاص. حين يُقسم عنوان إلى شطرين باستخدام ، كما تُقسم عناوين الفصول المزدوجة في الروايات الصينية، تربط الصيغ ذات السطر الواحد (العمود، وجدول المحتويات، والترويسات) الشطرين بمسافة إيديوغرافية حين يكون الجانبان حروفًا صينية أو يابانية، وبمسافة عادية في غير ذلك. |
numberPosition | 'before' | 'replace' | 'before' | موضع الرقم المولَّد. 'before': قبل العنوان، موصولًا به عبر numberSeparator. 'replace': الرقم هو العنوان كله، ولا يُطبع العنوان المكتوب في المصدر، فإن # Night مع numberingTemplate: 'الليلة {1:ordinal-feminine}' يطبع الليلة الثانية. يُدرج جدول المحتويات الرقم عنوانًا للمدخل (بلا عمود أرقام)، وتقرؤه أيضًا الترويسات (، ) وإشارات PDF المرجعية؛ ويكون فارغًا لمثل هذا العنوان. لا يسري إلا على العناوين المرقّمة التي لها قالب (قالب مستواها أو نمطها)؛ وأي عنوان آخر يحتفظ بنصه. ويجوز أن يضبط نمط العنوان قيمته الخاصة، 'before' للاحتفاظ بالعناوين التي كان مستواه سيستبدلها. |
breakBefore | HeadingBreakBeforeConfig | H1: { enabled: true, parity: 'always-odd' }H2–H6: { enabled: false, parity: 'any' } | فرض فاصل صفحة قبل كل عنوان من هذا المستوى. parity: 'odd' / 'even' يقيّد كذلك الجانب الذي يُفتتح فيه العنوان من الصفحتين المتقابلتين — وتُدرج صفحة حشو فارغة عند الحاجة (وتُحتسب في ترقيم الصفحات). أما 'always-odd' / 'always-even' فتضمنان فوق ذلك صفحة فاصلة فارغة إلزامية واحدة على الأقل بين المحتوى السابق والعنوان الجديد (تنتمي الصفحة الفاصلة إلى الفصل السابق؛ وأي حشو إضافي للزوجية ينتمي إلى الفصل الجديد). حين يكون العنوان أول كتلة في المستند والصفحة الأولى ما زالت فارغة، يُتخطى فرض الزوجية — فيقع العنوان في الصفحة 1 كما كُتب. الحقل غير المضبوط يحتفظ بالقيمة الافتراضية للمستوى: { parity: 'odd' } في H1 يظل يفرض الفاصل. |
hidden | boolean | false | عنوان بنيوي: لا يطبع شيئًا ولا يشغل مكانًا في العمود ولا في إطار — لا نص ولا هوامش ولا شريط افتتاح — لكنه يفعل كل ما يفعله العنوان عدا ذلك. ما زال breakBefore الخاص به يفتح صفحة، ويفتتح قسم نمطه، ويُعدّ (ما لم يقل نمطه numbered: false)، ويُدرجه :::toc، وتسمّيه ترويسات ، ويحصل على إشارة مرجعية في PDF. يناسب الإهداء أو صفحة التصدير (epigraph) أو بيانات الطبع (colophon) التي يحتاجها جدول المحتويات وإشارات القارئ المرجعية، ولا تُظهرها الصفحة. اضبطه على نمط عنوان لا على مستوى كامل؛ ويتجاوزه العنوان باستخدام / . |
headings: {
fontFamily: 'Merriweather',
levels: [
// Canonical book preset: chapters on a right-hand (odd) page.
{ level: 1, fontSize: { value: 24, unit: 'pt' }, breakBefore: { enabled: true, parity: 'odd' } },
{ level: 2, fontSize: { value: 18, unit: 'pt' }, italic: true },
]
}يعمل snapToGrid لكل مستوى أيضًا. إن لم يُضبط، يتبع المستوى headings.snapToGrid؛ وإن ضُبط، تجاوزه — فيمكن لمستند واحد أن يضع عناوين H2 فيه على بعد سطر ونصف فوق نصها، خارج الشبكة حتى نقطة الالتقاط التالية، بينما تقرّب عناوين H3 المسافة تحتها صعودًا إلى أسطر شبكة كاملة. ويمكن لنمط العنوان أن يضبطه كذلك، للعناوين التي تستخدمه.
headings: {
marginBottom: { value: 1.5, unit: 'em' },
levels: [
{ level: 2, snapToGrid: false }, // exactly 1.5 em under every H2
{ level: 3 }, // inherits headings.snapToGrid: true
],
}#الفاصل قبل العنوان
breakBefore مستقل عن عناصر التحكم في الترقيم: تفعيله يفرض فاصل صفحة، لكن العدّاد الرقمي لا يُعاد ضبطه إلا حين تُدرج صراحةً توجيه :::numbering. تُحتسب صفحات الزوجية الفارغة صفحاتٍ حقيقية في التسلسل، وتتلقى الترويسات والتذييلات وفق قواعدها المعتادة للصفحات الفردية والزوجية.
التوجيه :::pagebreak الواقع مباشرة قبل مثل هذا العنوان لا يحل محل فاصله: يظل العنوان يطبّق زوجيته بعد الصفحة التي فتحها التوجيه، وقد يضيف ذلك صفحة فارغة. انظر أنماط العناوين لعنوان ينبغي أن يبدأ مباشرة بعد فاصل يدوي.
قيم الزوجية
| القيمة | السلوك |
|---|---|
'any' (الافتراضية) | لا قيد على الزوجية. يُفتتح العنوان ببساطة في الصفحة التالية. |
'odd' | ضمان أن يُفتتح العنوان في صفحة فردية (يمنى في الكتاب اللاتيني). لا تُدرج صفحة فارغة واحدة إلا إذا كانت الصفحة التالية الطبيعية زوجية. |
'even' | مثلها، لكن لصفحة زوجية (يسرى في الكتاب اللاتيني). |
'always-odd' | ضمان صفحة فاصلة فارغة إلزامية واحدة على الأقل بين المحتوى السابق والعنوان الجديد، ثم ضمان أن تكون الصفحة فردية. مفيدة حين يجب أن يبدأ كل فصل في صفحتين متقابلتين جديدتين. |
'always-even' | مثلها، لكن لصفحة زوجية. |
تبعية الصفحات الفارغة
تحمل الصفحات الفارغة التي يدرجها breakBefore ترويسات بعنوان الفصل بحسب سبب إدراجها:
- الصفحات المدرجة لتلبية قيد الزوجية (
'odd'أو'even'أو ذيل الزوجية في'always-*') تنتمي إلى الفصل القادم. يُحلّ العنصر النائب{chapterTitle}في ترويستها إلى عنوان الفصل الجديد — لأن الصفحة الفارغة لا توجد إلا لدفع الفصل الجديد إلى الزوجية الصحيحة. - الصفحة الفاصلة الإلزامية في البداية التي يدرجها
'always-odd'/'always-even'تنتمي إلى الفصل السابق. إنها استراحة مقصودة في نهاية الفصل، لذا تظل ترويسة{chapterTitle}تُظهر عنوان الفصل القديم.
تتبع ترويسات القسم المنمَّط ولوحة ألوانه (انظر أنماط العناوين) القاعدتين نفسيهما في الصفحات الفارغة.
استثناء بداية المستند
حين تكون أول كتلة في المستند عنوانًا مفعَّلًا فيه breakBefore — أو حين يبدأ المصدر بالتوجيه :::pagebreak — يُتخطى فرض الزوجية ما دامت الصفحة الأولى فارغة. يقع العنوان في الصفحة 1 كما كُتب، بصرف النظر عن الزوجية المضبوطة، فلا يرث مستند يبدأ بالعنوان # Chapter 1 المضبوط على parity: 'odd' صفحة فارغة زائفة في بدايته. وبمجرد وضع أي محتوى، يعمل فرض الزوجية كالمعتاد.
#الامتداد والتصميم المتقدم
يقبل كل مستوى عناوين حقلين إضافيين يتحكمان في طريقة رسم العنوان صفحةَ افتتاح فصل بعرض الصفحة كله.
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
span | 'column' | 'page' | 'column' | عند 'page' يُعامل العنوان صفحةَ افتتاح فصل (opener)، ويُلحق تصميمه المتقدم (إن كان مفعّلًا) بالصفحة شريطَ افتتاح فوق المتن. اقرنه بالقيمة breakBefore.enabled: true كي تبدأ الافتتاحية صفحة جديدة دائمًا. إن لم يكن له تصميم خاص، ترسم العنوانَ افتتاحيةٌ افتراضية عبر منطقة المحتوى كلها، بطباعة المستوى وتباعد أسطره، مع مقاطعه العريضة والمائلة والعلوية والسفلية (headings.inlineMarks)، ويُقاس الشريط بذلك العرض. يتسع الشريط لكل سطر ترسمه الافتتاحية: حيث تأخذ الافتتاحية أسطرًا أكثر مما يأخذه العنوان في قياسه الخاص (عنوان مضبوط كانت مسافاته ستنكمش ليسعه سطر واحد، أو فاصل قسري )، يأخذها الشريط أيضًا. حتى postext 1.4 كان الشريط يُقاس والعنوان ملتف بعرض العمود، فكان العنوان الذي يسعه سطر واحد في منطقة المحتوى يأخذ شريطًا بارتفاع سطرين، والسطر موسَّط فيه. تبدأ الأعمدة الأخرى تحت الشريط كما يبدأ عمود العنوان نفسه. النص الذي يفتتح أحدها يبدأ حيث كان سيبدأ النص الواقع مباشرة تحت الافتتاحية، أيًّا كان ما يلي الافتتاحية في عمودها: marginBottom الخاص بالافتتاحية تحت عنوانها أو تصميمها، مأخوذًا إلى سطر الشبكة التالي حين يلتقط المستوى الشبكة. العنوان أو المعادلة المنفصلة أو صف جدول المحتويات الذي يفتتح أحدها يوضع على مستوى أول عنوان أو معادلة منفصلة تحت الافتتاحية، مع حساب الهوامش كما هناك؛ وحين يستمر عمود الافتتاحية بأي شيء آخر، يبدأ حيث يبدأ ذلك النص. العنوان المخفي تحت الافتتاحية لا يُحتسب، والمسافة التي تضيفها موازنة الأعمدة فوق العنوان الواقع تحت الافتتاحية لا تتكرر في الأعمدة الأخرى. ما تلتقطه هذه الأعمدة إلى الشبكة يقع على شبكة الصفحة. حتى postext 1.4 كانت كتلتها الأولى تستقر عند قاعدة الشريط: خارج الشبكة حين ينتهي الشريط بين سطرين، وملاصقة للشريط تمامًا بلا هامش حين يلي الافتتاحيةَ عنوان (أعلى بسطر مما هي الآن حين ينتهي الشريط على الشبكة)؛ وكان العنوان هناك يُسقط أيضًا الهامش العلوي الذي احتفظ به عنوان العمود الأول. |
advancedDesign | HeadingAdvancedDesignConfig | | خانة تصميم حرّ التركيب لهذا المستوى. عند enabled تؤلف عناصر الخانة الافتتاحية. استخدم داخل عنصر نصي لرسم نص العنوان؛ واستخدم و وغيرهما لإدراج رقم العنوان المنسَّق. |
advancedDesign.minHeight | Dimension | — | أدنى ارتفاع محجوز للعنوان في تدفق العمود. يأخذ العنوان max(design content bottom, minHeight)، ثم marginBottom الخاص بالعنوان تحته (من نمط عنوانه أو مستواه أو headings.marginBottom؛ 0.5 em من حجم العنوان افتراضيًا)، ويُقرَّب المجموع صعودًا إلى شبكة خطوط الأساس حين تلتقط العناوين الشبكة. وهكذا يمكن للافتتاحية أن تدفع النص الأساسي إلى الأسفل (أو أن تستحوذ على الصفحة كلها) حتى حين تكون عناصرها قصيرة أو مثبتة إلى إطارَي الصفحة أو النزف فوق العنوان. لشريط ارتفاعه minHeight بالضبط، اضبط ذلك marginBottom على 0 واجعل minHeight عددًا صحيحًا من أسطر الشبكة. يسري كلما كانت قيمة enabled هي true، حتى مع خانة فارغة. انظر الارتفاع المحجوز لما يحتسبه أسفل محتوى التصميم. |
افتتاحية بطول الصفحة. حين يمتد الارتفاع المحجوز إلى ما بعد قاعدة العمود — غلاف قيمة minHeight فيه تساوي ارتفاع الصفحة، أو صورة أو صندوق مثبت إلى الصفحة أو النزف ويمتد حتى حدّ القص — تستحوذ الافتتاحية على بقية الصفحة: يبدأ النص الذي يليها في الصفحة التالية، في كل عمود من تخطيط بعمودين أو بعمود ونصف على السواء. لذلك لا يحتاج الغلاف إلى :::pagebreak بعده (ولا ضرر من وجوده: لا يضيف صفحة فارغة). العنوان داخل العمود (span: 'column') الذي يكون تصميمه أطول من عموده يستحوذ على ذلك العمود، ويبدأ النص عند رأس العمود التالي. عندئذ تمتد كتلة العنوان حتى قاعدة العمود الذي تستحوذ عليه، ويُخرج تصميمها مقابل ذلك الشريط: العناصر المثبتة إلى الأعلى تبقى حيث ثُبتت، والعناصر التي تتبع الشريط — المثبتة إلى وسطه أو قاعدته، أو التي ارتفاعها 'fill' — تلتزم بالمتسع الذي يستحوذ عليه العنوان، كما تلتزم بالارتفاع المحجوز حين يتسع له العمود (حتى postext 1.4 كانت الكتلة تحتفظ بارتفاع نصها، فكانت تلك العناصر تُخرج مقابل شريط بارتفاع عنوان واحد، ويستمر النص الذي بعده تحت التصميم). عناصر الصفحة الثابتة التي لا ينبغي أن تستحوذ على الصفحة — شريط على الحافة الخارجية (fore-edge) بطول الصفحة كلها، أو زخرفة في أسفل الصفحة — مكانها تصميم الترويسة أو التذييل: إذا ثُبتت إلى 'page' أو 'bleed' وعُرضت مع pages: 'opener'، فإنها تُرسم في صفحة الافتتاح ولا تحجز أي مساحة من المتن.
مثال: افتتاحية فصل بسيطة تُظهر «Chapter N» فوق العنوان:
{
"headings": {
"levels": [
{
"level": 1,
"span": "page",
"breakBefore": { "enabled": true, "parity": "always-odd" },
"advancedDesign": {
"enabled": true,
"slot": {
"elements": [
{
"kind": "text",
"id": "chapterLabel",
"placement": {
"anchor": { "to": "container", "edge": "top" },
"offset": { "y": { "value": 48, "unit": "pt" } },
"size": { "width": "fill" }
},
"content": "Chapter {numberRoman}",
"fontSize": { "value": 10, "unit": "pt" },
"align": "center",
"overflow": "ellipsis-end"
},
{
"kind": "text",
"id": "chapterTitle",
"placement": {
"anchor": { "to": "#chapterLabel", "edge": "below" },
"offset": { "y": { "value": 12, "unit": "pt" } },
"size": { "width": "fill" }
},
"content": "{titleText}",
"fontSize": { "value": 24, "unit": "pt" },
"fontWeight": 700,
"align": "center",
"overflow": "wrap",
"hyphenate": true
}
]
}
}
}
]
}
}العناصر النائبة الخاصة بالعناوين المتاحة داخل خانة تصميم المستوى:
{titleText}— النص العادي للعنوان (بلا بادئة الترقيم). الفاصل القسري في العنوان (\\) يكون فاصل سطر هنا، في شريط الافتتاح وفي التصميم داخل العمود على السواء (حتى postext 1.4 كان التصميم داخل العمود يطبع مسافة مكانه، بينما يُقاس ارتفاعه مع الفاصل). الأسطر التي يلتف إليها العنوان المخفي في عموده تُضمّ مجددًا في العنوان كما كُتب: الكلمة المقسومة بعد واصلتها الأصلية تحتفظ بالواصلة بلا مسافة بعدها، والكلمة التي قسمها العمود تعود كاملة بلا الواصلة التي أضافها الفصل، والمجموعة الملتحمة بمسافة غير قابلة للفصل التي كانت أعرض من العمود فقُسمت عند تلك المسافة تستعيد مسافتها غير القابلة للفصل (Capítulo XVIIIلاCapítuloXVIII)، وكل فاصل آخر يعيد مسافته الواحدة. حتى postext 1.4 كان كل فاصل سطر يصبح مسافة، فكان العنوان الملتف عند واصلة يطبعWord- Book، وكان الفاصل القسري في مثل هذا العنوان يُحذف.{number}— الرقم المنسَّق وفقnumberingTemplateالخاص بالمستوى.{numberDecimal},{numberRoman},{numberRomanLower},{numberAlpha},{numberAlphaLower}— عدّاد العنوان (العدّ الجاري لمستواه، أيًا كان ما يطبعه القالب) بصيغ أعداد أخرى: الفصل الثالث يعطي3وIIIوiiiوCوc، معnumberingTemplateأو بدونه. العنوان غير المرقّم (نمط فيهnumbered: false) يتركها فارغة.{numberWords},{numberWordsLower},{numberOrdinalWords},{numberOrdinalWordsLower}— العدّاد نفسه مكتوبًا بالحروف، بحرف أول كبير أو بأحرف صغيرة: Three / three، Third / third (انظر الأعداد المكتوبة بالحروف).{numberHan}— العدّاد نفسه بالأرقام الصينية، بنظام الكتابة الذي يحددهlocaleالخاص بالمستند: الافتتاحية المضبوطة بالقالب第{numberHan}回تطبع 第十二回 للفصل الثاني عشر بينما يُدرج جدول المحتويات12.{chapterNumber},{chapterTitle},{pageNumber},{totalPages},{bookTotalPages},{title},{subtitle},{author},{publishDate}— عناصر نائبة مشتركة للبيانات الوصفية.{attr.<key>}— سمة مكتوبة على سطر العنوان نفسه (# Title {author="I. Zango Martín"})، وإلا فسمة H1 للفصل الحالي. السمات المفقودة تُحلّ إلى سلسلة فارغة دون تحذير.
يطبع {chapterNumber} ما تطبعه الترويسات للفصل: رقم H1 حين يكون لمستواه (أو لنمطه) numberingTemplate، وإلا فترتيب الفصل — 1، 2… مستمرًا بعد الفصول التي أُخرجت قبل هذا الفصل — ولا شيء لفصل غير مرقّم أو لنمط قالبه ''. يقرأ تصميم العنوان الفصل الذي ينتمي إليه عنوانه: عنوان المستوى 1 يقرأ فصله، والعنوان الأدنى يقرأ آخر عنوان من المستوى 1 قبله. ويصح ذلك أيضًا حيث يلتقي فصلان في صفحة واحدة، بينما تطبع ترويسات تلك الصفحة الفصل اللاحق. ويُقاس الارتفاع الذي تحجزه الافتتاحية بالقيمة نفسها. حتى postext 1.4 كان يُقاس ببادئة رقم العنوان (الفارغة دون قالب)، فقد يُرسم التصميم الذي يعتمد ارتفاعه على {chapterNumber} أطول من المتسع الذي أخذه؛ وكان التصميم يطبع فصل الصفحة، فكان أول فصلين يتشاركان صفحة يُظهر رقم الثاني.
تتبع العناصر النائبة للعدّاد الكتابَ عبر الفصول التي تُخرج واحدًا تلو الآخر (العدّادات التي تمررها continuationAfter())، وتعيد السمة startAt في العنوان بدء عدّها (انظر صيغة المستند › سمات العناوين). افتتاحية تقول Chapter III فوق عنوان مرقّم 3. في التدفق وفي جدول المحتويات:
{
level: 1,
span: 'page',
numberingTemplate: '{1}.',
advancedDesign: {
enabled: true,
slot: { elements: [
{ kind: 'text', id: 'label', content: 'Chapter {numberRoman}', fontSize: { value: 10, unit: 'pt' }, overflow: 'ellipsis-end',
placement: { anchor: { to: 'container', edge: 'top-left' }, size: { width: 'fill', height: 'auto' } } },
{ kind: 'text', id: 'title', content: '{titleText}', fontSize: { value: 24, unit: 'pt' }, overflow: 'wrap',
placement: { anchor: { to: '#label', edge: 'below' }, size: { width: 'fill', height: 'auto' } } },
] },
},
}الارتفاع المحجوز
يشغل العنوان ذو التصميم المتقدم مكانًا في العمود كأي كتلة: يبدأ النص الأساسي بعده تحت ذلك المكان. والمكان هو أكبر ثلاثة ارتفاعات، تُقاس كلها نزولًا من أعلى العنوان (من أعلى منطقة المحتوى في افتتاحية تبدأ صفحتها):
- نص العنوان نفسه، مضبوطًا بطباعة المستوى (يكون مخفيًا تحت التصميم، لكنه يحتفظ بأسطره)؛
- أسفل محتوى التصميم — أدنى حافة سفلية بين العناصر المحتسبة (أدناه)؛
advancedDesign.minHeight.
يُضاف marginBottom الخاص بالعنوان بعده (من نمط عنوانه أو مستواه أو headings.marginBottom؛ 0.5 em افتراضيًا)، ويُلتقط الناتج صعودًا إلى شبكة خطوط الأساس حين تلتقط العناوين الشبكة. في الافتتاحية (span: 'page') يُترك الشريط نفسه خاليًا في كل عمود من الصفحة. وفي العنوان داخل العمود الذي يتسع له عموده، يُخرج التصميم في صندوق العنوان، الذي يكون بهذا الارتفاع بالضبط.
أي العناصر تُحتسب. كل عنصر في التصميم يُحتسب — النصوص والخطوط والصناديق والصور على السواء (حتى postext 1.4 لم يكن عنصر image يُحتسب أبدًا، فكان النص قد يبدأ فوق صورة الشريط ما لم يُبعده minHeight) — باستثناء:
- العناصر التي فيها
reserve: false— زخرفة يجوز أن تقع تحت النص؛ - العناصر التي تتبع الشريط نفسه — المثبتة إلى الصف الأوسط من الحاوية (
left،center،right) أو صفها السفلي (bottom-left،bottom،bottom-right)، والتي ارتفاعها'fill'مقابل الحاوية (الصندوق أو الخط العمودي الذي لا ارتفاع له يملؤها افتراضيًا)، وأي عنصر مثبت إلى أحدها. الحاوية هي الشريط المحجوز، فهذه العناصر تستقر على قاعدته أو تمتد عبره: خط تحت الشريط، أو لوحة ملوّنة خلف العنوان. إنها تتبع الارتفاع ولا تحدده أبدًا. والنص بينها الذي يحتفظ بارتفاعه الخاص ما زال يحتاج إلى متسع: نص العنوان المثبت إلى قاعدة الشريط يجعل الشريط بطول ذلك النص على الأقل، فلا يبدأ أبدًا فوق أعلى العنوان. معminHeight: 36mmونص العنوان مثبتbottom-left، يكون صندوق العنوان 36 مم مضافًا إليهاmarginBottomالخاص به، مقرَّبًا صعودًا إلى الشبكة، ويقع نص العنوان عند قاعدة ذلك الصندوق، فوق النص التالي مباشرة؛ ومن دونminHeight، فإن نص العنوان الذي يأخذ أسطرًا أكثر من نص العنوان المخفي يجعل الشريط بعمق ذلك النص. حتى postext 1.4 كان مثل هذا النص يُرسم صاعدًا فوق النص الذي يسبق العنوان. الصناديق والخطوط والصور التي تتبع الشريط لا تضع حدًا أدنى كهذا، فاللوحة المثبتة إلى القاعدة يمكن أن تمتد فوق العنوان.
العناصر المثبتة إلى الصفحة أو النزف تُحتسب بقدر امتدادها تحت أعلى العنوان. الشريط الممتد عبر أعلى الصفحة الذي ينتهي فوق العنوان لا يُحتسب منه شيء؛ والصورة الممتدة إلى النزف الكامل التي تتجاوزه تدفع النص إلى أسفل حتى حافتها السفلى. وكذلك أي شيء يقع منخفضًا في الصفحة: الختم على بعد 25 مم فوق أسفل الصفحة، أو الشريط الجانبي بطول الصفحة، أو الإطار، يحجز الصفحة حتى حافته السفلى، وعادة يبدأ النص في الصفحة التالية. علّم مثل هذه الزخرفة بالقيمة reserve: false — فهي تظل تُرسم ويمكن للعناصر أن تُثبت إليها — وأعطِ العنوان المتسع الذي يحتاجه بعناصره النصية أو بالقيمة minHeight:
{
"kind": "image", "id": "seal", "resourceId": "seal", "reserve": false,
"placement": {
"anchor": { "to": "page", "edge": "bottom-right" },
"offset": { "x": { "value": -25, "unit": "mm" }, "y": { "value": -25, "unit": "mm" } },
"size": { "width": { "value": 30, "unit": "mm" } }
}
}العنصر المثبت إلى عنصر لا يحجز مكانًا يظل يُحتسب ما لم يُعلَّم هو أيضًا (التعليق المثبت إلى الختم يحتاج إلى reserve: false خاص به).
أين يُرسم. يُرسم تصميم الافتتاحية (span: 'page') قبل المتن، فالزخرفة التي لا تحجز شيئًا تقع تحت النص. أما تصميم العنوان داخل العمود فيُرسم مع كتلة العنوان — فوق الكتل التي تسبقه في العمود، وتحت الكتل التي تليه — حيثما وُضع فوق قاعدة عموده: في الهوامش الجانبية، والهامش العلوي، والنزف، والأعمدة المجاورة أيضًا. تقصّه قاعدة العمود، في Canvas وفي PDF، لأن التدفق ينتهي هناك (انظر أطول من العمود أدناه). حتى postext 1.4 كانت Canvas وPDF تقصّانه أيضًا عند أعلى عموده، فكان الشريط المثبت إلى أعلى الصفحة أو النزف يمتد إلى الهوامش الجانبية لكنه يتوقف عند الهامش العلوي. الزخرفة المثبتة إلى أسفل الصفحة مكانها الافتتاحية، أو تصميم التذييل مع pages: 'opener'.
أطول من العمود. حين يمتد المتسع إلى ما بعد قاعدة العمود — minHeight بطول الصفحة، أو صورة أو إطار يمتد حتى حدّ القص — يستحوذ العنوان على بقية صفحته (الافتتاحية، في كل عمود) أو عموده (العنوان داخل العمود)، ويبدأ النص الذي بعده في الصفحة أو العمود التالي. عندئذ تمتد كتلته حتى قاعدة العمود، ولا تُقصّ أبدًا إلى ارتفاع نصها، فيكون الشريط الذي يُخرج تصميمه مقابله هو المتسع الذي يستحوذ عليه (بما في ذلك minHeight، حتى القاعدة). التصميم الأطول من الصفحة نفسها — نص تمهيدي طويل في صفحة شاشة صغيرة — يظل يُقصّ عند أسفل الصفحة (والتصميم داخل العمود عند أسفل عموده): يُدرجه Sandbox في لوحة الفحوص باسم تصميم العنوان مقطوع. لا يصدر الإخراج تحذيرًا بشأنه — فالغلاف الذي يستحوذ على صفحته حالة معتادة ولا يفقد شيئًا — لكن أي مضيف يمكنه إجراء الفحص نفسه على الإخراج النهائي: تعيد collectHeadingDesignCuts(doc) كائنًا { kind: 'headingDesignCut', pageIndex, level, where, overflowPx, sourceStart, sourceEnd } لكل عنوان يقع نص تصميمه بعد أسفل حدّ قص الصفحة (where: 'page'، افتتاحية) أو بعد أسفل عموده (where: 'column')، وتصف formatWarning كلًا منها. انظر افتتاحية بطول الصفحة ضمن الامتداد والتصميم المتقدم.
العمود الجانبي. في تخطيط العمود ونصف الذي يحمل عموده الجانبي العناصر العائمة (sideColumnRole: 'floats')، فإن عنصر تصميم عنوان داخل العمود الذي يقف في العمود الجانبي — رقم فصل مثبت إلى الصفحة في عمود الهامش الخارجي لافتتاحية كتاب مدرسي — يُبعد عنه مكدّس العناصر الجانبية. كل شكل أو جدول أو إطار span: 'side' تضعه الصفحة بعد العنوان يترك فجوة عنصر عائم واحدة بينه وبين كل عنصر من هذا النوع: يبقى حيث يضعه المكدّس حين يتسع فوق العنصر، وإلا ذهب تحته — أو انتظر الصفحة التالية حين لا تتسع له بقية العمود الجانبي هناك. وهكذا فإن رقمًا عند رأس الهامش يُبقي المكدّس كله تحته، بينما رقم قسم معلّق في الهامش بجانب عنوان أدنى في الصفحة يترك رأس الهامش للأشكال التي تحيل إليها الصفحة (يبقى الشكل الهامشي في أعلى صفحته). ما يحمله العمود الجانبي بالفعل عند وضع العنوان لا يُحرّك: الشكل المكدّس سابقًا في الصفحة والذي يمتد حتى عنصر العنوان يبقى حيث هو، تحت العنصر، لذا فإن التصميم الذي يقف عنصره في العمود الجانبي في منتصف الصفحة يحتاج إلى أن تأتي الإحالة إلى أشكاله بعد العنوان. العناصر التي فيها reserve: false تترك العمود الجانبي حرًا، كما تترك النص. الافتتاحية (span: 'page') لا تحتاج إلى شيء من هذا: شريطها محجوز في كل عمود، بما فيه العمود الجانبي. حتى postext 1.4 كان الشكل الجانبي المحال إليه في مثل هذه الافتتاحية يوضع عند رأس العمود الجانبي، فوق الرقم.
الأغلفة. عنوان الغلاف الذي يملأ صفحته — minHeight بطول الصفحة، أو صورة بحجم الصفحة الكاملة في تصميمه — يرسل إذن النص الذي بعده إلى الصفحة التالية بنفسه، في التخطيطات ذات العمود الواحد والأعمدة المتعددة على السواء. والتوجيه :::pagebreak بعده مباشرة اختياري ولا ضرر منه: فاصل الصفحة في صفحة ما زالت فارغة لا يفعل شيئًا، فلا يضيف صفحة فارغة أبدًا. لا يُحتاج إليه إلا حين يتوقف تصميم الغلاف قبل أسفل الصفحة ويُراد مع ذلك أن يبدأ النص في صفحة جديدة.
# Annual report 2026 {style="cover"}
:::pagebreak
# Letter from the chair#الأعداد المكتوبة بالحروف
لاحقتان من لواحق قالب الترقيم تكتبان العدّاد بالكلمات، بلغة المستند (locale في المستوى الأعلى، وإلا فلغة تقسيم الكلمات — انظر لغة المستند): words للعدد الأصلي وordinal للعدد الترتيبي. حالة أحرف اللاحقة تحدد حالة أحرف الكلمات، كما تفعل A / a للحروف:
| الرمز | الإنجليزية (21) | الإسبانية (21) | الصينية (21) |
|---|---|---|---|
| twenty-one | veintiuno | 二十一 |
| Twenty-one | Veintiuno | 二十一 |
| TWENTY-ONE | VEINTIUNO | 二十一 |
| twenty-first | vigesimoprimero | 第二十一 |
| Twenty-first | Vigesimoprimero | 第二十一 |
| TWENTY-FIRST | VIGESIMOPRIMERO | 第二十一 |
تُكتب الأعداد بالكلمات في الإنجليزية والإسبانية والصينية والعربية؛ وأي لغة أخرى تأخذ الكلمات الإنجليزية، كما تفعل نصوص استمرار الجداول المدمجة. تكتب الصينية الأرقام غير الرسمية في simp-chinese-informal أو trad-chinese-informal، بحسب نظام الكتابة في locale (一万 / 一萬)، مع 第 قبل العدد الترتيبي؛ ولا حالة أحرف للحروف الصينية، فتطبع الكتابات الثلاث للاحقة النتيجة نفسها. تتبع الإنجليزية الاستعمال الأمريكي (one hundred five، بلا and). وتستخدم الإسبانية صيغ المذكر، كما يُرقَّم capítulo أو libro (capítulo primero، tercero، veintiuno)، وتكتب الأعداد الترتيبية من 13 إلى 29 كلمة واحدة، كما تفضّل الأكاديمية الملكية الإسبانية (RAE) (decimotercero، vigesimoprimero). تُكتب الأعداد الأصلية بالحروف حتى 999 999 والترتيبية الإسبانية حتى 999؛ وتُطبع الأعداد الأكبر بالأرقام.
تتطابق الأعداد العربية في التذكير والتأنيث مع المعدود، لذا تأخذ اللاحقة مُعدِّلًا: -feminine (أو -f) للمعدود المؤنث، و-masculine (-m، الافتراضي) للمذكر؛ و-classical تكتب المئات بصيغة مائة، كما تفعل طبعات بولاق ومعظم الطبعات المصرية، بدل صيغة مئة الحديثة. يكتب {1:ordinal} العدد الترتيبي المعرّف المرفوع الذي يستخدمه العنوان: الفصل {1:ordinal} يعطي الفصل الأول، الفصل الحادي عشر، الفصل الحادي والعشرون؛ والليلة {1:ordinal-feminine} يعطي الليلة الأولى، الليلة الحادية عشرة، الليلة الحادية والعشرون، الليلة المئتان، وفوق المئة صيغة «بعد» الكلاسيكية: الليلة الخامسة والأربعون بعد الثلاثمئة، الليلة الحادية بعد الألف. ويكتب {1:words} العدد الأصلي (واحد وعشرون؛ والمؤنث إحدى عشرة، واحدة وعشرون). تُكتب الأعداد الترتيبية حتى 9 999 والأصلية حتى 99 999؛ ويمكن الجمع بين المعدّلات ({1:ordinal-f-classical})، وتتجاهلها اللغات الأخرى. لا أحرف كبيرة في العربية، فلا تغيّر حالة أحرف اللاحقة شيئًا. العنصران النائبان للتصميم {numberWords} و{numberOrdinalWords} يكتبان صيغ المذكر؛ ولافتتاحية بصيغة المؤنث، ضع العدد الترتيبي في قالب المستوى واطبعه بالعنصر {number}.
في تصميم العنوان، يكتب {numberWords} / {numberWordsLower} و{numberOrdinalWords} / {numberOrdinalWordsLower} عدّاد العنوان بالطريقة نفسها، فيمكن أن تقول الافتتاحية Chapter One بينما يُدرج جدول المحتويات 1. وتعطي textTransform: 'uppercase' في العنصر النصي الأحرف الكبيرة:
// Spanish novel: "CAPÍTULO PRIMERO" above the title, "1." in the contents.
{ level: 1, numberingTemplate: '{1}.', span: 'page',
advancedDesign: { enabled: true, slot: { elements: [
{ kind: 'text', id: 'n', content: 'Capítulo {numberOrdinalWordsLower}', textTransform: 'uppercase', /* … */ },
{ kind: 'text', id: 't', content: '{titleText}', /* … */ },
] } } }#القوائم غير المرقّمة
تتحكم الخاصية unorderedLists في طريقة إخراج القوائم النقطية (-، *، +) وقوائم المهام بصيغة GFM (- [ ]، - [x]). يمكن أن تتداخل القوائم حتى خمسة مستويات.
#القيم الافتراضية للقوائم غير المرقّمة
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
fontFamily | string | ترث bodyText.fontFamily | الخط المستخدم لنص البند. |
color | ColorValue | اللون الرئيسي (#295AA3) | لون نص البنود ونقاطها. مربوط بالمُدخل main-color في لوحة الألوان الافتراضية. |
fontWeight | number | 700 | وزن خط نص البند (100–900). ترث النقاط هذا الوزن ما لم يُتجاوز في مستوى بعينه. |
italic | boolean | false | يُخرج نص البند بخط مائل. |
bulletChar | string | '•' | الرمز (glyph) المستخدم علامةً للنقطة. |
bulletFontSize | Dimension | 1 em | حجم رمز النقطة. تتغير الوحدات النسبية بتغير حجم خط المتن. |
gap | Dimension | 0.5 em | المسافة الأفقية بين النقطة ونص البند. |
indent | Dimension | 0 em | الإزاحة الأساسية للمستوى 1. تتسلسل المستويات الأعمق من موضع بداية نص المستوى الأب ما لم تُتجاوز (انظر أدناه). |
bulletVerticalOffset | Dimension | 0 em | ضبط دقيق للموضع الرأسي للنقطة. القيم السالبة ترفع النقطة، والموجبة تخفضها. |
marginTop / marginBottom | Dimension | 1.5 em | المسافة قبل القائمة كلها وبعدها. |
itemSpacing | Dimension | 0 em | مسافة رأسية إضافية تُدرج بين البنود فوق ارتفاع السطر. حول قائمة متداخلة في بند من قائمة أخرى، تُطبَّق مسافة القائمة الخارجية على الجانبين: قبل البند الأول من القائمة المتداخلة وبعد آخر بند فيها (حتى postext 1.4 كان البند التالي لقائمة متداخلة يأخذ مسافة القائمة المتداخلة). |
snapTopToGrid | boolean | false | يقرّب المسافة فوق القائمة إلى الأعلى كي تستقر نقطتها الأولى على شبكة خطوط الأساس، كما يفعل النص تحت العنوان؛ وتصبح marginTop حينئذ حدًّا أدنى. نهاية القائمة تعيد التدفق إلى الشبكة في الحالتين، فإذا كانت itemSpacing تساوي 0 اصطفّ كل بند مع النص في العمود المجاور. معطّل افتراضيًا، كما كان حتى postext 1.4: قيمة marginTop التي لا تساوي عددًا صحيحًا من الأسطر تُبقي البنود خارج الشبكة حتى نهاية القائمة. لا يتأثر بذلك القوائم داخل الإطارات (callout)، إذ إن دواخلها خارج الشبكة أصلًا. |
hangingIndent | boolean | true | عند تفعيله، تصطف الأسطر الملتفّة مع أول حرف من النص بدلًا من أن تبدأ تحت النقطة. |
levels | UnorderedListLevelConfig[] | — | تجاوزات لكل عمق من المستويات 1–5. انظر أدناه. |
#امتدادات قوائم المهام
تُخرَج بنود المهام بصيغة GFM (- [ ] …، - [x] …) بنودًا غير مرقّمة يحل فيها رمز مربع اختيار محل النقطة. الحقول الآتية لا تنطبق إلا على بنود المهام:
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
taskCheckboxChar | string | '☐' | الرمز المستخدم للمهام غير المنجزة. |
taskCheckedChar | string | '☑' | الرمز المستخدم للمهام المنجزة. |
taskCompletedStrikethrough | boolean | true | يرسم خط شطب عبر نص المهام المنجزة. |
taskCompletedColor | ColorValue | يرث لون البند | لون اختياري يُطبَّق على نص المهام المنجزة. إذا أُغفل استُخدم لون البند المعتاد. |
#تجاوزات كل مستوى في القوائم غير المرقّمة
كل مُدخل في levels يستهدف عمقًا واحدًا (1–5) ويمكنه أن يتجاوز أيًّا مما يلي:
| الخاصية | النوع | الوصف |
|---|---|---|
bulletChar | string | رمز النقطة لهذا العمق. |
fontFamily | string | عائلة خط البند لهذا العمق. |
fontSize | Dimension | حجم رمز النقطة لهذا العمق. |
color | ColorValue | لون البند. |
fontWeight | number | وزن خط البند. |
italic | boolean | تفعيل الخط المائل أو تعطيله. |
indent | Dimension | إزاحة صريحة للنقطة في هذا العمق. انظر قاعدة التسلسل أدناه. |
verticalOffset | Dimension | ضبط رأسي دقيق للنقطة في هذا العمق. |
تسلسل الإزاحة. يبدأ المستوى 1 دائمًا عند قيمة indent العامة (وهي افتراضيًا 0 em، أي أن النقاط مثبّتة عند حافة العمود). في المستويات 2–5، إذا تركت indent دون تعريف وضع المحرّك النقطة عند موضع بداية نص المستوى السابق (إزاحة الأب + عرض النقطة + gap). عيّن indent صريحة لمستوى ما لتقطع التسلسل وتثبّت ذلك العمق حيث تشاء.
unorderedLists: {
bulletChar: '—',
gap: { value: 0.4, unit: 'em' },
hangingIndent: true,
levels: [
{ level: 2, bulletChar: '·' },
{ level: 3, bulletChar: '◦', color: { hex: '#666666', model: 'hex' } },
],
}#القوائم المرقّمة
تتحكم الخاصية orderedLists في القوائم المرقّمة (1.، 2)، إلخ). يمكن أن تتداخل القوائم حتى خمسة مستويات، ويمكن لكل عمق أن يستخدم تنسيق ترقيم مختلفًا.
#القيم الافتراضية للقوائم المرقّمة
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
fontFamily | string | ترث bodyText.fontFamily | الخط المستخدم لنص البند ولعلامة الرقم. |
color | ColorValue | اللون الرئيسي (#295AA3) | لون نص البنود وعلامات أرقامها. مربوط بالمُدخل main-color في لوحة الألوان الافتراضية. |
fontWeight | number | 700 | وزن خط نص البند وعلامات الأرقام (100–900). |
italic | boolean | false | يُخرج نص البند بخط مائل. |
numberFormat | OrderedListNumberFormat | 'arabic' | نمط الترقيم: 'arabic'، 'lower-alpha'، 'upper-alpha'، 'lower-roman'، 'upper-roman'. تعمل كذلك الكتابات المستخدمة في الإعدادات الأخرى ('decimal'، 'roman-lower'، 'i'…؛ انظر صيغ كتابة تنسيق الترقيم)؛ والقيمة غير المعروفة تُرقّم بالأرقام العربية (0–9) ويُبلَّغ عنها. |
prefix | string | '' | نص يوضع قبل الرقم، بنمط الفاصل: مع '(' هنا و')' فاصلًا تُقرأ القائمة الصينية (一)، (二). وحين يُرسم الفاصل مقطعًا مستقلًا، تُرسم البادئة مقطعًا مستقلًا كذلك، قبل الرقم مباشرة. |
separator | string | '.' | الحرف الموضوع بين الرقم والنص، وهو عادةً '.' أو ')'. |
separatorFontFamily | string | ترث fontFamily | خط الفاصل. إذا اختلف أي جانب من نمط الفاصل عن نمط الرقم، رُسم الفاصل مقطعًا مستقلًا بعد الرقم (المحاذى إلى اليمين)، مثل 1 بخط Optima Bold أسود يليه • بخط DIN Pro Bold أزرق. |
separatorFontWeight | number | ترث fontWeight | وزن خط الفاصل (100–900). |
separatorItalic | boolean | ترث italic | يُخرج الفاصل بخط مائل. |
separatorColor | ColorValue | ترث color | لون الفاصل. تُحترم الإشارات إلى لوحة الألوان. |
separatorGap | Dimension | 0 em | المسافة بين الرقم والفاصل. يبقى نص البند يبدأ على بُعد gap بعد الفاصل. |
numberFontSize | Dimension | 1 em | حجم علامة الرقم. |
gap | Dimension | 0.5 em | المسافة الأفقية بين الرقم ونص البند. |
indent | Dimension | 0 em | الإزاحة الأساسية للمستوى 1؛ تتسلسل المستويات الأعمق من موضع بداية نص المستوى الأب ما لم تُتجاوز. |
numberVerticalOffset | Dimension | 0 em | ضبط دقيق للموضع الرأسي لعلامة الرقم. |
marginTop / marginBottom | Dimension | 1.5 em | المسافة قبل القائمة كلها وبعدها. |
itemSpacing | Dimension | 0 em | مسافة رأسية إضافية بين البنود. حول قائمة متداخلة في بند من قائمة أخرى، تُطبَّق مسافة القائمة الخارجية على الجانبين: قبل البند الأول من القائمة المتداخلة وبعد آخر بند فيها (حتى postext 1.4 كان البند التالي لقائمة متداخلة يأخذ مسافة القائمة المتداخلة). |
snapTopToGrid | boolean | false | يقرّب المسافة فوق القائمة إلى الأعلى كي يستقر رقمها الأول على شبكة خطوط الأساس، كما يفعل النص تحت العنوان؛ وتصبح marginTop حينئذ حدًّا أدنى. نهاية القائمة تعيد التدفق إلى الشبكة في الحالتين، فإذا كانت itemSpacing تساوي 0 اصطفّ كل بند مع النص في العمود المجاور. معطّل افتراضيًا، كما كان حتى postext 1.4: قيمة marginTop التي لا تساوي عددًا صحيحًا من الأسطر تُبقي البنود خارج الشبكة حتى نهاية القائمة. لا يتأثر بذلك القوائم داخل الإطارات (callout)، إذ إن دواخلها خارج الشبكة أصلًا. |
numberWidth | 'run' | 'level' | 'run' | عرض عمود الرقم في البند، وهو ما يحدد موضع بداية نصه؛ وتُصفّ الأرقام فيه محاذاةً إلى اليمين. 'run': أعرض رقم في السلسلة التي ينتمي إليها البند، أي بنود العمق الواحد التي لا يفصل بينها إلا بنود أعمق. الشكل أو الفقرة أو الإطار بين بندين يبدأ سلسلة جديدة، فقد يبدأ نص ii) بعد جدول أبعد قليلًا إلى اليمين من نص i) قبله، وتبدأ قائمة من تسعة بنود نصها إلى يسار نص قائمة من اثني عشر بندًا. 'level': أعرض رقم في عمق البند في المستند كله (الفصل، في الكتاب)، فتبدأ كل قائمة، وكل جزء من قائمة متقطعة، نصها في الموضع نفسه، كما تفعل أصلًا إزاحات المستويات الأعمق. |
hangingIndent | boolean | true | تصطف الأسطر الملتفّة مع أول حرف من النص بدلًا من أن تبدأ تحت الرقم. |
levels | OrderedListLevelConfig[] | — | تجاوزات لكل عمق من المستويات 1–5. |
#تجاوزات كل مستوى في القوائم المرقّمة
يمكن لكل مُدخل في levels أن يتجاوز numberFormat وprefix وseparator وfontFamily وfontSize وcolor وfontWeight وitalic وindent وverticalOffset ونمط الفاصل (separatorFontFamily، separatorFontWeight، separatorItalic، separatorColor، separatorGap)، وتنطبق قاعدة تسلسل الإزاحة نفسها المطبقة على القوائم غير المرقّمة. يرث نمط الفاصل في مستوى ما نمط الرقم في المستوى نفسه، ما لم يُعطَ إعداد الفاصل على مستوى القائمة كلها.
المحاذاة إلى اليمين. يقيس المسار أعرض رقم منسَّق داخل السلسلة ويزيح كل بنود تلك السلسلة كي تصطف علامات الأرقام على حافتها اليمنى. في قائمة من عشرة بنود تُخرج من 1. إلى 10.، تُحشى الأرقام ذات الخانة الواحدة من جهة اليمين كي يبقى الفاصل في العمود نفسه.
orderedLists: {
numberFormat: 'arabic',
separator: '.',
levels: [
{ level: 2, numberFormat: 'lower-alpha' },
{ level: 3, numberFormat: 'lower-roman', separator: ')' },
],
}ينتج عن ذلك المزيج المتداخل المألوف:
1. First item
a. Sub-item
i) Deep note
b. Sub-item
2. Second item
تراتبية المستندات الصينية (GB/T 15834—2011، الملحق B.3) تتألف من خمسة مستويات: 一、 ثم (一) ثم 1. ثم (1) ثم ①:
orderedLists: {
levels: [
{ level: 1, numberFormat: 'simp-chinese-informal', separator: '、' },
{ level: 2, numberFormat: 'simp-chinese-informal', prefix: '(', separator: ')' },
{ level: 3, numberFormat: 'arabic', separator: '.' },
{ level: 4, numberFormat: 'arabic', prefix: '(', separator: ')' },
{ level: 5, numberFormat: 'circled-decimal', separator: '' },
],
}#الصيغ الرياضية
تتحكم الخاصية math في طريقة تحليل صيغ LaTeX وإخراجها، سواء داخل المحدِّدين $...$ (ضمن السطر) أو $$...$$ (صيغة منفصلة، display). المحرّك الذي تقوم عليه هو MathJax (الحزمة mathjax-full) في وضع الإخراج SVG، ويُحوَّل إلى صورة نقطية في Canvas ويُضمَّن في PDF رموزًا قابلة للتحجيم.
interface MathConfig {
enabled?: boolean; // Render LaTeX. When false, spans pass through as literal TeX.
fontSizeScale?: number; // × the surrounding text's size (the body size for display maths).
color?: ColorValue; // Formula colour; inherits body colour if omitted.
marginTop?: Dimension; // Space above display math blocks.
marginBottom?: Dimension; // Minimum space below; baseline grid snap may enlarge it.
indentAfterDisplay?: boolean; // Indent a paragraph that follows a display formula.
keepWithLeadIn?: boolean; // Keep a display formula with the line that leads into it.
}| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
enabled | boolean | true | عند القيمة false تبقى مقاطع $...$ و$$...$$ تُحلَّل (فتظل تحذيرات المحدِّدات غير المغلقة تُطلق)، لكنها تُخرج بنص TeX المصدري كما هو. يفيد ذلك حين يحتوي المحتوى على علامات الدولار عمدًا، أو حين تريد تعطيل إخراج الرياضيات تمامًا. |
fontSizeScale | number | 1.0 | معامل يُضرب في حجم النص المحيط قبل الإخراج: الـ em الواحد من خط TeX في الصيغة يساوي ذلك الحجم × fontSizeScale، والحجم هو bodyText.fontSize للصيغة المنفصلة وللرياضيات ضمن السطر في نص المتن، وحجم الكتلة الحاوية للرياضيات ضمن السطر في عنوان أو نمط فقرة أو تعليق أو متن إطار. القيمة 1.0 تطابق النص المحيط؛ والقيم بين 0.9 و1.1 شائعة حين يبدو خط الرياضيات أكبر قليلًا أو أصغر قليلًا من خط النثر. تغيّر في postext 1.5: حتى 1.4 كانت الصيغ تخرج أكبر من ذلك بنحو 13% (انظر أدناه). |
color | ColorValue | يرث لون المتن | لون الصيغة المُخرجة. أغفله ليرث bodyText.color. عيّنه صراحةً حين تريد للصيغ لونًا مختلفًا عن النثر، كأن يطابق لون إبراز العناوين. |
marginTop | Dimension | 0.8em | المسافة فوق كتلة الصيغة المنفصلة. تُهمل في الرياضيات ضمن السطر. |
marginBottom | Dimension | 0.8em | المسافة تحت كتلة الصيغة المنفصلة. وهي حد أدنى: قد يمدّها الالتصاق بالشبكة كي يقع خط الأساس التالي على أحد خطوط الشبكة (سواء رسمت page.baselineGrid الشبكة أم لا). |
indentAfterDisplay | boolean | true | يزيح السطر الأول من الفقرة التي تلي صيغة منفصلة، كأي فقرة أخرى. أما false فتجعل كل فقرة تلي صيغة منفصلة مباشرةً بلا إزاحة، بوصفها تتمة للجملة التي قطعتها الصيغة ("where L is…"). والصيغة المكتوبة داخل فقرة، بلا سطر فارغ فوقها ولا تحتها، يليها دائمًا نص بلا إزاحة: النص الواقع تحت $$ الختامية يتابع تلك الفقرة ولا يُزاح أبدًا (انظر الصيغ الرياضية). |
keepWithLeadIn | boolean | false | يُبقي الصيغة المنفصلة في عمود السطر الذي يمهّد لها، وهو ما يسميه TeX جزاء ما قبل الصيغة (predisplay penalty). حين لا تتسع الصيغة تحت السطر الأخير من الفقرة التي قبلها، ينتقل ذلك السطر مع الصيغة إلى العمود أو الصفحة التالية؛ وحين يكون عدد الأسطر المتروكة خلفه أقل من bodyText.widowMinLines (أقل من سطر واحد حين يكون bodyText.avoidWidows معطّلًا)، تنتقل الفقرة التي تبدأ في ذلك العمود كاملةً (مع العناوين التي تختم العمود فوقها، بحسب headings.keepWithNext). يقف السطر المنقول وحده في رأس العمود التالي، أيًّا كان ما تطلبه قاعدة الأسطر اليتيمة (orphans). ومع false تنتقل الصيغة وحدها، وقد يختم السطرُ الذي يقدّمها العمودَ السابق، أو يقع فوق شكل يتصدر العمود التالي. |
math: {
enabled: true,
fontSizeScale: 1.0,
color: { hex: '#295AA3', model: 'hex' },
marginTop: { value: 1, unit: 'em' },
marginBottom: { value: 1, unit: 'em' },
}حجم الصيغ، تغيّر في postext 1.5. يعطي MathJax صندوق الصيغة بوحدة ex، والـ ex الواحد من خط TeX فيها يساوي 0.442 em. حتى postext 1.4 كان المحرّك يعدّه نصف em، فكانت كل صيغة تُنضَّد أكبر بنحو 13% من bodyText.fontSize × fontSizeScale. صارت الصيغ الآن تخرج بالحجم الموثَّق، فيُعاد تدفق الأسطر والصفحات التي فيها رياضيات. والإعدادات المكتوبة برمجيًا للإصدار 1.4 تحتفظ بحجم صيغ 1.4 إذا مُرّرت عبر pinLegacyMathSize من postext/bundle، مرة واحدة، كما كُتبت:
import { pinLegacyMathSize } from 'postext/bundle';
config = pinLegacyMathSize(config); // formulas, and the space around display formulas, as 1.4 set themوثمة تغييرات أخرى في القواعد في 1.5 قد تحرّك صفحاتها أيضًا: فواصل أسطر العناوين، والمسافة حول الشكل ضمن السطر (في النص الجاري وفي الإطارات)، والعلامات المضمَّنة في عناوينها، وحجم حروفها الاستهلالية، والحيّز المحجوز تحت سطر ينتهي بنقطتين للقائمة التي يقدّمها، والأسطر التي يتركها قطع الإطار من فقرة أو بند قائمة، وفواصل الأسطر بعد الشَّرطة، وتقسيم أسطر النص غير المضبوط، وانقسام الفقرة تحت العنوان، وفواصل الأسطر بعد واصلة الكلمة المركّبة، والمسافة تحت الحاوية :::paragraphs. الدالة migrateConfig، إذا شُغّلت مرة واحدة مع نص Markdown الذي يُخرجه الإعداد، تثبّت ما يحتاجه هذا النص من تلك التغييرات، بما فيها حجم الصيغ (انظر الحزم التي كتبها postext 1.4 أو ما قبله):
import { migrateConfig } from 'postext/bundle';
config = migrateConfig(config, undefined, { content: markdown }); // heading breaks, formulas, inline gaps, heading marks, drop caps, colon lines, box cuts, dash breaks, compound breaks, ragged breaking, splits under a heading and container space as 1.4 set themيمنع ذلك ما كانت تغييرات القواعد هذه ستحرّكه، لكنه لا يحفظ كل صفحة من صفحات 1.4. فالإصدار 1.5 يصلح أيضًا أخطاء في الإخراج، والإصلاح لا يُثبَّت: يحصل عليه الإعداد القديم كما يحصل عليه الجديد، فقد تتحرك صفحة يمسّها. ومن هذه الإصلاحات: العنوان الممتد عبر الصفحة بلا تصميم خاص به يُقاس على عرض الصفحة ويُنضَّد بتباعد أسطر مستواه؛ لا يُحجز شيء تحت خط أساس الحرف الاستهلالي في تصميم العنوان؛ يأخذ الحرف الاستهلالي لون لوحة القسم، ويُنضَّد حتى في نص التصميم الذي ليست قيمة overflow فيه 'wrap'، فيلتف النص حينئذ؛ الفقرة داخل الإطار تُرسم بالتتبّع الذي قيست به؛ الإطار المقسوم يحتفظ بعمود أيقونته في كل جزء منه؛ سطر الإطار الذي كانت مسافاته ستتمدد إلى أكثر من 3× يُنضَّد غير مضبوط، كما في النص الجاري؛ أداة الفقرة المرخاة لا تجعل أبدًا سطرًا مضبوطًا أعرض مما تسمح به maxWordSpacing؛ الإطار العائم يحتفظ بقيمة marginBottom (في الشريط العلوي) أو marginTop (في الشريط السفلي) إذا كانت أعرض من فجوة العناصر العائمة؛ نص التصميم المتوسّط أو المحاذى إلى اليمين الذي فيه تتبّع (كالترويسة أو عنوان صفحة الافتتاح) يوضع بحسب حروفه، دون التتبّع الذي بعد الحرف الأخير؛ تحت عنوان افتتاح ممتد عبر الصفحة، يبدأ النص الذي يفتتح العمود الثاني حيث كان سيبدأ النص الواقع مباشرة تحت عنوان الافتتاح، حتى حين يلي عنوانٌ عنوانَ الافتتاح؛ تتوقف نقاط جدول المحتويات قبل رقم الصفحة في الخط الذي يباعد بتقنين الأزواج (kerning) بين النقاط المتتالية؛ تقرأ الترويسة العنوان كما كُتب، بلا مسافة حيث ينتهي أحد أسطره بعد واصلة أو شَرطة أو داخل كلمة قُطعت لضيق العرض (MEDIOAMBIENTALES لا MEDIOAMBIENTALE S)، ويقرؤه {titleText} في تصميم العنوان بالطريقة نفسها (thousand-colour لا thousand- colour)؛ النص المثبَّت في أسفل شريط تصميم العنوان أو وسطه يُبقي الشريط طويلًا بما يكفي لاحتوائه، فلم يعد يرتفع فوق النص الذي يعلو العنوان؛ الكلمة الأعرض من سطرها، إذا قُطعت بجوار واصلة تحملها، تُقطع بعد تلك الواصلة ولا تُضاف إليها واصلة ثانية؛ البند التالي لقائمة متداخلة في أخرى يأخذ itemSpacing قائمته هو، لا قيمة القائمة المتداخلة؛ وبقية الكلمة المقطوعة لأنها أعرض من سطرها تحتفظ بمواضع القطع الخاصة بها، فيظل عنوان الويب ينقطع عند مفاصله والكلمة المركّبة عند واصلاتها، بدلًا من مقاطع القاموس.
تضرب pinLegacyMathSize المقياس في 1.1312 (0.5 ÷ 0.442) وتقسم هوامش الصيغ المنفصلة المعبَّر عنها بوحدة em على المعامل نفسه. المقياس وحده لا يكفي لكتاب فيه صيغ منفصلة: فهوامشها أطوال بمقياس حجم الصيغة نفسها، فكانت ستكبر بنسبة الـ 13% نفسها وتدفع النص الذي تحتها إلى الأسفل. وهذه هي القيم الثلاث مكتوبةً للهوامش الافتراضية:
math: {
fontSizeScale: 1.131,
marginTop: { value: 0.7072, unit: 'em' },
marginBottom: { value: 0.7072, unit: 'em' },
} // formulas as large as 1.4 set them, with the space 1.4 left around themحيث يكون الإعداد مخزَّنًا، يستطيع المحرّك أن يتعرّف على ذلك ويقوم به عنك: openBundle / readBundle لحزمة .postext كُتبت قبل 1.5 (انظر الحزم التي كتبها postext 1.4 أو ما قبله)، وSandbox للكتب ونسخة العمل وملفات postext-config.json المحفوظة آنذاك (انظر Sandbox › حفظ العمل). تُقرأ هذه عبر migrateConfig، التي تثبّت الحجم (pinLegacyMathSize): تصبح fontSizeScale مساويةً للمقياس المخزَّن (1 إن لم يُعيَّن) × 1.1312، ويُقسَم هامش الصيغة المنفصلة المعبَّر عنه بوحدة em أو rem، وهو طول بمقياس حجم الصيغة نفسها، على المعامل نفسه، فتبقى المسافة حول الصيغة المنفصلة كما تركها 1.4 (تصبح القيمة الافتراضية 0.8 em هي 0.7072 em). أما الهامش بوحدة من وحدات الصفحة (pt، mm…) فيُترك كما هو، وكذلك الإعداد الذي فيه enabled: false أو الذي ليس في كتابه أي $. عندئذ يُخرَج الكتاب القديم كما أخرجه 1.4، ويُظهر قسم math فيه الحجم الذي يُنضَّد به. ولتنضيد هذا الكتاب بحجم اليوم بدلًا من ذلك، أعد تعيين تلك القيم: الصيغ الرياضية › معامل الحجم والهامش العلوي للصيغة المعروضة والهامش السفلي للصيغة المعروضة في Sandbox، أو برمجيًا:
math: { ...config.math, fontSizeScale: 1, marginTop: undefined, marginBottom: undefined } // today's size and marginsالمعادلات المرقّمة. الصيغة المنفصلة التي فيها \tag{…} تمتد على عرض قياسها (العمود، أو العرض الداخلي للإطار الذي تقع فيه): تُوسَّط المعادلة ويُصفّ رقمها محاذىً إلى اليمين على سطر المعادلة، في كل صف موسوم من بيئة align. أما \tag*{…} فيطبع التسمية كما كُتبت، بلا أقواس. لا يُطبع رقم إلا للوسوم الصريحة؛ فالبيئات مثل equation لا تُرقَّم من تلقاء نفسها. والمعادلة المرقّمة الأعرض من قياسها تفيض إلى اليمين، كأي صيغة منفصلة. (حتى postext 1.4 لم تكن الصيغة التي فيها \tag تُرسم أصلًا.)
دالتا الحل والتجريد مماثلتان لنظيراتهما في الأقسام الأخرى:
import {
DEFAULT_MATH_CONFIG,
resolveMathConfig,
stripMathDefaults,
} from 'postext';
const resolved = resolveMathConfig(config.math);
const minimal = stripMathDefaults(config.math);#تشغيل محرّك الرياضيات
يُحمَّل MathJax عند الطلب، لا مع بقية المحرّك. حين تُجري الإخراج على الخيط الرئيسي، شغّله قبل بناء مستند فيه صيغ:
import { buildDocument, initMathEngine, renderPage } from 'postext';
await initMathEngine(); // loads MathJax once; later calls resolve at once
const doc = buildDocument({ markdown: 'Euler: $e^{i\\pi}+1=0$.' }, config);
document.body.append(renderPage(doc.pages[0], doc));- إلى أن يعمل المحرّك، تكون الصيغ عناصر نائبة. تُخرَج كل صيغة صندوقًا رماديًا بحجم تقديري. وإذا أُخرج مستند فيه رياضيات قبل أن تُستدعى
initMathEngine()ولو مرة، أظهرت وحدة التحكم تحذيرًا واحدًا بذلك. - عامل الإخراج (worker) يشغّله عنك. البناء في
postext/workerيستدعيinitMathEngine()بنفسه حين يحتوي نص Markdown على$. - أخرج الآن، ثم أعد الإخراج حين يجهز. تخبرك
isMathReady()هل المحرّك يعمل. وتستدعيonMathReady(fn)الدالةfnحين يجهز (فورًا إن كان جاهزًا) وتعيد دالة تلغي الاستدعاء. يستطيع المحرّر أن يعرض العناصر النائبة فورًا ثم يعيد الإخراج حين يصل MathJax؛ ولا يُطبع أي تحذير ما دامتinitMathEngine()قيد التنفيذ. - الإخفاق. ترفض
initMathEngine()(reject) حين يتعذر تحميل MathJax، والاستدعاء اللاحق يحاول من جديد. - أي مُجمِّع (bundler)، أو Node، أو CDN. يأتي MathJax داخل الحزمة وحدةً واحدة مجمَّعة مسبقًا (نحو 1.8 MB قبل الضغط، ولا تجلبها إلا
initMathEngine). يعمل الاستيراد البسيطimport { initMathEngine } from 'https://esm.sh/postext'؛ ولا حاجة إلى?bundle. الحزمةmathjax-fullلا تلزم إلا لبناء postext، فتثبيت postext لا يثبّتها. MathJax ومحلّل mhchem المضمَّن فيه مرخّصان بترخيص Apache-2.0: تأتي إشعاراتهما ونص الترخيص بجوار الوحدة، فيdist/math/THIRD_PARTY_LICENSES.txt. - محرّك واحد لكل صفحة. يتشارك المحرّك وذاكرته المؤقتة للصيغ المُخرجة كلُّ ما يستورد
postextفي نطاق JavaScript نفسه (انظر الحالة العامة المشتركة في صفحة واحدة).
لقواعد الكتابة في جانب المستند ($...$، $$...$$، تهريب علامة الدولار الحرفية)، انظر صيغة المستند.
#الحواشي السفلية
تحدد الخاصية footnotes أين تذهب الحواشي المستشهد بها عبر [^id]، وكيف تُرقَّم وكيف تبدو. الترميز موصوف في صيغة المستند.
interface FootnotesConfig {
placement?: 'column' | 'chapterEnd'; // Foot of the citing column, or after the chapter.
numbering?: 'chapter' | 'document' | 'page' | 'column'; // Start again at each chapter, run on, or start again on each page / column.
numberFormat?: string; // decimal, lower-roman, circled-decimal (①)…
markerPosition?: 'auto' | 'superscript' | 'inline'; // Raised, or on the baseline.
markerSize?: Dimension; // Inline marker size; em is the text around it.
chapterEndAlign?: 'foot' | 'text'; // chapterEnd: notes at the column foot, or under the text.
fontSize?: Dimension; // Note size; em is the body size.
lineHeight?: Dimension; // Note leading; em is the note size.
color?: ColorValue; // Note colour; the body colour when unset.
textAlign?: TextAlign; // The body alignment when unset.
hangingIndent?: Dimension; // Indent of a note's turnover lines.
spaceBetween?: Dimension; // Space between two notes.
spaceAbove?: Dimension; // Space between the text and the rule; em is the body size.
spaceBelowRule?: Dimension; // Space between the rule and the first note.
separator?: {
enabled?: boolean; // Draw the rule.
width?: number; // Rule length, a fraction of the column width.
lineWidth?: Dimension; // Rule thickness.
color?: ColorValue; // The note colour when unset.
};
}| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
placement | 'column' | 'chapterEnd' | 'column' | 'column' تضع كل حاشية في أسفل العمود الذي يحوي السطر المستشهِد بها، تحت خط فاصل قصير؛ وفي تخطيط من عمود واحد يكون ذلك أسفل الصفحة. و'chapterEnd' تضع كل حواشي الفصل بعد آخر كتلة فيه، بترتيب الاستشهاد. |
numbering | 'chapter' | 'document' | 'page' | 'column' | 'chapter' | 'chapter' تبدأ من 1 من جديد تحت كل عنوان من المستوى 1 وفي بداية كل مستند. و'document' تستمر عبر المستند، وفي الكتاب الذي يُخرَج فصلًا فصلًا تستمر من فصل إلى الذي يليه (تحمل continuationAfter الرقم الأخير في continuation.footnoteNumber). و'page' تبدأ من 1 من جديد في كل صفحة، و'column' في كل عمود، فتعدّ الحواشي حيث يضعها الإخراج (أعمدة الصفحة بترتيب القراءة): وهي الحواشي المعتادة أسفل الصفحة في الكتاب الصيني، 页下注. يُخرَج المستند، ثم يُرقَّم بحسب المواضع التي وقعت فيها حواشيه، ثم يُعاد إخراجه حتى تثبت الأرقام (ثلاث عمليات بناء إضافية على الأكثر). كلتاهما تنطبقان على الحواشي في أسفل العمود: مع placement: 'chapterEnd' تُرقَّم الحواشي بحسب الفصل. |
numberFormat | string | 'decimal' | طريقة كتابة الأرقام، بأي صيغة كتابة تقبلها إعدادات الترقيم: 'decimal'، 'lower-roman'، 'lower-alpha'، 'circled-decimal' (أو '①')، 'cjk-decimal'، '一'… وتستخدمها العلامة والرقم الذي يفتتح الحاشية كلاهما. تكتب circled-decimal الأرقام التي تتجاوز 50 بالأرقام العشرية. والاسم غير المعروف يُرقِّم بالأرقام العشرية، مع تحذير unknownNumberFormat. |
markerPosition | 'auto' | 'superscript' | 'inline' | 'auto' | 'superscript' ترفع العلامة في النص والرقمَ الذي يفتتح الحاشية، بحجم مصغَّر. و'inline' تضعهما على خط الأساس: العلامة بحجم markerSize، ورقم الحاشية بحجم الحاشية؛ وفي النص الرأسي تقف العلامة الدائرية ضمن السطر منتصبةً في خلية خاصة بها. و'auto' تعني ضمن السطر مع circled-decimal، ومرفوعة مع كل تنسيق آخر. وفي الحالتين تبقى العلامة مع الحرف الذي قبلها ولا تفتتح سطرًا أبدًا. |
markerSize | Dimension | 1em | حجم العلامة ضمن السطر؛ وem هو حجم النص المحيط بها (0.75em تصغير شائع). لا أثر له في العلامة المرفوعة. |
markerTemplate | string | '' | طريقة كتابة رقم الحاشية، حيث يمثّله بتنسيق الترقيم وبأرقام المستند: '()' تعطي العلامات المحاطة بأقواس المعهودة في الكتب العربية، «(١)». وتكتب بها العلامةَ في النص والرقمَ الذي يفتتح الحاشية على السواء. والقالب الذي يخلو من يُقرأ على أنه القيمة الافتراضية. |
noteNumberPosition | 'auto' | 'superscript' | 'inline' | 'auto' | موضع الرقم الذي يفتتح الحاشية: مرفوعًا، أو على السطر بحجم الحاشية. 'auto' تتبع markerPosition. الكتب العربية ترفع العلامة في النص وتضع رقم الحاشية نفسها على السطر. |
chapterEndAlign | 'foot' | 'text' | 'foot' | مع placement: 'chapterEnd': 'foot' تضع الحواشي التي تختم عمودًا في أسفله، وتبقى الأسطر المتبقية بين النص والحواشي، كما تقف الحواشي في أسفل العمود. و'text' تضعها مباشرةً تحت النص. |
fontSize | Dimension | 0.8em | حجم نص الحاشية. em وrem هما حجم المتن. تستخدم الحواشي عائلة خط المتن وأوزانه. |
lineHeight | Dimension | 1.25em | تباعد أسطر نص الحاشية؛ وem هو حجم الحاشية. الحواشي خارج شبكة خطوط الأساس: تتراكم صعودًا من أسفل العمود، ويبقى النص فوقها على الشبكة. |
color | ColorValue | لون المتن | لون نص الحاشية. |
textAlign | TextAlign | محاذاة المتن | محاذاة نص الحاشية. |
hangingIndent | Dimension | 0 | إزاحة السطر الثاني وما بعده من الحاشية، كي تصطف بعد رقمها. |
spaceBetween | Dimension | 0 | المسافة بين حاشيتين. |
spaceAbove | Dimension | 0.5em | المسافة بين آخر سطر من النص والخط الفاصل؛ وem هو حجم المتن. مع 'chapterEnd' وchapterEndAlign: 'text'، يكون مجموع spaceAbove + spaceBelowRule هو المسافة بين النص وأول حاشية. |
spaceBelowRule | Dimension | 0.4em | المسافة بين الخط الفاصل وأول حاشية. |
separator.enabled | boolean | true | يرسم الخط الفاصل فوق حواشي كل عمود. ومع false تبقى المسافات التي فوقها. |
separator.width | number | 0.3 | طول الخط الفاصل كسرًا من عرض العمود (0–1)، بدءًا من الحافة اليسرى للعمود. |
separator.lineWidth | Dimension | 0.5pt | سُمك الخط الفاصل. |
separator.color | ColorValue | لون الحاشية | لون الخط الفاصل. |
footnotes: {
fontSize: { value: 7.5, unit: 'pt' },
lineHeight: { value: 9.5, unit: 'pt' },
hangingIndent: { value: 0.8, unit: 'em' },
separator: { width: 0.25, lineWidth: { value: 0.4, unit: 'pt' } },
}كيف تُخرَج الحواشي في أسفل العمود:
- الحاشية والاستشهاد يتشاركان العمود. قبل أن يضع الإخراج سطرًا، يضيف ارتفاع الحواشي التي يستشهد بها ذلك السطر لأول مرة (والخط الفاصل، مع أول حاشية في العمود). والسطر الذي لا تتسع حواشيه تحته ينتقل إلى العمود التالي مع بقية فقرته، وفق قاعدتي الأسطر اليتيمة والأرامل. تتقلص مساحة النص في العمود بمقدار ارتفاع الحواشي، فلا تحسب موازنةُ الأعمدة والشريطُ الختامي للفصل إلا النص.
- عدة حواشي في عمود واحد تتراكم بترتيب الاستشهاد تحت خط فاصل واحد. والحاشية التي يُستشهد بها مرة أخرى لاحقًا تحتفظ برقمها ولا تُنضَّد ثانيةً.
- العناصر العائمة السفلية. الشكل الذي يشغل أسفل عمود بعد وضع حواشيه يوضع فوقها؛ والحواشي التي توضع بعد الشكل تأتي فوقه.
- الإطارات. الحاشية المستشهد بها داخل إطار (ضمن السطر أو عائم أو ثابت) تذهب إلى أسفل العمود الذي يستمر فيه النص بعد الإطار، وهو عادةً العمود نفسه. والإطار الذي يختم المستند يترك حواشيه في أسفل العمود الذي انتهى فيه النص.
- الحدود. لا تُقسَم الحاشية أبدًا: الحاشية الأطول من العمود تفيض منه. والعلامات في التعليقات وخلايا الجداول والعناوين لا تُقرأ (تُطبع كما كُتبت).
- المُخرجات. ترسم Canvas وHTML وPDF الحواشي والعلامات (رقمًا مرفوعًا) والخط الفاصل. في PDF ترتبط كل علامة بحاشيتها، وPDF الموسوم يجعل كل حاشية عنصر
Noteبمعرّف/IDفريد مُدرج في/IDTreeمن شجرة البنية (PDF/UA-1). الحواشي كتلVDTBlockعُيّنت فيهاfootnoteNote، فيpage.floats؛ والخطوط الفاصلة فيpage.footnoteAreas. - التحذيرات.
undefinedFootnote(علامة بلا تعريف: يُطبع الرقم فوق حاشية فارغة) وunusedFootnote(تعريف لا تستشهد به أي علامة: لا يُنضَّد).
دالتا الحل والتجريد مماثلتان لنظيراتهما في الأقسام الأخرى:
import { DEFAULT_FOOTNOTES_CONFIG, resolveFootnotesConfig, stripFootnotesDefaults } from 'postext';#الإحالات المرجعية
تحدد الخاصية crossRefs الكلمات التي تطبعها الإحالة المرجعية حول رقم أو صفحة، والنمطَ الذي تأخذه :ref حين لا تحدد نمطًا. يحمل كل قالب {n} حيث يوضع الرقم؛ والقالب الذي يخلو منه يأتي الرقم بعده مسبوقًا بمسافة غير قابلة للكسر ("§" يطبع § 3.2). والقالب غير المعيَّن يتبع لغة المستند: chapter / section / p. بالإنجليزية، وcapítulo / sección / pág. بالإسبانية، و第{n}章 / 第{n}节 / 第{n}页 بالصينية، وهكذا للفرنسية والألمانية والإيطالية والبرتغالية والكتالونية والهولندية.
interface CrossRefsConfig {
chapter?: string; // Words around a level-1 heading's number: "chapter {n}".
section?: string; // Around any other heading's number: "section {n}".
page?: string; // Around a page number: "p. {n}".
defaultStyle?: 'default' | 'number' | 'title' | 'page'; // A :ref without style=.
}| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
chapter | string | بحسب اللغة | إحالة إلى عنوان من المستوى 1: "chapter {n}". ورقم العنوان الذي يتضمن قالبُه الكلمةَ أصلًا (Chapter {1}، 第{1:一}章) يُطبع كما هو. |
section | string | بحسب اللغة | إحالة إلى عنوان من المستوى 2 إلى 6: "section {n}"، "§ {n}". |
page | string | بحسب اللغة | إحالة إلى صفحة (style=page): "p. {n}"، "page {n}". |
defaultStyle | 'default' | 'number' | 'title' | 'page' | 'default' | ما تطبعه :ref إلى عنوان أو مرساة دون style=. 'default': العنوان المرقّم بكلمته ورقمه، وغير المرقّم بنص عنوانه، والمرساة بنصها. والإحالة التي تعيّن style تحتفظ به، ولا تتأثر الإحالات إلى الأشكال والجداول. |
تأخذ الإحالات لون كل إحالة ووزن خطها وميلها (bodyText.referenceColor، referenceBold، referenceItalic).
#الاستشهادات
تختار الخاصية citations نمط الاستشهاد وطريقة ظهور الاستشهادات وقائمة المراجع. الترميز موصوف في الاستشهادات والمراجع؛ ويطبّق النمطَ الحزمةُ postext-citeproc.
interface CitationsConfig {
style?: string; // 'apa', 'ieee', 'chicago-notes-bibliography'… or 'custom'
customStyle?: string; // a whole CSL style (.csl XML), used with style: 'custom'
locale?: string; // CSL locale; the document language when unset
link?: boolean; // citations link to their entries
marker?: 'style' | 'brackets' | 'parentheses' | 'superscript' | 'corner';
collapseRanges?: boolean;
notes?: 'footnote' | 'warichu';
bibliography?: {
title?: string; // unset: the document language's word; '' or ' ': none
scope?: 'book' | 'chapter';
auto?: boolean;
fontSize?: Dimension;
lineHeight?: Dimension;
hangingIndent?: Dimension;
entrySpacing?: Dimension;
labelWidth?: Dimension;
labelAlign?: 'left' | 'right';
doi?: 'link' | 'text' | 'hide';
includeUncited?: boolean;
groupByLanguage?: boolean;
};
}| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
style | string | 'apa' | معرّف نمط مضمَّن (انظر الأنماط) أو 'custom'. النمط هو الذي يقرر ما تقوله الاستشهادات والمُدخلات: الأسماء، والتواريخ، والترتيب، وعلامات الترقيم، وهل الاستشهادات حواشٍ. |
customStyle | string | — | نمط CSL كامل، أي محتوى XML لملف .csl، يُستخدم حين تكون style هي 'custom'. يحمّله Sandbox من ملف. |
locale | string | لغة المستند | لغة CSL المحلية التي يكتب بها النمط كلماته (en-US، es-ES، zh-CN، zh-TW…). |
link | boolean | true | يرتبط الاستشهاد بمُدخله في قائمة المراجع (رابط في PDF، ومرساة في HTML، ونقرة في Sandbox). |
marker | 'style' | 'brackets' | 'parentheses' | 'superscript' | 'corner' | 'style' | كيف يعلّم النمط المرقَّم الاستشهاد: كما يكتبه النمط، أو [1]، أو (1)، أو رقمًا مرفوعًا، أو 〔1〕 (منتصبًا في النص الرأسي). ويلي الرقمَ محدِّدُ الموضع (locator). |
collapseRanges | boolean | true | الأرقام المتتالية تُكتب نطاقًا: 1–3 في علامة واحدة خاصة بها، و[2]–[4] في نمط IEEE. وfalse تبقيها منفصلة. |
notes | 'footnote' | 'warichu' | 'footnote' | أين يضع نمطُ الحواشي استشهاداته: في حواشٍ سفلية (توضع وتُرقَّم كما تحدد footnotes)، أو في تعليقات من صفين داخل السطر (夹注). |
bibliography.title | string | بحسب اللغة | العنوان فوق القائمة، فقرة بخط عريض. إن تُرك فارغًا فلا عنوان. والعنوان الذي تكتبه بنفسك يوضع فوق :::bibliography. |
bibliography.scope | 'book' | 'chapter' | 'book' | قائمة واحدة بكل عمل يستشهد به الكتاب، أو قائمة لكل فصل بالأعمال التي يستشهد بها. في المستند المؤلف من عدة فصول، يبدأ كل H1 قائمة فصل جديدة؛ ومع auto يحصل الفصل الذي لا يضع :::bibliography على قائمته في نهايته. |
bibliography.auto | boolean | true | يضع القائمة بعد النص (في الفصل الأخير، للقائمة الشاملة للكتاب) حين لا تضعها أي :::bibliography. |
bibliography.fontSize | Dimension | 0.9em | حجم المُدخلات؛ وem هو حجم المتن. |
bibliography.lineHeight | Dimension | تباعد أسطر المتن | تباعد أسطر المُدخلات. |
bibliography.hangingIndent | Dimension | 2em | إزاحة الأسطر التالية من المُدخل غير المرقّم. |
bibliography.entrySpacing | Dimension | 0.3em | المسافة بين مُدخلين. |
bibliography.labelWidth | Dimension | أطول تسمية | عرض العمود الذي تقف فيه أرقام القائمة المرقّمة: يبدأ نص كل مُدخل على هذه المسافة إلى الداخل، في سطره الأول كما في أسطره التالية، فيتشارك 9. و10. العمود. وتبقى التسمية جزءًا من نص المُدخل. |
bibliography.labelAlign | 'left' | 'right' | 'left' | موضع التسمية في عمودها: ملاصقة لحافته اليسرى، أو ملاصقة للنص (فينتهي 9. و10. معًا). |
bibliography.doi | 'link' | 'text' | 'hide' | 'link' | معرّفات DOI وعناوين URL روابطَ، أو نصًّا عاديًا، أو تُحذف. |
bibliography.includeUncited | boolean | false | يُدرج كل مرجع، سواء استُشهد به أم لا (مثل nocite: "@*"). |
bibliography.groupByLanguage | boolean | false | الأعمال المكتوبة بالصينية واليابانية والكورية أولًا، ثم الأخرى. لأنماط المؤلف والتاريخ والمؤلف والصفحة فقط: القائمة المرقّمة تحتفظ بترتيب أرقامها. |
#تنضيد نصوص شرق آسيا
تحدّد الخاصية cjk طريقة تنضيد النصوص الصينية واليابانية والكورية: الأعراف الإقليمية التي تتبعها، والمواضع التي يجوز فيها كسر أسطرها، وعرض علامات ترقيمها وهل تتدلّى خارج السطر، والمسافة بين حروف الهان (Han) والحروف اللاتينية، وشبكة الحروف في منطقة النص، وكيف تُطبع العلامات الصينية وقراءات الروبي (ruby) وحواشي الواريتشو (warichu) في النص. كل الحقول اختيارية، والقيمة 'auto' تتبع منطقة لغة المستند (locale)، فالكتاب الذي إعداده locale: 'zh-Hant' يكسر أسطره ويضبط ترقيمه على طريقة تايوان دون أي إعداد آخر. تشرح صفحة الإخراج الصيني القواعد التي تقوم عليها هذه الإعدادات، منطقةً منطقة، مع إعدادين كاملين.
interface CjkConfig {
region?: 'auto' | 'mainland' | 'taiwan' | 'hongkong'; // Regional conventions.
lineBreak?: 'auto' | 'none' | 'basic' | 'gb' | 'strict'; // Which marks may not open or close a line.
punctuationWidth?: 'auto' | 'fullwidth' | 'kaiming' | 'lineEndHalf' | 'halfwidth';
compressAdjacent?: 'auto' | boolean; // Two marks that meet take 1.5 em.
trimLineStart?: 'auto' | boolean; // Brackets at a line edge lose their outer half.
hangingPunctuation?: 'none' | 'allow' | 'force';
latinSpacing?: Dimension; // Between Han and Latin; default 0.25 em.
uprightDigits?: 0 | 2 | 3 | 4; // Vertical text: numbers in one upright cell; default 2.
grid?: { enabled?: boolean; charsPerLine?: number; linesPerPage?: number; show?: boolean };
emphasis?: 'auto' | 'italic' | 'dots'; // What *…* does to Chinese characters.
bookTitleMark?: 'auto' | 'brackets' | 'wavy' | 'none'; // What :book[…] prints.
annotationColor?: ColorValue; // Dots and name/title lines; default the text colour.
ruby?: { fontFamily?: string; fontSize?: Dimension; color?: ColorValue; position?: 'auto' | 'over' | 'under' | 'right' };
warichu?: { fontSize?: Dimension; color?: ColorValue; open?: string; close?: string };
}| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
region | 'auto' | 'mainland' | 'taiwan' | 'hongkong' | 'auto' | الأعراف التي يتبعها النص، وفق المناطق التي تصفها وثيقة W3C Requirements for Chinese Text Layout (clreq). تقرأ 'auto' المنطقة من locale، أو من bodyText.hyphenation.locale إن لم يُضبط locale: تعطي zh وzh-Hans وzh-CN وzh-SG القيمة 'mainland'؛ وتعطي zh-Hant وzh-TW القيمة 'taiwan'؛ وتعطي zh-HK وzh-MO القيمة 'hongkong'؛ وتعطي أي لغة أخرى 'mainland'. تختار المنطقة القيم الافتراضية للخصائص lineBreak وpunctuationWidth وcompressAdjacent وtrimLineStart. |
lineBreak | 'auto' | 'none' | 'basic' | 'gb' | 'strict' | 'auto' | العلامات التي لا يجوز أن تبدأ سطرًا أو تنهيه (clreq §6.1.1؛ انظر الجدول أدناه). 'auto': 'gb' للبر الصيني الرئيسي، و'basic' لتايوان وهونغ كونغ. |
punctuationWidth | 'auto' | 'fullwidth' | 'kaiming' | 'lineEndHalf' | 'halfwidth' | 'auto' | عرض العلامات كاملة العرض (انظر عروض علامات الترقيم). 'auto': 'kaiming' للبر الرئيسي، و'fullwidth' لتايوان وهونغ كونغ. |
compressAdjacent | 'auto' | boolean | 'auto' | تتخلّى علامتان متجاورتان (。」، 》(، :“) عن نصف em من البياض بينهما، فيأخذ الزوج 1.5 em بدلًا من 2. 'auto': مفعّل للبر الرئيسي وهونغ كونغ، ومعطّل لتايوان. |
trimLineStart | 'auto' | boolean | 'auto' | يتخلّى القوس أو علامة الاقتباس الافتتاحية التي تبدأ سطرًا عن نصف em الذي يسبقها، والختامية التي تنهي سطرًا عن نصف em الذي يليها. 'auto': مفعّل للبر الرئيسي وهونغ كونغ، ومعطّل لتايوان. |
hangingPunctuation | 'none' | 'allow' | 'force' | 'none' | هل يجوز لعلامة وقف أو علامة انتهاء أن تتدلّى خارج نهاية السطر (انظر علامات الترقيم المتدلّية). |
latinSpacing | Dimension | { value: 0.25, unit: 'em' } | المسافة بين حرف هان وحرف لاتيني أو رقم مجاور له، بوحدة em من حجم نص CJK أو بأي طول؛ والقيمة 0 تلغيها (انظر المسافة بين الهان واللاتيني). |
uprightDigits | 0 | 2 | 3 | 4 | 2 | في النص العمودي: يقف العدد الذي لا تتجاوز أرقامه هذا العدد في خلية قائمة واحدة (tate-chu-yoko)، إلا إذا ورد في جملة لاتينية، فيتبع كلماتها مُدارًا على جانبه؛ والقيمة 0 تلغي ذلك (انظر الأرقام في النص العمودي). |
grid | { enabled?, charsPerLine?, linesPerPage?, show? } | معطّلة | منطقة النص محسوبة بعدد الحروف في السطر وعدد الأسطر في الصفحة (انظر شبكة الحروف). |
emphasis | 'auto' | 'italic' | 'dots' | 'auto' | ما يفعله التوكيد في Markdown (…) بالحروف الصينية: تضع 'dots' نقطة توكيد تحت كل حرف (وعلى يمينه في النص العمودي)، كما تفعل :dots[…]، وتحتفظ الحروف اللاتينية الواقعة في التوكيد نفسه بميلها؛ وتُميل 'italic' الحروف، وهو ما لا يقدر عليه الخط الصيني إلا بالتزييف. 'auto': 'dots' حين تكون لغة المستند الصينية، و'italic' في غير ذلك (انظر العلامات والروبي والواريتشو). |
bookTitleMark | 'auto' | 'brackets' | 'wavy' | 'none' | 'auto' | ما تطبعه :book[…]: 《》 حول العنوان (〈〉 داخل عنوان آخر)، أو خط عنوان الكتاب المتموّج تحته، أو العنوان مجرّدًا. 'auto': الأقواس للبر الرئيسي، والخط المتموّج لتايوان وهونغ كونغ. |
annotationColor | ColorValue | لون النص | لون نقاط التوكيد وخطوط أسماء الأعلام وعناوين الكتب؛ وهو القيمة الافتراضية للونَي الروبي والواريتشو. |
ruby | { fontFamily?, fontSize?, color?, position? } | نصف الحجم، 'auto' | قراءات :ruby[…] و{紅樓|hóng|lóu}: الخط (افتراضيًا خط النص)، والحجم (افتراضيًا { value: 0.5, unit: 'em' } من النص؛ والزويين (zhuyin) بنسبة 60 % منه)، واللون، وموضعها حين لا يحدده الروبي ('auto': الزويين على يمين كل حرف، والبينيين (pinyin) فوق النص في النص الأفقي وعلى يمينه في النص العمودي). |
warichu | { fontSize?, color?, open?, close? } | نصف الحجم، بلا أقواس | الحواشي ذات الصفّين في :warichu[…]: حجم الحاشية (افتراضيًا نصف em، فيملأ الصفّان em السطر)، ولونها، والقوسان المنضّدان بحجم النص قبل صفّها الأول وبعد صفّها الأخير (ويغلب open / close الخاصّان بالحاشية). |
| المستوى | لا تبدأ السطر أبدًا | لا تنهي السطر أبدًا |
|---|---|---|
none | لا شيء: يجوز كسر السطر بين أي حرفين، كما تفعل الصحف في تايوان وهونغ كونغ. | لا شيء. |
basic | علامات الوقف والانتهاء 、,;:。!?.‼⁇⁈⁉؛ وعلامات الاقتباس الختامية ” ’ 」 』 والأقواس الختامية )〕]}】〗》〉؛ وأدوات الوصل – ~ ~ والشرطة المفردة — بين كلمتين؛ والنقاط الوسطى · ‧ ・؛ وعلامات التكرار 々〻ゝゞヽヾ و ー؛ ووحدات الأعداد % ‰ ° ℃ % ومربعات الوحدات ㎡ ㎏ ㏄. | علامات الاقتباس الافتتاحية “ ‘ 「 『 والأقواس الافتتاحية (〔[{【〖《〈؛ ورموز العملات ¥ $ € £. |
gb | basic، والشرطة المائلة / / (GB/T 15834—2011 §5.1.9). | basic، والشرطة المائلة. |
strict | gb، والشرطة المزدوجة —— ⸺ وعلامة الحذف …… ⋯⋯. | gb. |
في كل المستويات:
- —— و…… وحدة واحدة عرضها 2 em لا تنقسم أبدًا؛ ويجوز أن تنفصل وحدتان متتاليتان من هذا النوع عند الحدّ بينهما.
- يبقى العدد مع علاماته ووحدته (¥5,999، 50%، 50%، 120㎡)، حتى عبر مسافة (−3 ℃، 50 %)، وتبقى الكلمة اللاتينية كاملة: يُنضَّد النص الغربي الواقع بين حروف CJK سلسلةً لا يُكسر السطر داخلها أبدًا، إلا إذا كانت السلسلة وحدها أعرض من السطر (فتُقسم حينئذ عند مقطع لفظي بواصلة، أو عند آخر حرف يتسع له السطر؛ وعنوان الويب عند مفاصله). ومربعات الوحدات ㎡ ㎏ ㎞ ㏄ (U+3371–337A، U+3380–33DF، U+33FF) تنتمي إلى سلسلة عددها ولا تجعل الفقرة اللاتينية فقرة CJK.
- العدد أو الكلمة المكتوبة بأرقام وحروف كاملة العرض (123456، 50%، ¥599، 3.14، 12:30، ABC) لا تنقسم كذلك أبدًا؛ ومع ذلك يوزّع السطر المضبوط حروفها كما يوزّع حروف الهان.
- لا تكون المسافة بين الكلمات موضع كسر إلا حيث تسمح القواعد: لا يُكسر السطر عندها إذا كان الحرف التالي مما لا يجوز أن يبدأ سطرًا (
参见图表 ( 第三章 )لا يبدأ سطرًا أبدًا بـ )) أو كان الحرف الأخير قبلها مما لا يجوز أن ينهي سطرًا. - تبقى علامة الحاشية السفلية و
:refوالنص المرتفع أو المنخفض مع الحرف الذي يسبقها. - المسافة الأيديوغرافية U+3000 حرف عرضه em واحد: يجوز كسر السطر بعدها، ولا يجوز قبلها أبدًا؛ ولا تُمطّ أبدًا ولا تُحذف أبدًا في بداية السطر. ولإزاحة الحرفين المعتادة في الفقرات الصينية اضبط
bodyText.firstLineIndent: { value: 2, unit: 'em' }بدلًا من كتابة مسافتين U+3000 (يحذف المحلّل ما تبدأ به الفقرة منها).
حين لا يتسع السطر لحرف ويجوز لهذا الحرف أن يبدأ السطر التالي، ينزل إليه ويوزّع السطر المضبوط ما بقي فيه. وحين لا يجوز له أن يبدأ السطر التالي (فاصلة، أو قوس ختامي، أو الحرف الذي يلي قوسًا افتتاحيًا)، يحاول السطر أولًا أن يستوعبه بالضغط (الدفع إلى الداخل، push-in، clreq §6.2.2.3): إذا كان البياض الذي يجوز له التخلّي عنه — انظر عروض علامات الترقيم — يغطي الفائض، دخل الحرف السطرَ (مع العلامات التي يجب أن تبقى معه، كعلامة اقتباس ختامية بعد نقطة) وضُبط السطر على العرض. وإلا تخلّى السطر عن حروف لصالحه (الدفع إلى الخارج، push-out): يرجع موضع الكسر إلى آخر موضع تسمح به القواعد، ويوزّع السطر المضبوط ما بقي. وفي سطر أضيق من عدد مع نقطته، لا يجوز لشيء أن ينهي السطر، فيُكسر قبل النقطة رغم ذلك.
الفقرات التي ينطبق عليها هذا: تلك التي تضم من حروف CJK (الهان، والكانا، والبوبوموفو، والهانغول) أكثر مما تضم من مسافات بين الكلمات، حتى لو لم يتجاور حرفان منها (第1条、第2条، 价¥5,999。好). والمسافة المجاورة لحرف أو علامة CJK ليست مسافة بين كلمات: يُنضَّد 2026 年 9 月 28 日 كما يُنضَّد 2026年9月28日. وتُنضَّد هذه الفقرات سطرًا سطرًا، في المسار العادي والمسار المنسَّق على السواء، فتنكسر الفقرة التي فيها كلمة غامقة تمامًا كما ينكسر النص نفسه بدونها. أما الفقرة اللاتينية التي تقتبس عنوانًا أو اسمًا صينيًا ففيها مسافات أكثر من الحروف: فتحتفظ بالكسر الأمثل للأسطر، ويجوز كسر السطر بجوار حروف CJK فيها وفق القواعد نفسها. وتُكسر التعليقات وخلايا الجداول والحواشي والإطارات على مستوى المستند أيضًا. ولا يمتد تقسيم الكلمات بالقاموس إلى الكلمات اللاتينية في فقرة CJK.
يوزَّع سطر CJK المضبوط الذي ليس آخر فقرته على العرض، بهذا الترتيب (clreq §6.2.2.4): المسافات بين الكلمات الغربية، حتى نصف em لكل منها؛ ثم المسافات بين الهان واللاتيني، حتى نصف em لكل منها؛ ثم كل فجوة بين الحروف، وتلك المسافات، بالتساوي. لا توضع مسافة داخل كلمة لاتينية أو عدد أو علامة عرضها 2 em، ولا بجوار أداة وصل أو شرطة مائلة. يُضبط التباعد لكل مقطع من السطر (VDTLineSegment.tracking، بالبكسل بعد كل حرف، وهو جزء من width المقطع)، وترسمه مخرجات Canvas وHTML وPDF تباعدًا بين الحروف؛ والكلمة اللاتينية التي تليها فجوة يُفرد حرفها الأخير في مقطع خاص به. يضم المقطع سلسلة غربية واحدة، أو حروفًا من نمط واحد ورابط واحد وتباعد واحد تتقدّم بالمقدار نفسه، لذا يوزّع Sandbox عرض المقطع عليها بالتساوي حين يضع المؤشر، ولا يغطي الرابط إلا حروفه. وحين يحتاج السطر إلى أكثر من نصف em بين حروفه (أو أكثر من bodyText.maxJustifyTracking إن ضُبط)، يُنضَّد بذلك المقدار وينتهي قبل بلوغ العرض: يوسَم السطر بـ cjkLoose وragged، ويُبلغ البناء عن تحذير محتوى cjkLooseLine مع نص السطر. والسبب المعتاد كلمة لاتينية طويلة أو عنوان ويب لا يمكن أن يصعد إلى السطر السابق. والسطر الخالي من أي حرف CJK (رأس عنوان ويب طويل) يُنضَّد بحافة حرّة دون التحذير، كما يُنضَّد سطر لاتيني من كلمة واحدة. انظر تقسيم الكلمات بالواصلة وضبط الأسطر.
#عروض علامات الترقيم
العلامة الصينية كاملة العرض نصف em من رسم الحرف (glyph) ونصف em من البياض، وما تضبطه الإعدادات هو البياض، لا رسم الحرف أبدًا (clreq §6.3.2). يقع البياض قبل رسم القوس أو علامة الاقتباس الافتتاحية، وبعد رسم الختامية، وبعد رسم علامات الوقف والانتهاء في البر الرئيسي 、,。.;:?!، التي تجلس في زاوية مربعها؛ أما العلامات التي تتوسط مربعها في تايوان وهونغ كونغ (、,。.;:) والنقاط الوسطى فتحتفظ بربع em على كل جانب. وتبقى ?! بعرض em واحد في النص الأفقي في تايوان وهونغ كونغ، وتبقى :;?! كذلك في النص العمودي في كل مكان. والنقطة الوسطى في البر الرئيسي نصف em في كل الأنماط، متوسطة، في اتجاهَي الكتابة كليهما (GB/T 15834، clreq §5.1): يتخلّى الرسم كامل العرض عن بياضه على الجانبين، وفي النص العمودي تكون خليتها نصف em من الأساس.
| النمط | داخل السطر | في نهاية السطر |
|---|---|---|
fullwidth (全角式) | كل علامة em واحد. | em واحد؛ والقوس الختامي نصف em، مع trimLineStart. |
kaiming (开明式) | 。.?! em واحد؛ و,、;:، والأقواس، وعلامات الاقتباس، والنقاط الوسطى نصف em. وهو نمط معظم كتب البر الرئيسي. | كل علامة نصف em. |
lineEndHalf (行末半角) | كل علامة em واحد. | كل علامة نصف em (GB/T 15834—2011 §5.1.10 بقراءة حرفية). |
halfwidth (半角式) | كل علامة نصف em، كما في القواميس. | نصف em. |
مع compressAdjacent، تتخلّى علامتان متجاورتان عن البياض بينهما (القواعد الثماني في مسودات clreq السابقة): قوس ختامي بعد قوس ختامي آخر أو بعد علامة وقف أو انتهاء من البر الرئيسي (。」، لا بعد العلامات المتوسطة في تايوان وهونغ كونغ)، وعلامة وقف أو انتهاء بعد قوس ختامي (」,)، وقوس افتتاحي بعد أيٍّ منها أو بعد قوس افتتاحي آخر (,「، 》(، 「『)، وربع em بين نقطة وسطى وقوس ختامي قبلها أو قوس افتتاحي بعدها. ولا يتجاوز الضغط أبدًا ما يبلغ بالزوج 1.5 em: في نمط Kaiming يأخذ 。” أصلًا 1.5 em ويحتفظ بها، ويأخذ 》( em واحدًا. ويضع Kaiming بياض علامة الانتهاء بعد العلامة الختامية التي تليها، مع compressAdjacent أو بدونها: يتجاور الرسمان ويأتي نصف em بعد علامة الاقتباس (。”␣母، لا 。␣”母)، ويذهب إلى نهاية السطر كما يذهب بياض علامة الانتهاء. ومع trimLineStart، يتخلّى القوس الافتتاحي الذي يبدأ سطرًا عن بياضه السابق، فيصطف حبره مع حافة النص (وفي السطر الأول من الفقرة يقع على بعد نصف em داخل الإزاحة)، ويتخلّى القوس الختامي الذي ينهي سطرًا عن بياضه اللاحق. والعلامة المتوسطة تتخلّى عن ربع em على كل جانب، لا عن نصف em على جانب واحد أبدًا. وحين ينكسر السطر بين علامتين، لا تحتفظ أيٌّ منهما بالضغط: تُنضَّد كل منهما علامةً على حافة سطر (فالعلامة , كاملة العرض التي يبدأ 「 التالي لها السطر الجديد تنهي سطرها بعرض em كامل).
يستوعب السطر حرفًا لا يجوز أن يبدأ السطر التالي (الدفع إلى الداخل) حين يغطي البياضُ الذي لا يزال يجوز له التخلّي عنه الفائضَ، وفق ترتيب clreq: المسافات بين الكلمات حتى ربع em، ثم النقاط الوسطى، فالأقواس، فعلامات الوقف، فالمسافات بين الهان واللاتيني حتى ثُمن em، وعلامات الانتهاء أخيرًا، وتتقاسم كل خطوة الضغط بالتساوي. لا يسمح kaiming إلا لعلامات الانتهاء بالنزول إلى نصف em، وفي هذه الحالة وحدها: السطر الذي ينقصه حرف واحد فقط يوزَّع، فتحتفظ 。?! بعرض em داخل السطر. ويسمح lineEndHalf لكل علامة بالنزول إلى نصف em؛ ولا تتخلّى علامات fullwidth عن شيء (فالكتاب كامل العرض يحافظ على شبكته)، ولا يبقى لعلامات halfwidth شيء تتخلّى عنه.
العلامات التي يتشاركها النص اللاتيني مع الصيني (“ ” ‘ ’ … — ·) تأخذ مربع العلامة الصينية في النص الصيني، أيًّا كان تقدّمها (advance) في الخط نفسه: يرسم LXGW WenKai “ ” بعرض 0.35 em، ويرسم Noto Serif SC الشرطة الطويلة بعرض 0.89 em و· بعرض ثلث em. وتُعدّ صينية حين يكون أقرب حرف على أيٍّ من جانبيها (متجاوزًا علامات أخرى من هذا النوع) صينيًا، أو حين لا يجاورها شيء غربي. يقع رسم علامة الاقتباس الافتتاحية في نهاية مربعها ذي em الواحد، والختامية في بدايته؛ وتقع النقطة الوسطى وعلامة الحذف والشرطة المفردة في الوسط؛ ويُنضَّد زوج الحذف (……) كما يضبط الخط الاثنين معًا، متوسطًا في عرضه البالغ 2 em. أما 破折号 (——) فخط واحد متصل: تُمطّ كل شرطة على em الخاص بها انطلاقًا من الحيّز الجانبي للحرف في الخط نفسه، فتتراكب الضربتان عند الوصل، وتُرفع إلى منتصف ارتفاع الحروف (VDTLineSegment.inkScale، مقياس يخص الرسم وحده). ثم يضبط النمط المربع كما يضبط مربع أي علامة أخرى. ومع وجود نص غربي على الجانبين (他说:He said “yes” and left.) تحتفظ هذه العلامات بتقدّم الخط، والرسم الذي يبلغ عرضه em واحدًا أصلًا لا يتغيّر فيه شيء.
تُبقي المُخرِجات تباعد الترقيم الخاص بالمتصفح بعيدًا عن النص. فـ Chrome يضبط أولى علامتين متجاورتين بنصف عرض حين يقيسهما أو يرسمهما في سلسلة واحدة (本)》录 بخط Noto Serif SC عرضه 3.5 em بهذه الطريقة)، لذا تُقاس العلامتان المتجاورتان وتُرسمان منفصلتين، وتحمل أسطر HTML في نص CJK الخاصيتين text-spacing-trim: space-all وtext-autospace: no-autospace مع تعطيل الميزات chws وhalt وvchw.
في VDT تكون العلامة التي تخلّت عن بياض مقطعًا مستقلًا، width فيه هو التقدّم الذي تحتفظ به، مع ضبط inkOffset (بالبكسل): ترسم المُخرِجات رسم الحرف عند x + inkOffset، فالعلامة التي تخلّت عن البياض الذي قبل رسمها (قوس افتتاحي في بداية سطر) تُرسم قبل مربعها بذلك المقدار (إزاحة سالبة). والعلامة المشتركة المنضّدة في مربع صيني تحمل موضع رسمها في المربع، ناقصًا أي بياض تخلّت عنه قبله، وقد يكون موجبًا. يعرض PDF هذا الرسم بتباعد حروف ينهي تقدّمه حيث ينتهي مربعه، فلا يرى برنامج القراءة أبدًا أنه يتجاوز الحرف التالي، ويضع سطره في نطاق /ActualText (انظر المسافة بين الهان واللاتيني). والمنطقة هي التي تحدّد في أي جانب يقع بياض العلامة، لا الخط: نضّد الكتاب بخط من منطقته (Noto Serif SC للبر الرئيسي، وTC لتايوان، وHK لهونغ كونغ)؛ فالخط التقليدي تحت وسم البر الرئيسي يضغط الجانب الخطأ من علاماته المتوسطة.
#علامات الترقيم المتدلّية
تسمح hangingPunctuation: 'allow' لإحدى العلامات 、,。. (وفي البر الرئيسي، حيث تجلس العلامات في بداية مربعها، أيضًا ;:?!) بأن تتدلّى خارج نهاية السطر حين كانت ستبدأ السطر التالي لولا ذلك ولا يستطيع ضغط السطر استيعابها؛ ولا يحدث ذلك أبدًا في النص الأفقي في تايوان وهونغ كونغ، لأن علاماتها المتوسطة ستبدو مقطوعة (أما في النص العمودي فيجوز). وفي النص العمودي تقع العلامة المتدلّية تحت ذيل السطر. وتُدلّي 'force' هذه العلامة كلما أنهت سطرًا (عدا السطر الأخير من الفقرة)، وفورًا حين لا يتسع لها السطر. ولا تتدلّى علامة أبدًا حين تلامسها علامة أخرى (。」، ,「). يوسَم المقطع المتدلّي بـ hangs؛ ويستبعده bbox.width الخاص بالسطر وضبطُه ومحاذاتُه، ويوسّع Canvas وPDF قصّ العمود بعرض أعرض علامة متدلّية (hangingPunctuationOverhang)، فلا تُقطع أبدًا. لا تُدلّي معظم الكتب الصينية علامات الترقيم؛ ولا يوصي بها clreq إلا مع شبكة حروف.
#المسافة بين الهان واللاتيني
تضع latinSpacing (افتراضيًا ربع em) مسافة بين حرف هان (أو كانا) وحرف لاتيني أو رقم أوروبي مجاور له: يُنضَّد 用iPhone拍照 و1999年 على شكل 用 iPhone 拍照 و1999 年. ولا توضع في بداية السطر أو نهايته، ولا بين حرف لاتيني وعلامة صينية (用iPhone, لا يضع شيئًا قبل الفاصلة)، ولا داخل الأقواس الصينية ((iPhone))، ولا بجوار رمز ليس حرفًا ولا رقمًا (为¥5,999). والمسافة التي كتبها المؤلف عند حدّ كهذا (用 iPhone 拍照، كما يرد في كثير من نصوص الويب) تُستبدل بمسافة الهان واللاتيني ولا تُضاف إليها، فتُنضَّد الكتابتان بالطريقة نفسها؛ أما المسافة غير القابلة للكسر والمسافة الأيديوغرافية فتُتركان كما هما. وفي السطر المضبوط تنمو حتى نصف em قبل أن تُوزَّع الحروف؛ وفي السطر الذي يستوعب حرفًا لا يجوز أن يبدأ السطر التالي تتقلّص حتى ثُمن em. ويُحوَّل الطول بأي وحدة غير em وفق dpi الصفحة. والقيمة 0 تلغيها، وتبقى المسافة المكتوبة هناك مسافة بين كلمات.
المسافة مقطع من kind: 'space' موسوم بـ autospace، وtext فيه فارغ (أو هو المسافة التي كتبها المؤلف)؛ وwidth فيه نهائي، ولا يمسّه ضبط المسافات الخاص بالمُخرِجات. ولا تظهر أبدًا في النص العادي، فيقرأ البحث والنسخ واللصق ونطاقات المصدر النص كما كُتب. ويضع PDF كل سطر منضَّد على أجزاء (حروف موزَّعة، ومسافات بين الهان واللاتيني، وعلامات تخلّت عن بياض أو متدلّية) في /Span قيمة /ActualText فيه نص السطر، فيقرأ استخراج النص 用iPhone拍照 لا 用 iPhone 拍 照، ويقرأ السطر ذا العلامات نصف العرض سطرًا واحدًا.
#الأرقام في النص العمودي
في النص العمودي تُدار الكلمة اللاتينية أو العدد الطويل على جانبه، ويقف العدد القصير قائمًا، أرقامه متجاورة في خلية واحدة عرضها em واحد: tate-chu-yoko (縱中橫، clreq §2.1.3، CSS text-combine-upright). يحدّد uprightDigits عدد الأرقام التي يجوز أن يتألف منها هذا العدد: 2 (الافتراضي)، أو 3، أو 4، أو 0 لإلغائه. فـ 2026年9月28日 مع 2 يصبح 2026 على جانبه، و9 قائمًا، و28 قائمًا في خلية واحدة.
- العدد كله أو لا شيء منه: مع
2يبقى العدد ذو الأرقام الثلاثة على جانبه، ولا ينقسم أبدًا. - العدد الملاصق لحرف لاتيني (
A4،mp3،3D) يبقى في كلمته، على جانبه؛ وكذلك العدد المكتوب بفاصلة عشرية أو بتجميع الأرقام (3.14،10,000). - العدد داخل جملة لاتينية يتبع الجملة: حين تقع كلمة لاتينية على جانبيه، يجري على جانبه مع الكلمات (
printed in 49 and 32 copies،chapters 49, 32 and (7) of). وعلى كل جانب يمتد البحث متجاوزًا المسافات، والأعداد الأخرى، وعلامات الحواشي، وعلامات ترقيم السلسلة المُدارة (, . : ; ( ) ' " - /)، والشرطتين القصيرة والطويلة، وعلامات الاقتباس المنحنية (pages 3–5 of،the “49” copies) حتى أول حرف لاتيني أو صيني. والعدد الذي تقود علاماته إلى نص صيني يقف قائمًا (上午12:30:45开会،比分为3:2:1،见图(3)所示،他住在"12"号楼)، وكذلك العدد المجاور لحرف أو علامة صينية، بمسافة أو بدونها (第 3 回،用iPhone 15拍攝،第3 copies)، والعدد بجانب رمز يقف قائمًا (a 30×40 print)، والعدد في بداية الفقرة أو نهايتها، إذ لا تقع كلمة إلا على جانب واحد منه (49 copies were printed،on page 7.: اكتب:sideways[…]لتديره). وتُقرأ الفقرة كاملة، عبر فواصل الأسطر والتوكيد والروابط. - بجوار رمز يضعه Unicode قائمًا يأخذ كل عدد خليته الخاصة:
30×40هو 30، و×، و40، كلها قائمة. - الخلية التي تضم حروفًا أكثر مما يتسع له em تُضغط عرضيًا إلى em؛ ولا يدخلها التتبّع ولا الضبط أبدًا، بل يأتيان بعدها فقط.
- عند الكسر والضبط تُعدّ الخلية حرفًا صينيًا واحدًا، ولا توضع حولها مسافة بين الهان واللاتيني.
- يجد المُقيِّس وكل المُخرِجات الخلايا نفسها: يرسم Canvas الأرقام مُعادةً إلى الوضع القائم ومضغوطة، ويفعل PDF الشيء نفسه بالخط الأفقي، ويلفّها HTML في
text-combine-upright: all.
يدويًا، تتجاوز ثلاث علامات سطرية الإعداد في النص العمودي ولا تغيّر شيئًا في النص الأفقي (انظر صيغة المستند › الاتجاه في النص العمودي):
第:tcy[120]回、:upright[GDP]與:sideways[12]تضع :tcy[…] نصها في خلية قائمة واحدة، وتُقيم :upright[…] كل حرف في خلية خاصة به (والحروف اللاتينية في وسطها)، وتدير :sideways[…] السلسلة كلها، بما فيها الحروف الصينية. في VDT تكون سلسلة :tcy مقطعًا فيه tcy: true، بعرض em واحد؛ وتحمل سلسلة :upright أو :sideways الخاصية orientation. أما الأعداد التي يضعها uprightDigits في خلية واحدة فلا تحمل وسمًا: تجدها verticalRuns(graphemes, region, uprightDigits). وفي الفقرة، يحمل العدد القصير الذي يجري مع كلمات لاتينية orientation: 'sideways'، كأنه مكتوب :sideways[…]: فمقطعه لا يضم دائمًا الكلمات التي يجري معها.
#شبكة الحروف
تُحدَّد منطقة النص الصينية بالحروف (clreq §7.1.1): حجم المتن × عدد الحروف في السطر × عدد الأسطر في الصفحة، مضافًا إليها الفراغ بين الأسطر، والفاصل بين العمودين حين يكون هناك عمودان. ويضبطها grid بهذه الطريقة:
cjk: { grid: { enabled: true, charsPerLine: 28, linesPerPage: 28, show: true } }حين يكون enabled مفعّلًا، تُعاد كتابة الإعدادات قبل أن يقرأها أي شيء: يصبح عرض كل عمود charsPerLine em من bodyText.fontSize، وارتفاع منطقة النص linesPerPage سطرًا من bodyText.lineHeight؛ ومع layoutType: 'double' يكون الفاصل بين العمودين هو layout.gutterWidth مقرّبًا إلى عدد صحيح من em، لا يقل عن واحد. وتعمل الهوامش في page.margins حدودًا دنيا: توضع منطقة النص في منتصف المساحة التي تتركها، ويُزاد كل هامش بنصف المساحة المتبقية على محوره (الكتاب ذو الهوامش المتناظرة يحافظ على الفرق بين هامشيه الداخلي والخارجي، ويُزاد كلاهما بالمقدار نفسه). وإن تُرك charsPerLine وlinesPerPage دون ضبط، أخذا أكبر عدد يتسع. والعدد الأكبر مما يتسع يُخفض إلى ما يتسع ويُبلغ عنه بتحذير إعداد cjkGridClamped. ويبقى حجم الخط وارتفاع السطر كما كُتبا. في تخطيط oneAndHalf يكون charsPerLine للعمود الرئيسي؛ ويأخذ العمود الجانبي عدد em الصحيح الأقرب إلى العرض الذي يعطيه له sideColumnPercent داخل الهوامش المضبوطة (لا يقل عن واحد)، ويُقرَّب الفاصل كما في تخطيط العمودين، وتُعاد كتابة sideColumnPercent بحيث يُقطع العمودان كلاهما بعدد صحيح من em. وفي النص العمودي (layout.writingMode: 'vertical-rl') تجري حروف السطر نزولًا في الصفحة وتجري الأسطر عرضًا، فيقيس charsPerLine ارتفاع الصفحة، ويعطي تخطيط double طبقتين مكدّستين من الأعلى إلى الأسفل، وتقف خلايا الشبكة المرسومة فوق النص على المحور الذي تتوسطه الحروف.
تخطيطان من الممارسة الصينية، بحجم 五号 (10.5 pt) مع فراغ بين الأسطر قدره 6 pt (lineHeight: 16.5pt):
- 大32开، 140 × 203 mm، 28 × 28: عرض سطر 294 pt (103.7 mm) ومنطقة نص 462 pt (163 mm)؛ و
page.marginsبقيم 16/20 mm للداخلي والخارجي و18/20 mm للأعلى والأسفل تترك لها متسعًا. - 16开، 184 × 260 mm، عمودان من 23 حرفًا بحجم 小五 (9 pt، وأسطر 13.5 pt) مع فاصل بعرض حرفين: 2 × 207 pt + 18 pt.
يرسم show الشبكة (稿纸) فوق منطقة النص، مربعًا رماديًا فاتحًا لكل موضع حرف في كل سطر (مربع em للحرف حول خط أساسه)، في كل عمود من الصفحة (والعمود الجانبي في oneAndHalf على الجانب الذي تضعه فيه زوجية الصفحة أو فرديتها)، على Canvas وفي HTML. ولا يرسمها PDF إلا حين يُعطى renderToPdf الخيار characterGrid: true: فهي أداة مساعدة على الشاشة. تُعيد cjkGridGeometry(config) الشبكة التي تضبطها الإعدادات (الأعداد المستخدمة، والهوامش، ومنطقة النص)، وتُعيد applyCjkGrid(config) الإعدادات بعد إعادة كتابتها.
#العلامات والروبي والواريتشو
ترميز العلامات الصينية والروبي والواريتشو يُبقي نصه في الفقرة: فالبحث وجدول المحتويات ومراسي الفهرس الأبجدي والنص المنسوخ تقرأ الحروف كما كُتبت، ولا يتوقف على هذه الإعدادات إلا ما يُرسم حولها.
- نقاط التوكيد (着重号،
:dots[…]، و*…*على الحروف الصينية معemphasis: 'dots') تأخذ نقطة واحدة لكل حرف، متوسطة عليه (دون احتساب تباعد السطر المضبوط)، ولا توضع أبدًا على علامات الترقيم أو المسافات: تحت الحرف في النص الأفقي، وعلى يمينه في النص العمودي (clreq §5.3.1). يختارstyleنقطة مصمتة أو دائرة مفرغة أو نقطة سمسمية، ويرسمfill="open"المحيط وحده، ويحدّدpos="over|under"الجانب. - خطوط أسماء الأعلام وعناوين الكتب (专名号
:name[…]، و书名号:book[…]معbookTitleMark: 'wavy') تمتد تحت مربع em للحروف (وعلى يساره في النص العمودي)، عبر المسافات داخل السلسلة. وحيث تلتقي سلسلتان، يتخلّى كل طرف عن ثُمن em، فتُقرأ:name[賈寶玉]:name[林黛玉]اسمين. وحين تُعلِّم النقاط والخط النص نفسه على جانب واحد، يكون الخط أقرب إلى النص. ومع'brackets'تكون 《》 نصًا تُكسر به الأسطر، يُرسم ويُنسخ كأي حرف آخر؛ ولا تشغل أي حرف من النص العادي أو من خريطة المصدر (توسَم المقاطع بـinserted). - قراءات الروبي تقع في الفراغ بين الأسطر ملاصقة لمربع em للأساس، متوسطة عليه؛ والقراءة التي تضم حرفًا لاتينيًا (بينيين) وتقف فوق أساسها تُرفع بعمق الذيول النازلة في خطها (g, j, p, q, y) و0.04 em من النص زيادة على ذلك، لتتجاوز الأساس، وتحتفظ كل قراءات السطر بخط أساس واحد؛ والقراءة الأعرض من أساسها توسّع مربع الأساس، ناقصًا ربع em الروبي الذي يجوز أن تمتد به على جار بلا قراءة، وتبقى بين قراءتين مسافة ربع em الروبي (وبين قراءتَي زويين ربع em الرموز، فتبقى ثلاثة رموز بجانب كل من حرفين داخل خلاياها). يجوز كسر الروبي الأحادي (قراءة لكل حرف) بين حروفه؛ أما الروبي الجماعي فلا ينكسر أبدًا. وفي بداية السطر أو نهايته يصطف الأساس والقراءة مع الحافة (clreq §5.5.4). ويقف الزويين في النص الأفقي في عمود على يمين كل حرف، ويكبر مربع الحرف بقدر العمود؛ وفي النص العمودي يمتد العمود نفسه نزولًا على يمين الحروف. وتذهب علامة النبرة إلى يمين العمود ونصف حبرها فوق أعلى الرمز الأخير (clreq §5.5.3.3)، وتُوضع وفق حبرها لأن الخطوط تضع هذه العلامات عالية في مربع em الخاص بها، وتقف في النص العمودي قائمة، شأنها شأن نقطة النبرة المحايدة فوق الرمز الأول.
- حواشي الواريتشو (双行夹注) تنطوي في صفّين بحجم الحاشية، متوسطين على السطر بلا فراغ بينهما. يأخذ الصف العلوي حروفًا حتى يضم نصف الجزء على الأقل، فلا يكون الصف الثاني أبدًا هو الأطول، ثم حرفًا آخر ما دام الصف السفلي سيبدأ بعلامة لا يجوز أن تبدأ سطرًا. والحاشية الأطول من المساحة الباقية في السطر تملؤها وتواصل في السطر أو العمود أو الصفحة التالية؛ ولا يوضع قوساها إلا قبل صفّها الأول وبعد صفّها الأخير. وفي النص العمودي يكون الصف العلوي هو الأيمن، ويُقرأ أولًا.
لا تتغيّر خطوة الأسطر أبدًا: تسكن العلامات والقراءات في تباعد الأسطر. والفقرة التي يقل فيها الفراغ بين الأسطر (ارتفاع السطر ناقصًا حجم النص) عن نصف em مع علامات على جانب واحد، أو عن خمسة أثمان em مع علامات على الجانبين (clreq §5.6.1)، يُبلغ عنها بتحذير محتوى cjkMarksExceedLeading؛ والفقرة التي تكون قراءاتها أطول من فراغها (وتُحتسب قراءة البينيين مع ارتفاعها) بالتحذير rubyExceedsLeading. أعطِ هذه الفقرات نمط فقرة بتباعد أسطر أكبر.
في VDT يحمل المقطع المُعلَّم cjkMarks، ويضع التخطيط النقاط والخطوط على أسطره في VDTLine.marks (نقاط، ودوائر، ونقاط سمسمية، وخطوط مستقيمة ومتموّجة، في إطار تدفّق السطر)، وترسمها Canvas وHTML وPDF كما هي (PDF: Artifact /Layout؛ HTML: صناديق aria-hidden، والنص المنقّط في <em>). ويحمل مقطع أساس الروبي ruby (القراءة وسلاسلها) ويرسم أساسه عند inkOffset حين توزّعه القراءة؛ وجزء حاشية الواريتشو في السطر مقطع واحد text فيه صفّه العلوي ثم السفلي، وwarichu فيه الصفّان اللذان ترسمهما المُخرِجات بدلًا منه. ويضع PDF الموسوم القراءة في RT ضمن Ruby أساسها والحاشية في Warichu، ويقرأ /ActualText للسطر النص الأساسي والحاشية مرة واحدة. وتُرسم العلامات والقراءات والحواشي في نص المتن والعناوين والقوائم والإطارات؛ أما التعليقات وخلايا الجداول ونص التصميم فتحتفظ بالنص دونها.
دالة الحل (resolver) ودالة التجريد (stripper) على نمط الأقسام الأخرى؛ وتضبط setCjkLineBreak وsetCjkComposition المستوى والتركيب (عروض علامات الترقيم، والتدلّي، ومسافة الهان واللاتيني؛ cjkCompositionOf(resolved.cjk, dpi)) للقياسات التي تُجرى خارج buildDocument، الذي يضبطهما من الإعدادات. وخارج البناء، تحتفظ العلامات بتقدّمها الكامل ولا توضع مسافة بين الهان واللاتيني:
import { DEFAULT_CJK_CONFIG, resolveCjkConfig, stripCjkDefaults, cjkRegionOf, setCjkLineBreak, setCjkComposition, cjkCompositionOf } from 'postext';
resolveCjkConfig(undefined, 'zh-HK');
// { region: 'hongkong', lineBreak: 'basic', punctuationWidth: 'fullwidth', compressAdjacent: true,
// trimLineStart: true, hangingPunctuation: 'none', latinSpacing: { value: 0.25, unit: 'em' }, uprightDigits: 2,
// grid: { enabled: false, charsPerLine: 0, linesPerPage: 0, show: false },
// emphasis: 'dots', bookTitleMark: 'wavy',
// ruby: { fontSize: { value: 0.5, unit: 'em' }, position: 'auto' },
// warichu: { fontSize: { value: 0.5, unit: 'em' }, open: '', close: '' } }في Sandbox توجد هذه الإعدادات في التصميم › نظام الكتابة › تنضيد لغات شرق آسيا.
#أنواع الموارد
نوع المورد فئة يحدّدها المستخدم — شكل، جدول، مخطط، شيفرة… — تتحكم في طريقة ترقيم موارد هذا النوع ووضع تعليقاتها والإحالة إليها. تقع القائمة في config.resourceTypes؛ ويحرّرها Sandbox في التصميم › الأشكال والجداول › الترقيم والموضع.
حين لا يُضبط config.resourceTypes، يأتي Postext بنوعين افتراضيين مدمجين: شكل وجدول، يُرقَّم كلاهما {h1}.{n} (ويُعاد العدّ عند كل عنوان من المستوى 1) بعدّادات عشرية. ويُسمَّيان بلغة المستند: config.locale، وإلا bodyText.hyphenation.locale، وإلا الإنجليزية (انظر لغة المستند).
الأنواع الافتراضية المدمجة تراعي اللغة. فالدالة المصدَّرة defaultResourceTypes(locale = 'en') تترجم أسماء الأنواع والتسميات المختصرة وبادئات التعليقات إلى لغة المستند — تعطي الإنجليزية Figure/Fig. وTable/Tab.؛ وتعطي الإسبانية Figura/Fig. وTabla/Tabla؛ وللفرنسية والألمانية والإيطالية والبرتغالية والكتالونية والهولندية تسمياتها أيضًا (يسردها الجدول في لغة المستند). وتُحلّ الوسوم الإقليمية مثل es-ES بحسب اللغة، وأي لغة بلا ترجمات تعود إلى الإنجليزية. أما سلوك الترقيم (numberingTemplate: '{h1}.{n}'، resetOn: 'h1'، العدّادات العشرية) فمستقل عن اللغة. وكل استدعاء يعيد كائنات جديدة، فيمكنك تعديل النتيجة بحرية:
import { defaultResourceTypes } from 'postext';
const types = defaultResourceTypes('es');
// => [{ id: 'figure', name: 'Figura', shortLabel: 'Fig.', captionPrefix: 'Figura',
// numberingTemplate: '{h1}.{n}', resetOn: 'h1', counterFormat: 'decimal', … },
// { id: 'table', name: 'Tabla', shortLabel: 'Tabla', captionPrefix: 'Tabla', … }]type ResourceCounterFormat =
| 'decimal'
| 'roman-lower'
| 'roman-upper'
| 'alpha-lower'
| 'alpha-upper';
type ResourceCounterReset = 'never' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
interface ResourcePlacement {
position?: 'auto' | 'top' | 'bottom' | 'here'; // which free slot a float may take; 'here' = inline embed at the ::resource directive
span?: 'column' | 'page' | 'side'; // one column, the full content width, or the float-only side column
rotate?: 'ccw' | 'cw'; // a quarter turn: a landscape table on a page of its own
width?: number; // fraction (0 < width < 1) of the column or page width; default: the whole width
align?: 'left' | 'center' | 'right'; // where a float narrower than its column sits; default 'left'
captionSide?: boolean; // caption beside the figure, in the side column of a oneAndHalf layout (column floats only)
}
interface ResourceType {
id: string; // stable id, referenced by Resource.typeId
name: string; // singular display name, e.g. "Figure"
namePlural?: string; // optional plural, e.g. "Figures"
shortLabel: string; // compact label for inline refs, e.g. "Fig."
numberingTemplate: string; // "{h1}.{n}" or "{n}"
resetOn: ResourceCounterReset; // when the {n} counter resets
counterFormat: ResourceCounterFormat;// how {n} is formatted
captionPrefix: string; // prepended to the caption, e.g. "Figure"
defaultPlacement?: ResourcePlacement;// fallback placement for this type's resources
}ResourcePlacement هو الشكل نفسه الذي يضبطه المورد في placement الخاص به. يختار position نوع الموضع الشاغر الذي يجوز للعنصر العائم أن يشغله — auto (الافتراضي) يأخذ أول موضع بعد الإحالة الأولى، وtop / bottom يقصرانه على هذا النوع من الأشرطة، وhere يُدرج المورد في مكانه ضمن النص. ويحدّد span امتداد العنصر العائم: عمود واحد، أو عرض المحتوى كاملًا، أو العمود الجانبي المخصص للعناصر العائمة في تخطيط العمود ونصف العمود. ويدير rotate المورد ربع دورة ويجعله عنصرًا عائمًا بعرض الصفحة في صفحة مستقلة. ويضيّق width العنصر العائم إلى كسر من عموده (أو من الصفحة للعنصر العائم بعرض الصفحة) — جدول صغير في عمود عريض مثلًا. ويحدّد align موضع هذا العنصر العائم الأضيق — إلى اليسار افتراضيًا، أو في الوسط، أو إلى اليمين — وموضع الصورة الأضيق من موضعها داخله: صورة نقطية أصغر من العمود، أو صورة صغّرها layout.fitFiguresToPage. ويحتفظ التعليق والملاحظة بعرض الموضع. (حتى postext 1.4 كانت هذه الصورة تُنضَّد دائمًا ملاصقة لليسار.) ويضع captionSide التعليق بجانب الشكل في العمود الجانبي المخصص للعناصر العائمة في تخطيط العمود ونصف العمود (layout.sideColumnRole: 'floats')، بمحاذاة أعلى الشكل (أو أسفله للعنصر العائم في أسفل الصفحة)؛ ولا ينطبق إلا على العناصر العائمة بعرض العمود، والصفحة التي ليس فيها عمود كهذا تُبقي التعليق تحت الشكل. وحين لا يضبط المورد ولا نوعه موضعًا، يكون الافتراضي المدمج auto / column.
| الخاصية | النوع | الوصف |
|---|---|---|
id | string | معرّف ثابت يشير إليه typeId في كل مورد. يُضبط مرة واحدة عند إنشاء النوع؛ وحذف نوع لا تزال موارد تشير إليه يثير تحذير نوع معلّق. |
name | string | اسم العرض بالمفرد. تستخدمه الإحالة السطرية style="full" (مثلًا Figure 1.7). |
namePlural | string (اختياري) | اسم العرض بالجمع، لتسميات الواجهة وقوائم الموارد. |
shortLabel | string | اختصار موجز تستخدمه طريقة الإحالة السطرية الافتراضية (مثلًا Fig. 1.7). |
numberingTemplate | string | قالب الرقم المحسوب. انظر رموز القالب أدناه. والأشكال الشائعة (ضمن الفصل، مثلًا 2.3) و (عدّ متصل واحد). |
resetOn | ResourceCounterReset | تعطي 'never' عدًّا متصلًا واحدًا على امتداد المستند؛ وتعيد 'h1'..'h6' ضبط العدّاد كلما صودف عنوان من ذلك المستوى (أو من أي مستوى أعلى منه). اضبطها لتطابق مستوى العنوان الوارد في القالب — مثلًا مع resetOn: 'h1'. |
counterFormat | ResourceCounterFormat | طريقة عرض العدّاد : عشري (1, 2, 3)، أو روماني صغير/كبير (i, ii / I, II)، أو أبجدي صغير/كبير (a, b / A, B). وتُقبل أيضًا الكتابات المستخدمة للصفحات والقوائم ('lower-roman'، 'arabic'…؛ انظر كتابات صيغ الترقيم)؛ والقيمة غير المعروفة تعدّ بالعشري ويُبلغ عنها. أما رموز العناوين (…) فتُعرض دائمًا أعدادًا عشرية. |
captionPrefix | string | النص الذي يُضاف قبل تعليق الشكل/الجدول. يلي الرقمُ المحسوب البادئةَ — فيُعرض التعليق على هيئة . ، مثلًا الشكل 1.7. المخطط الأصلي. والنوع الذي يكون numberingTemplate فيه فارغًا لا رقم له، ويُقرأ تعليقه . . وتُحذف المسافات في نهاية البادئة، والبادئة التي تنتهي أصلًا بـ . أو : أو ! أو ? أو … (أو بصيغتها كاملة العرض) لا تأخذ نقطة ثانية: Pl. Lines at 0°. |
defaultPlacement | ResourcePlacement (اختياري) | الموضع الذي تستخدمه موارد هذا النوع التي لا تضبط placement الخاص بها: position وspan وrotate وwidth وalign وcaptionSide، ويُحلّ كل منها مستقلًا. وحين لا يضبط المورد ولا النوع حقلًا ما، ينطبق الافتراضي المدمج: auto / column، قائمًا، بالعرض الكامل، محاذيًا لليسار، والتعليق تحت الشكل. انظر الترقيم والإحالات أدناه لسلسلة الحل، وصيغة المستند › الموارد لما تفعله كل قيمة، بما فيها الموارد المُدارة. |
captionStyle | CaptionStyleConfig (اختياري) | تجاوز جزئي لـنمط التعليق لموارد هذا النوع. لا تحلّ محل captionStyle العام إلا المفاتيح التي تضبطها؛ ويُورث كل ما عداها (وcolor المتجاوَز يحدّد أيضًا لونَي التسمية والملاحظة ما لم يُضبطا صراحة). الاستخدام المعتاد: جداول تعليقها فوقها على شريط ملوّن بينما تُبقي الأشكال تعليقها تحتها. وتُحلّ مراجع لوحة الألوان كأي لون آخر. |
#رموز القالب
يُعرض numberingTemplate بالمحرّك نفسه الذي يرقّم العناوين (انظر العناوين). ويتعرّف على نوعين من الرموز:
{n}— عدّاد النوع، منسَّقًا وفقcounterFormat. وهي القيمة التي تزيد مع كل مورد ويُعاد ضبطها وفقresetOn.{h1}…{h6}— أرقام العناوين السارية عند موضع الإحالة الأولى، وتُعرض دائمًا أعدادًا عشرية.{h1}رقم العنوان الحالي من المستوى 1، و{h2}من المستوى 2، وهكذا.
وأي نص آخر يُطبع حرفيًا. والشرطة المائلة العكسية تجعل { أو } أو \ حرفًا عاديًا. وحين لا تكون لرمز عنوان قيمة في النطاق (مثل {h1} قبل أي عنوان من المستوى 1)، يُحذف مع الفاصل المجاور له — فيؤول {h1}.{n} إلى العدّاد وحده دون خلل.
القالب الفارغ ('') لا يطبع رقمًا، وإن ظل النوع يعدّ موارده: يُقرأ التعليق Do. Lines at 0° وتطبع :ref التسمية وحدها (Do). وحتى postext 1.4 كان هذا التعليق يُقرأ Do .Lines at 0° وكانت الإحالة تنتهي بمسافة غير قابلة للكسر.
| القالب | مع h1 = 2، والعدّاد = 3 | ملاحظات |
|---|---|---|
| 3 | عدّ متصل واحد. استخدمه مع resetOn: 'never'. |
| 2.3 | ضمن الفصل. استخدمه مع resetOn: 'h1'. |
| 2.0.3 | ضمن القسم. استخدمه مع resetOn: 'h2'. |
#الترقيم والإحالات
الرقم الذي ينتجه نوع المورد هو ما تطبعه :ref وما تسبقه بادئة التعليق. والصيغة :ref{id} هي الصيغة الأساسية: الإحالة الأولى بترتيب القراءة تُدرِج المورد، فيطفو إلى أول موضع شاغر بعد تلك الإحالة — أسفل العمود المُحيل، أو أعلى العمود الفارغ التالي أو أسفله، أو شريط في الصفحة التالية (وفق موضعه المحلول — position: 'auto' | 'top' | 'bottom' | 'here' وspan: 'column' | 'page' | 'side'، إضافة إلى rotate وwidth وalign وcaptionSide، تُحلّ لكل مورد، ثم من defaultPlacement الخاص بالنوع، ثم من الافتراضي المدمج auto / column؛ و'top' / 'bottom' يقصران البحث على هذا النوع من المواضع). أما التضمين الكتلي ::resource{id} فاختياري، ولا يلزم إلا مع placement.position: 'here' — تضمين سطري غير عائم عند نقطة محددة في التدفّق. والقواعد الكاملة من جهة المستند — الصيغتان وخيارا style وtext في :ref — موثّقة في صيغة المستند › الموارد، بما في ذلك كيف يحدّد ترتيب الإحالات الأولى العدّ.
وهذا يماثل ترقيم العناوين: فكما يحمل مستوى العنوان numberingTemplate، يحمل نوع المورد واحدًا أيضًا — لكن عدّاد المورد ({n}) يتقدّم مع كل إحالة أولى لا مع كل عنوان، وresetOn يربطه من جديد بتسلسل العناوين.
#ما الذي يُرقَّم
يُرقَّم المورد حين يحيل إليه النص — بـ :ref أو بتضمين ::resource — بترتيب تلك الإحالات الأولى، أيًّا كان موضعه: عائمًا، أو سطريًا (here)، أو في العمود الجانبي، أو مُدارًا. والمورد الذي لا يرسمه إلا تصميم — عنصر image في صفحة افتتاح الفصل، أو في ترويسة، أو في صفحة جزء — أو الذي لا يحيل إليه شيء، لا يأخذ رقمًا ولا يُقدّم عدّاد نوعه. ففي مقال مصوّر لوحاته الممتدة حتى حافة النزف صورٌ لصفحات الافتتاح ولوحته الأصغر الوحيدة عنصر عائم مُحال إليه، تكون تلك اللوحة العائمة هي اللوحة I، مهما عرضت صفحات الافتتاح من لوحات قبلها؛ رقّم لوحات الافتتاح في تصميمها (بسمة مثل {attr.plate}) واحتفظ بالعدّاد للوحات التي يستشهد بها النص.
في كتاب يُخرَج فصلًا فصلًا (Sandbox، أو buildBundle، أو buildDocument مع العدّادات التي تمرّرها continuationAfter())، تكون الإحالة الأولى في الكتاب كله هي التي تُحتسب: يحتفظ المورد بالرقم الذي أخذه في الفصل الذي يحيل إليه أولًا، ولا يضعه إلا ذلك الفصل. وتطبع :ref في فصل لاحق ذلك الرقم ولا تضع شيئًا، وتضمين ::resource لمورد عائم هناك ليس إلا إحالة أخرى (أما التضمين السطري here فيُنضَّد حيث كُتب). وترتبط هذه الإحالة بالشكل حين يكون الشكل في المُخرَج نفسه: فملف PDF للكتاب كله يربطها بصفحة الفصل السابق. أما الفصل المعروض وحده، في HTML أو PDF، فينضّدها نصًا عاديًا بلون الروابط، لأن شكلها ليس في ذلك المستند. والمضيف الذي يضم HTML الفصول في صفحة واحدة يمرّر إلى renderToHtml الموارد التي ترسيها الفصول، في refTargets، فترتبط هذه الإحالة بشكل الفصل السابق من جديد:
import { anchoredResourceIds, buildBundle, renderToHtml } from 'postext';
const docs = buildBundle(bundle);
const refTargets = new Set(docs.flatMap((d) => [...anchoredResourceIds(d)]));
const html = docs.map((d) => renderToHtml(d, { refTargets })).join('');تغيّر في postext 1.5: حتى 1.4 كان كل فصل يحيل إلى شكل يعوّمه من جديد، وكان HTML كل :ref رابطًا، سواء أكان شكله في الصفحة أم لا.
{h1} هو العدّ المتصل للعناوين من المستوى 1: يتقدّم مع كل H1 ما لم يضبط نمط عنوانه numbered: false — فـ numberingTemplate الفارغ يُخفي رقم العنوان ولا يوقف العدّ. لذلك يرقّم المقال الذي ليس فيه H1 سوى عنوانه أشكاله 1.1، 1.2… مع النوعين المدمجين {h1}.{n}. وهناك طريقتان لطباعة الشكل 1، 2…:
- نوع مرقّم
{n}معresetOn: 'never'(معresetOn: 'h1'يبدأ العدّ من جديد عند كل H1)؛ - نمط عنوان مع
numbered: falseعلى العنوان الرئيسي، وعلى أي H1 آخر لا ينبغي أن يُحتسب: هذا العنوان لا يقدّم{h1}بل يتركه كما كان — فارغًا قبل أول H1 محتسب، حيث يؤول{h1}.{n}إلى العدّاد وحده — ولا يطلقresetOn: 'h1'أبدًا، فيستمر العدّ عبره. بعد# Introductionوالشكل 1.1 فيه، يكون أول شكل تحت# Appendixغير المرقّم هو 1.2، لا 2.1.
// Figure 1, 2, 3… in a single-article document
resourceTypes: defaultResourceTypes('en').map((t) => ({ ...t, numberingTemplate: '{n}', resetOn: 'never' })),#نمط الجداول
تتحكم الخاصية tableStyle في خطوط موارد الجداول وزخرفتها، أي في كل جدول ما لم يختر نمط جدول مسمّى. تُنسَّق خلايا المتن وخلايا الرأس كلٌّ على حدة. وحين لا تُحدَّد عائلة الخط وحجمه وألوانه، تُورَث من نص المتن بعد حسمه، فالمستند الذي ليس فيه tableStyle يرسم جداوله بخطوط نص المتن.
const config: PostextConfig = {
tableStyle: {
headerBold: true,
headerBackground: { hex: '#f0f0f0', model: 'hex' },
borders: true,
borderWidth: { value: 0.75, unit: 'pt' },
},
};| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
bodyFontFamily | string | خط نص المتن | عائلة الخط لخلايا المتن. |
bodyFontSize | Dimension | حجم نص المتن | حجم الخط لخلايا المتن. |
bodyColor | ColorValue | لون نص المتن | لون النص في خلايا المتن. |
headerFontFamily | string | خط نص المتن | عائلة الخط لخلايا الرأس. |
headerFontSize | Dimension | حجم نص المتن | حجم الخط لخلايا الرأس. |
headerColor | ColorValue | لون نص المتن | لون النص في خلايا الرأس. |
headerBold | boolean | true | ارسم خلايا الرأس بخط غامق. |
headerItalic | boolean | false | ارسم خلايا الرأس بخط مائل. |
headerLetterSpacing | Dimension | 0pt | التتبّع (tracking) بعد كل حرف من خلية رأس، بما في ذلك المسافات، كما في الخاصية letter-spacing في CSS. القيم الموجبة تباعد بين الحروف (رأس بالحروف الكبيرة يحتاج عادةً من 0.05em إلى 0.1em)، والسالبة تقاربها. وحدة em هي حجم خط الرأس. تُقاس أسطر الرأس مع التتبّع، فتلتف وتتوسّط وتُحاذى وهو محسوب فيها، وترسمه Canvas وHTML وPDF على النحو نفسه. ويسري على كل خلية رأس: صفوف الرأس وأي خلية معلَّمة بـ isHeader. |
headerTextTransform | 'none' | 'uppercase' | 'none' | نضّد خلايا الرأس بالحروف الكبيرة. يحتفظ النص بطوله، فيبقى Sandbox قادرًا على ربط كل حرف بموضعه في المصدر: الحرف الذي صيغته الكبيرة أطول (ß) يبقى كما هو. وتحتفظ الإحالات إلى الموارد بتسميتها. |
headerBackgroundEnabled | boolean | true | ارسم تعبئة خلف صف الرأس. |
headerBackground | ColorValue | #f0f0f0 | لون تعبئة صف الرأس. |
bodyBackgroundEnabled | boolean | false | ارسم تعبئة خلف صفوف المتن. |
bodyBackground | ColorValue | #ffffff | لون تعبئة صفوف المتن (لا يُرسم إلا عند التفعيل). |
bodyAlternateBackgroundEnabled | boolean | false | صفوف متناوبة الألوان: املأ كل صف ثانٍ من صفوف المتن بـ bodyAlternateBackground. انظر الصفوف المتناوبة الألوان. |
bodyAlternateBackground | ColorValue | #f2f2f2 | تعبئة صفوف المتن المتناوبة (لا تُرسم إلا عند التفعيل). |
borders | boolean | true | ارسم حدود الخلايا. |
borderColor | ColorValue | لون نص المتن | لون خط الحدود. |
borderWidth | Dimension | 0.75pt | سُمك خط الحدود (≈1px عند 96 DPI؛ يتغيّر مع دقة الصفحة DPI). |
cellPadding | Dimension | 0.375em | الحشوة الداخلية لكل خلية. |
rules | 'grid' | 'horizontal' | 'outer' | 'none' | 'grid' | أي الخطوط تُرسم حين تكون borders مفعّلة: شبكة الخلايا كاملة، أو الخطوط الأفقية وحدها (الحافة العليا والسفلى لكل صف، بلا خطوط عمودية)، أو الإطار الخارجي وحده، أو لا شيء. |
borderRadius | Dimension | 0 | نصف قطر زوايا الإطار الخارجي للجدول. يُرسم الإطار مستدير الزوايا (مع الخطوط grid أو outer)، وتُقَصّ عليه تعبئات الخلايا وخلفية الرأس، حتى مع rules: 'none' أو مع تعطيل الحدود، وتُشذَّب الخطوط الأفقية عند محيطه الخارجي؛ أما الخطوط الداخلية فتبقى مستقيمة. والجدول المقسوم على عدة صفحات يستدير الزاويتان العلويتان من جزئه الأول والسفليتان من جزئه الأخير. لا تتجاوز القيمة نصف عرض الجدول ونصف ارتفاعه. |
overflow | 'split' | 'clip' | 'hide' | 'split' | مصير الجدول الأطول من الصفحة: يستمر في الصفحات التالية، أو تُبقى الصفوف التي تتسع فقط، أو يُترك كله. انظر أدناه. |
continuedSuffix | string | '(cont.)' | يُلحق بخط مائل بتعليق كل جزء تالٍ من جدول مقسوم، بعد مسافة؛ واللاحقة التي تبدأ بحرف صيني أو بحرف كامل العرض ('(续)') تُنضَّد ملاصقة للتعليق بلا مسافة. |
continuesMarkerEnabled | boolean | true | ضع علامة تحت كل جزء يستمر في الصفحة التالية. |
continuesMarker | string | 'Continued' / 'Continúa' | نص تلك العلامة، يُنضَّد محاذيًا إلى اليمين تحت الجزء بخط الملاحظة (انظر نمط التعليقات). تتبع القيمة الافتراضية لغة المستند (انظر لغة المستند للغات الثماني). |
يُحتفظ بسُمك الحدود كسريًا: الخط الذي سُمكه 0.5pt يُرسم خطًا شعريًا في PDF وعلى الشاشة بدل أن يُقرَّب إلى بكسل كامل (الحد الأدنى 0.25px).
#الصفوف المتناوبة الألوان
يسهل تتبّع الجداول الطويلة للبيانات أفقيًا حين يُلوَّن كل صف ثانٍ. يفعّل bodyAlternateBackgroundEnabled الأشرطة، ويحدد bodyAlternateBackground لونها:
const config: PostextConfig = {
tableStyle: {
bodyBackgroundEnabled: true,
bodyBackground: { hex: '#ffffff', model: 'hex' },
bodyAlternateBackgroundEnabled: true,
bodyAlternateBackground: { hex: '#eef3fa', model: 'hex' },
},
};تُعدّ الصفوف بدءًا من أول صف بعد صفوف الرأس (TableModel.headerRowCount، أو الصفوف الأولى المكوّنة من خلايا رأس): يحتفظ ذلك الصف بـ bodyBackground، أو يبقى بلا تعبئة ما دام bodyBackgroundEnabled معطّلًا، ويأخذ الصف التالي التعبئة المتناوبة، وهكذا. يتبع العدُّ نموذجَ الجدول لا الصفحة، فالجدول المقسوم على عدة صفحات يحتفظ بشريط كل صف في كل صفحة، والخلية المدموجة عبر عدة صفوف تأخذ شريط صفها الأول. تحتفظ خلايا الرأس بتعبئة الرأس، وتتقدّم خلفية الخلية الخاصة background على الاثنتين، واللون المرتبط بلوحة الألوان يتبع لوحة الألوان. ونمط الجدول المسمّى يحدد هذين الحقلين كأي حقل آخر، فيمكن أن يكون نمط واحد مخطّطًا وجداول المستند الأخرى غير مخطّطة؛ وهما في Sandbox مفتاح صفوف متناوبة الألوان ولونه، تحت خلايا المتن.
في الـ VDT تحمل خلايا الصفوف المتناوبة alternate: true، ويحمل تخطيط الجدول bodyAlternateBackground. تعيد tableCellFill(table, cell) التعبئة التي تُرسم بها الخلية، أي تعبئتها الخاصة أو تعبئة الرأس أو التعبئة المتناوبة أو تعبئة المتن، وهي ما ترسمه المُخرِجات الثلاثة: Canvas وHTML وPDF. وتلتقي التعبئات المتجاورة بلا فاصل: المتصفح عند نسبة بكسل كسرية أو عارض PDF ينعّم حواف كل تعبئة وحدها فيترك الصفحة تظهر في خط شعري بين خليتين، لذلك يرسم مُخرِجا HTML وPDF tableCellFillRects(table)، أي تعبئة كل خلية مع شريط يمتد عبر كل حافة تشترك فيها مع خلية تُرسم بعدها فتغطيه تلك الخلية، ويُحاذي Canvas تعبئاته على بكسلات الجهاز.
#أنماط الجداول المسمّاة
قلّما يضبط المستند كل جداوله على هيئة واحدة: قائمة تحقق في شبكة كحلية بإطار مستدير، وصف خيارات لا يحيط به إلا إطاره الخارجي، وجدول بيانات بخطوط أفقية بسيطة. تعلن tableStyles متغيّرات مسمّاة، ويختار مورد الجدول واحدًا منها بـ table.styleId. وكل حقل لا يحدده النمط يُقرأ أولًا من tableStyle ثم من نص المتن، فلا يذكر النمط إلا ما يميّز جداوله. والجدول الذي ليس له styleId، أو له معرّف لا يعلنه أي نمط، يبقى على tableStyle، والمستند الذي ليس فيه tableStyles يُرسم تمامًا كما كان من قبل.
const config: PostextConfig = {
tableStyle: {
borderColor: { hex: '#163a76', model: 'hex' },
borderWidth: { value: 1.3, unit: 'pt' },
borderRadius: { value: 10, unit: 'pt' },
},
tableStyles: [
{
id: 'option',
name: 'Option row',
rules: 'outer',
borderColor: { hex: '#7a9cc6', model: 'hex' },
borderWidth: { value: 1, unit: 'pt' },
borderRadius: { value: 8, unit: 'pt' },
headerBackgroundEnabled: false,
},
],
};
// In the resources: this table is set in the "option" style.
const resource: Resource = {
id: 'choices', typeId: 'table', kind: 'table', createdAt: 0, updatedAt: 0,
table: { model: { rows: [/* … */] }, styleId: 'option' },
};يأخذ كل مدخل كل حقول tableStyle إضافةً إلى id (ما تشير إليه table.styleId) وname اختياري للمحرّر (قيمته الافتراضية هي المعرّف). وكل ما يستطيع النمط تحديده يسري على كل جدول على حدة: الخطوط، والتعبئات، والحدود، والخطوط الفاصلة، ونصف قطر الزوايا، والحشوة، وسلوك الفيض مع نصوص الاستمرار. تعيد resolveTableStylesConfig(styles, tableStyle, resolvedBodyText, locale?) القائمة المحسومة، وتعيد pickTableStyle(resolved, styleId) النمط الذي يُنضَّد به الجدول، وتحذف stripTableStylesDefaults الحقول غير المحددة (وتُبقي الحقل المساوي لقيمته الافتراضية المدمجة، لأنه يتقدّم على قيمة مختلفة في tableStyle).
#الجداول الأطول من الصفحة
الجدول العائم الذي لا يتسع في الصفحة الجديدة المعروضة عليه لا يُضغط ولا يفيض: مع overflow: 'split' (القيمة الافتراضية) يقطعه المحرّك بين الصفوف عند آخر حافة تتسع في الصفحة ويتابعه في الصفحات التالية، مهما بلغ عددها. يكرر كل جزء تالٍ صفوف رأس الجدول (TableModel.headerRowCount، أو الصفوف الأولى المكوّنة من خلايا رأس حين لا تكون محددة) ويحمل التعليق من جديد مع continuedSuffix بعد الوصف: «Table 6-4. Title (cont.)». وكل جزء يستمر تحته continuesMarker، محاذيًا إلى اليمين، بخط الملاحظة؛ وتُؤجَّل ملاحظة الجدول إلى الجزء الأخير. لا يمر القطع أبدًا عبر خلية مدموجة (الخلية الممتدة على عدة صفوف تنتقل كاملة إلى الجزء التالي)، والصف الذي يتصدّر الصفوف التي تحته، أي خلية واحدة تمتد على عرض الجدول كله، يُنقل إلى الجزء التالي بدل أن يبقى معزولًا في أسفل الصفحة.
أين ينتهي الجزء الأول. الجدول الذي يُعرض عليه رأس عمود فارغ بعد الإحالة إليه يأخذ الصفوف التي تتسع هناك ويستمر في الموضع التالي. ويملأ العمود حتى أسفله حين ينفرد بالعمود: الجزء الذي سيترك تحته أقل من ثلاثة أسطر من نص المتن يأخذها هي أيضًا، بدل أن يترك بقية قصيرة من النص. وحين يضم العمود شريطًا عائمًا آخر، كشكل بعرض الصفحة في رأسها مثلًا، يتوقف الجزء قبل أسفل العمود بثلاثة أسطر من نص المتن على الأقل، وهي المساحة التي يتركها أي عنصر عائم للنص حين يشارك عنصرًا آخر عمودًا واحدًا، فينتهي العمود بشيء من النص تحت الجدول لا بعناصر عائمة وحدها. ولكي يمتد جدول طويل إلى أسفل عموده، أحِل إليه في موضع لا تحمل فيه الصفحة التي يبدأ فيها أي عنصر عائم آخر (بعد صفحة شكل بعرض الصفحة مثلًا)، أو اجعل صفوفه تتسع في العمود.
يُبقي 'clip' الصفوف الأولى التي تتسع في الصفحة ويحذف الباقي بلا تنبيه (ومع ذلك تختم الملاحظة الجزء)؛ ويترك 'hide' الجدول كله. ولا يسري أيٌّ منهما إلا حين يكون الجدول أطول من صفحة: الجدول الذي يتسع يوضع كاملًا في أي وضع. ولا تُقسم الجداول المضمّنة في النص (placement.position: 'here').
تتبع نصوص الاستمرار الافتراضية لغة المستند (locale، وإلا فلغة تقسيم الكلمات): في الإنجليزية (cont.) / Continued، وفي الإسبانية (cont.) / Continúa، وعلى المنوال نفسه في الفرنسية والألمانية والإيطالية والبرتغالية والكتالونية والهولندية (مذكورة تحت لغة المستند).
محتوى الخلية Markdown مضمّن، وفاصل السطر داخل الخلية، سواء كان سطرًا جديدًا أو \\ كما في التعليقات والملاحظات، يبدأ فقرة جديدة. والفقرة التي تبدأ بنقطة تعداد أو شرطة (•، -، *، –) أو برقم (1.، 1)) تليها مسافة تُنضَّد عنصرَ قائمة: تُرسم العلامة كما كُتبت، ويتعلّق النص بها بمسافة unorderedLists.gap الخاصة بالمستند، وتُحاذى الأسطر الملتفّة مع النص، ومسافتان في البداية تزيدان مستوى التداخل. فالخلية المكتوبة هكذا • Ofrece elección\n• Acomoda a personas diestras y zurdas تخرج قائمة من عنصرين. السطر الذي ليس فيه إلا مسافات عادية لا يضيف شيئًا؛ أما السطر الذي يضم مسافة غير منقسمة (U+00A0) فهو سطر من الخلية، كما في CommonMark، فـ 1\n تليها مسافة غير منقسمة تجعل الصف بارتفاع سطرين. والمسافة غير المنقسمة في آخر نص الخلية تحتفظ بعرضها: 760 تليها مسافة غير منقسمة، محاذاةً إلى اليمين فوق (231)، تنتهي قبل الحافة بمسافة، فيقترب الـ 0 من الـ 1. ولا تصطف الأرقام تمامًا إلا حيث تكون المسافة بعرض القوس، وهي في أغلب الخطوط أضيق منه. (حتى postext 1.4 كانت الاثنتان تُحذفان.)
عروض الأعمدة جزء من نموذج الجدول لا من النمط: TableModel.columnWidths مصفوفة اختيارية من الأوزان النسبية، وزن لكل عمود، تُطبَّع عند الإخراج، فـ [2, 1, 1] تعطي العمود الأول نصف العرض. وحين تغيب المصفوفة، أو يكون طولها خاطئًا، أو يكون أحد أوزانها غير موجب، يُقسم العرض بالتساوي. ويحافظ محرّر الجداول على توافق المصفوفة حين تُضاف أعمدة أو تُزال.
#بناء نماذج الجداول
TableModel شبكة مرتبة بالصفوف، وكل خلية تُخرَج بحسب موضعها فيها: rows[r][c] تقع في العمود c. لذلك تُبقي الخلية المدموجة الخلايا التي تغطيها في الشبكة، وكل منها معلَّمة بـ hiddenBy تشير إلى خليتها الأساسية، بخلاف جدول HTML الذي يحذفها. وتحافظ دوال النموذج المصدَّرة من postext على هذا الشكل؛ وهي دوال نقية تعيد نموذجًا جديدًا: mergeCells(model, { start, end }) وunmergeCell(model, at)، وaddRow، وaddColumn، وremoveRow، وremoveColumn، وsetCellContent، وsetCellImage، وsetCellBackground، وsetAlignment. والدوال الأربع الخاصة بالصفوف والأعمدة تُبقي الدمج سليمًا: الصف أو العمود المضاف داخل كتلة مدموجة يوسّعها، والمضاف قبلها يزيحها، والمُزال منها يقلّصها، والكتلة التي تفقد صفها أو عمودها الأول تحتفظ بمحتواها في خليتها العليا اليسرى الجديدة، وتبقى كل hiddenBy مشيرة إلى خليتها الأساسية.
تبني parseTSV(text, options?) نموذجًا من نص مفصول بعلامات الجدولة، أي نطاقًا ملصوقًا من جدول بيانات: تُقسم الصفوف عند فواصل الأسطر، والخلايا عند علامات الجدولة، وتُكمَّل الصفوف القصيرة لتكون الشبكة مستطيلة. ويحوّل headerRows الصفوف الأولى إلى صفوف رأس: تأخذ خلاياها isHeader ويأخذ النموذج headerRowCount، فيكررها الجدول المقسوم على عدة صفحات.
import { parseTSV, mergeCells } from 'postext';
let model = parseTSV('Part\tQty\tNote\nBolt\t4\tM6\nNut\t8\t', { headerRows: 1 });
// model.headerRowCount === 1; model.rows[0][0] is { content: 'Part', isHeader: true }
model = mergeCells(model, { start: { row: 2, col: 1 }, end: { row: 2, col: 2 } });
// rows[2][1] gets colSpan: 2; rows[2][2] stays in the grid with hiddenBy: { row: 2, col: 1 }تفحص tableGridIssues(model) الشبكة. تعيد قائمة فارغة للنموذج السليم، وإلا فكل موضع تنكسر فيه الشبكة، بترتيب الصفوف: spanOverlap، أي خلية ظاهرة تحت colSpan / rowSpan لخلية أخرى (تسمّي coveredBy تلك الخلية)، وهو ما تفعله خلية مغطّاة حُذفت على طريقة HTML، لأن كل خلية بعدها تنزاح إلى موضع الدمج؛ وmissingCells، أي صف ينتهي قبل العمود الأخير دون دمج يغطي الباقي، فيترك فجوة.
import { tableGridIssues } from 'postext';
tableGridIssues({
rows: [
[{ content: 'A', colSpan: 2 }, { content: 'C' }],
[{ content: '1' }, { content: '2' }, { content: '3' }],
],
});
// => [{ kind: 'spanOverlap', row: 0, col: 1, coveredBy: { row: 0, col: 0 } },
// { kind: 'missingCells', row: 0, col: 2 }]الجدول الذي يستخدمه المستند وفي شبكته مثل هذه المشكلات يُبلَّغ عنه في doc.contentWarnings بوصفه raggedTableGrid (انظر التحذيرات في المستند).
#نمط التعليقات
تتحكم الخاصية captionStyle في تعليقات الموارد (السطر Figure 1 — … تحت الصور ومخططات SVG والجداول، أو فوقها). تشترك التسمية المرقّمة والوصف في الخط والحجم نفسيهما، وهذا قيد في المحرّك، لكن يمكن أن يكون للتسمية وزنها وميلها ولونها الخاص. وحين لا تُحدَّد عائلة الخط وحجمه ولونه، تُورَث من نص المتن. يمكن أن يقع التعليق فوق المورد (العرف المعتاد في الجداول) وأن يُنضَّد على شريط ملوّن يمتد بعرض الكتلة؛ وتُنسَّق الملاحظة الاختيارية الأصغر (سطر المصدر، الحقوق: Resource.note) عبر الكائن الفرعي note. ويستطيع نوع المورد أن يتجاوز أيًّا من هذه الحقول لموارده عبر ResourceType.captionStyle (انظر أنواع الموارد).
const config: PostextConfig = {
captionStyle: {
align: 'center',
labelBold: true,
labelColor: { hex: '#295AA3', model: 'hex' },
descriptionItalic: true,
position: 'above',
backgroundEnabled: true,
padding: { value: 0.35, unit: 'em' },
note: { italic: true, align: 'left' },
},
};| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
fontFamily | string | خط نص المتن | عائلة خط التعليق (التسمية والوصف). |
fontSize | Dimension | حجم نص المتن | حجم خط التعليق (التسمية والوصف). |
color | ColorValue | لون نص المتن | لون نص الوصف. |
align | 'left' | 'center' | 'right' | 'justify' | 'start' | 'end' | 'left' | المحاذاة الأفقية لأسطر التعليق. تمدّ 'justify' كل سطر إلى العرض الكامل ما عدا الأخير. وعلى شريط التعليق تُحاذى الأسطر داخل حشوته؛ والتعليق الجانبي يُحاذى داخل عرضه الخاص. |
gap | Dimension | 0.75em | المسافة العمودية بين المورد وتعليقه. |
labelBold | boolean | true | ارسم التسمية المرقّمة (مثل Figure 1) بخط غامق. |
labelItalic | boolean | false | ارسم التسمية المرقّمة بخط مائل. |
labelColor | ColorValue | color التعليق | لون التسمية المرقّمة. |
descriptionItalic | boolean | false | ارسم نص الوصف بخط مائل. |
position | 'above' | 'below' | 'below' | موضع التعليق. مع 'above' يأتي التعليق (وشريطه) أولًا، وينزل جسم المورد بمقدار ارتفاع التعليق مضافًا إليه gap؛ وعندئذ توضع الملاحظة تحت الجسم. |
backgroundEnabled | boolean | false | ارسم شريطًا خلف التعليق. يمتد الشريط بعرض الكتلة كاملًا ويحيط بأسطر التعليق مع padding من كل جانب. |
background | ColorValue | اللون الرئيسي في لوحة الألوان | لون تعبئة الشريط (لا يُرسم إلا عند التفعيل). |
padding | Dimension | 0.35em | الحشوة الداخلية بين حافة الشريط ونص التعليق. تُتجاهل حين يكون الشريط معطّلًا. |
note | object | — | تنسيق ملاحظة المورد؛ انظر الجدول الفرعي أدناه. |
labelNumberGap | string | مسافة غير منقسمة | ما يفصل التسمية عن الرقم، في التعليق وفي :ref المضمّنة: Figure 1.7، Fig. 1.7. وفي الصينية يتلاصقان: '' تعطي 图1-1. |
labelSeparator | string | '. ' | ما يلي الرقم، قبل الوصف: Figure 1.7. A caption. والتعليقات الصينية تأخذ مسافة إيديوغرافية، ' ' (图1-1 标题). والتسمية التي بلا رقم تحتفظ بقاعدتها الخاصة: نقطة، ما لم تنتهِ البادئة بنقطة. |
ينسّق الكائن الفرعي note الحقل Resource.note، وهو نص قصير (المصدر، الحقوق، ملاحظة) يُنضَّد تحت المورد بحجم أصغر. يقبل التنسيق المضمّن نفسه وعلامات :ref نفسها التي يقبلها التعليق، ويرث خط التعليق. يوضع تحت التعليق حين يكون التعليق في الأسفل، وتحت جسم المورد حين يكون التعليق في الأعلى؛ ويُحسب ارتفاعه ضمن الكتلة، فالمورد ذو الملاحظة يطفو وحدةً واحدة.
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
note.fontSize | Dimension | 0.85 × حجم التعليق | حجم خط الملاحظة. |
note.color | ColorValue | color التعليق | لون نص الملاحظة. |
note.italic | boolean | false | ارسم الملاحظة بخط مائل. |
note.gap | Dimension | 0.35em | المسافة بين الملاحظة وما يسبقها (التعليق أو الجسم). |
note.align | 'left' | 'center' | 'right' | 'justify' | 'start' | 'end' | 'left' | المحاذاة الأفقية لأسطر الملاحظة، كما في align للتعليق. |
تُدمج التجاوزات الخاصة بكل نوع عبر mergeCaptionStyle(resolvedCaptionStyle, override, palette?)، وهي مصدَّرة للتطبيقات المضيفة التي تحتاج إلى الحسم نفسه خارج خط المعالجة.
#نمط المخططات
تتحكم الخاصية diagramStyle في تلوين مخططات SVG المضمّنة (موارد kind: 'svg'). وميزتها الوحيدة حاليًا هي وضع الحبر الواحد (single ink): تمريرة إعادة تلوين تحوّل كل لون في المخطط إلى درجة من حبر واحد، فتُستنسخ الأشكال بأمانة حين يُطبع المستند بلون خاص (spot colour) واحد.
const config: PostextConfig = {
diagramStyle: {
singleInk: true,
inkColor: { hex: '#295AA3', model: 'hex' },
},
};| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
singleInk | boolean | false | أعد تلوين كل مخطط SVG مضمّن بدرجات من حبر واحد. |
inkColor | ColorValue | اللون الرئيسي (#295AA3) | الحبر. قيمته الافتراضية هي اللون الرئيسي في لوحة ألوان المستند (مرتبط بلوحة الألوان عبر paletteId: 'main-color')، فتغيير عيّنة اللون في اللوحة يعيد تلوين المخططات مع العناوين والمقاطع الغامقة. |
#كيف يعمل الحبر الواحد
حين يُفعَّل singleInk يُعاد كتابة كل لون في شيفرة SVG درجةً من inkColor شدتها 1 − الإضاءة النسبية (معاملات Rec. 709 مطبّقة على القنوات المرمّزة بغاما، وهو تقريب إدراكي يكفي تمامًا لتحويل الدرجات). يحافظ التحويل على القيمة المُدرَكة: الأبيض يصير بياض الورق، والأسود يصير الحبر كاملًا، والتعبئات الفاتحة تبقى فاتحة أيًّا كان لونها الأصلي. فالخلفية الصفراء الشاحبة تصير درجة شاحبة من الحبر، والخط الداكن يقترب من الحبر الكامل.
تتولى إعادة التلوين الدالة المصدَّرة applySingleInkToSvg(svgText, inkHex)، وتعمل على شيفرة SVG بوصفها نصًا دون DOM:
- تُعاد كتابة القيم الست عشرية
#rgb/#rgba/#rrggbb/#rrggbbaa، والدالتينrgb()/rgba()، والدالتينhsl()/hsla()أينما ظهرت: في سمات العرض، وفيstyleالمضمّنة، وفي التدرجات، وفي<defs>. ويمكن أن تستخدم الدوال قنوات صحيحة أو عشرية أو نسبًا مئوية، وصيغة الفواصل أو صيغة المسافات، فيُعاد تلوينrgb(11.37%, 20%, 50.59%)(كما يكتبها Cairo) وrgb(51 102 153 / 50%)أيضًا. وتُكتب الدالة بعد تلوينهاrgb(…)أوrgba(…). - لا تُستبدل الكلمتان
whiteوblackإلا حيث تظهران قيمتَي طلاء (fill،stroke،stop-color،flood-color،color، سمات كانت أو خصائص في نمط مضمّن)، ولا تُستبدلان أبدًا داخل محتوى النص أو التسميات. - تُترك
noneوtransparentوcurrentColorدون مساس، وكذلك الألوان المسمّاة الأخرى (red،steelblue…) والأسود الافتراضي للشكل أو النص الذي لا يحدد تعبئة. أعطِ هذه العناصر لونًا صريحًا لكي يُعاد تلوينها. - تُحفظ قنوات الشفافية (أنصاف بايتات
#rgba/#rrggbbaaومكوّنات الشفافية فيrgba(…)تمر دون تغيير؛ والشفافية المكتوبة بنسبة مئوية تُكتب رقمًا). - حين يتعذر تحليل
inkHexتُعاد المدخلات دون تغيير. - تحمل النتيجة
data-postext-single-ink="#…"(الحبر) على عنصرها الجذر<svg>، والشيفرة التي تحملها أصلًا تُعاد كما هي، أيًّا كان الحبر الذي تسمّيه. فالتحويل ليس متساوي القوى (idempotent): التمريرة الثانية تفتّح كل لون، فيخرج الأسود بنحو ثلثي الحبر، لذلك تُلوَّن الصورة مرة واحدة، أيًّا كان من يصل إليها أولًا: شيفرتك أو المُخرِجات. (العلامة جديدة في postext 1.5؛ والشيفرة التي لوّنها الإصدار 1.4 لا تحملها.)
import { applySingleInkToSvg } from 'postext';
const recoloured = applySingleInkToSvg(svgText, '#295AA3');
applySingleInkToSvg(recoloured, '#295AA3') === recoloured; // true: never twiceيسري الحبر الواحد في المُخرِجات الثلاثة: يعيد مُخرِج PDF تلوين بايتات SVG التي يسلّمها إليها resourceBytes قبل رسمها متجهات، ويلوّن مُخرِجا Canvas وHTML صور SVG التي يرسمانها حين تطلب منهما ذلك (انظر الحبر الواحد على اللوحة (Canvas) وفي HTML)، فيطابق ملف PDF المصدَّر المعاينةَ على الشاشة.
دالة الحسم ودالة الحذف تماثلان مثيلاتهما في الأقسام الأخرى، إلى جانب النوعين DiagramStyleConfig / ResolvedDiagramStyleConfig:
import {
DEFAULT_DIAGRAM_STYLE_CONFIG,
resolveDiagramStyleConfig,
stripDiagramStyleDefaults,
applySingleInkToSvg,
} from 'postext';
import type { DiagramStyleConfig, ResolvedDiagramStyleConfig } from 'postext';
const resolved = resolveDiagramStyleConfig(config.diagramStyle);
// => { singleInk: false, inkColor: { hex: '#295AA3', model: 'hex', paletteId: 'main-color' } }
const minimal = stripDiagramStyleDefaults(config.diagramStyle);
// => undefined when everything matches the defaults#الحبر الواحد على اللوحة (Canvas) وفي HTML
يتلقى مُخرِجا Canvas وHTML الصور مفكوكة الترميز مسبقًا (registerResourceImage) أو بوصفها عناوين URL (resourceImageUrl)، لا شيفرة SVG. وعند الطلب يطبّقان التحويل نفسه على ما يرسمانه:
- Canvas (
renderPage،renderPageToCanvas،renderToCanvas). كل صورة SVG يسري عليها التلوين، سواء كانت شكلًا أو صورة خلية في جدول أو صورة تصميم أو أيقونة إطار أو علامته، تُحوَّل إلى نقطيّة بحجمها الموضوع وتُلوَّن بكسلاتها بالحبر، سواء سُجّلت بوصفها<img>أوImageBitmap. وتُخزَّن الصورة النقطية الملوّنة مؤقتًا كأي صورة نقطية لمتجهات. أما الصورة النقطية الأصلية فلا تُلوَّن أبدًا. - HTML (
renderToHtml،renderToHtmlIndexed). تأخذ كل<img>من نوع SVG الخاصيةfilter: url(#pt-ink-…)، التي تشير إلىfeColorMatrixتحمله صفحتها: عنصر<svg>بحجم صفري يضم<filter>، يوضع أولًا في الصفحة، وهو جزء منdecorationHtmlالخاص بالصفحة في المُخرَج المفهرس. وتحمله كل صفحة ما دام الحبر الواحد ساريًا، سواء ضمّت صورة أم لا، فالتطبيق المضيف الذي يرقّع الكتل واحدة واحدة لا يُدخل أبدًا صورة ينقصها مرشّحها.
لا تلوين مرتين. حتى postext 1.4 كان مُخرِجا Canvas وHTML يرسمان الصور كما تُعطى، فكانت التطبيقات المضيفة تعيد تلوين الشيفرة بنفسها بـ applySingleInkToSvg قبل تسليمها. وما زالت محوّلات الحزم وSandbox تفعل ذلك، لأن تمريرة الشيفرة تعطي ألوان PDF بالضبط (انظر الفقرة الأخيرة أدناه). لذلك تُلوَّن الصورة مرة واحدة، وفق ثلاث قواعد تسري في المُخرِجات الثلاثة:
- الشيفرة المعلَّمة تُترك كما هي. يعيد مُخرِج PDF تلوين
resourceBytesبـapplySingleInkToSvg، فبايتات SVG التي لُوّنت من قبل تُرسم كما هي. وفي Canvas وفي HTML، الصورة المحمّلة من عنوان SVG بصيغة data URI تحمل شيفرته العلامة لا تُلوَّن أبدًا كذلك. - معطّل ما لم يُطلب، في postext 1.x. لا يلوّن مُخرِج Canvas صورة SVG سُجّلت دون علامة خاصة بها إلا حين يمرّر الرسم
singleInk: true(RenderPageOptions)، ويلوّن الصورة المسجّلة بـregisterResourceImage(id, img, { singleInk: true })في أي رسم. ويلوّن مُخرِج HTML حين تتلقىrenderToHtmlالقيمةsingleInk: true، أو حين تحمل دالة الحسمresourceImageUrlالخاصة بهاsingleInk: true. والتطبيق المضيف المكتوب للإصدار 1.4، الذي يعيد تلوين الشيفرة ويسجّل الصورة المفكوكة دون علامة، يحتفظ بمُخرَجه. وسيكون التلوين افتراضيًا في الإصدار الرئيسي التالي. singleInk: falseلا يُلوَّن أبدًا. خلف عنوان blob أو عنوان شبكة لا يمكن قراءة الشيفرة من جديد، فالصورة التي أعدت تلوينها بنفسك وفككت ترميزها بهذه الطريقة تُسجَّل بـsingleInk: false، كما يفعلregisterBundleImagesوSandbox. وتعيدbundleImageUrl(bundle)دالة حسم تحملsingleInk: false، وتسلّمbundleResourceBytesملف PDF بايتات الحزمة نفسها، فيعيد مُخرِج PDF تلوينها مرة واحدة.
يعرف المُخرِجان نوع كل صورة من الـ VDT: يحمل الشكل وصورة الخلية نوع موردهما، وتحمل كتلة صورة التصميم imageKind ('svg' أو 'bitmap')، المأخوذ من موردها عند الإخراج. أما الـ VDT المبني قبل وجود imageKind، فيعامل مُخرِج Canvas فيه صورة التصميم المسجّلة بوصفها مصدرًا متجهيًا على أنها SVG، ويعامل مُخرِج HTML بهذا الشكل الصورة التي عنوانها data URI من نوع SVG أو ينتهي بـ .svg.
إما أن تعيد تلوين الشيفرة بنفسك أو تترك المُخرِجات تلوّن الصورة الخام، لا الاثنين معًا:
import { applySingleInkToSvg, registerResourceImage, renderPage, renderToHtml } from 'postext';
// Raw SVG: tinted while diagramStyle.singleInk is on…
registerResourceImage('diagram.svg', rawImg, { singleInk: true });
// …or register it plainly and ask on each render.
registerResourceImage('diagram.svg', rawImg);
const canvas = renderPage(doc.pages[0], doc, { singleInk: true });
// Recoloured before decoding (as postext 1.4 hosts do): painted as given.
const inked = applySingleInkToSvg(svgText, ink);
registerResourceImage('diagram.svg', await decode(inked), { singleInk: false });
// The HTML backend with URLs to the raw markup.
const html = renderToHtml(doc, { resourceImageUrl: urlFor, singleInk: true });تأخذ renderToHtml قيمة singleInk الافتراضية من العلامة الخاصة بدالة الحسم، فلا تحتاج bundleImageUrl(bundle) إلى أي خيار.
لكل لون تعيد تمريرة الشيفرة كتابته (القيم الست عشرية، وقيم rgb() وhsl()، وwhite وblack؛ انظر كيف يعمل الحبر الواحد)، يعطي تحويل البكسلات النتيجة نفسها، بما في ذلك الحواف المنعَّمة والتدرجات. ويختلفان حيث تترك تمريرة الشيفرة اللون كما هو: الألوان المسمّاة غير white وblack، وcurrentColor، والأشكال والنصوص التي بلا تعبئة (المرسومة بالأسود الافتراضي)، والصور النقطية المضمّنة في SVG، فهذه تُلوَّن على الشاشة وتحتفظ بلونها في PDF. أعطِ كل عنصر في المخطط لونًا صريحًا بصيغة ست عشرية أو rgb() أو hsl() لتحصل على مُخرَج متطابق. وحين لا تستطيع اللوحة قراءة البكسلات من جديد (<img> من أصل آخر، محمّلة دون CORS)، تُرسم الصورة دون تلوين.
#أنماط الفقرات
تعلن الخاصية paragraphStyles أنماطًا مسمّاة يطبّقها المستند على مجموعة من الفقرات بحاوية :::paragraphs{style="…"}: المراجع، ومسارد المصطلحات، والملاحظات، وأي كتلة من المدخلات تحتاج إلى خطها ووزنها وميلها وحجمها وتباعد أسطرها وحروفها الكبيرة أو حروفها الكبيرة المصغّرة (small caps) أو إزاحتها المعلّقة (hanging indent) الخاصة. وكل حقل طباعي اختياري ويرث نص المتن حين لا يُحدَّد، فلا يذكر النمط إلا ما يختلف عن النص الجاري.
const config: PostextConfig = {
paragraphStyles: [
{
id: 'bibliography',
name: 'Bibliography',
fontSize: { value: 7, unit: 'pt' },
lineHeight: { value: 1.2, unit: 'em' },
hangingIndent: { value: 2, unit: 'em' },
spaceBetween: { value: 0.25, unit: 'em' },
marginTop: { value: 1, unit: 'em' },
marginBottom: { value: 1, unit: 'em' },
},
],
};## References
:::paragraphs{style="bibliography"}
Knuth, D. E. (1984). *The TeXbook*. Addison-Wesley.
Bringhurst, R. (2004). *The Elements of Typographic Style*. Hartley & Marks.
:::| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
id | string | إلزامي | المعرّف الذي تشير إليه :::paragraphs{style="…"}. |
name | string | id | اسم مقروء للبشر، لواجهات المحرّرات فقط. |
fontFamily | string | خط نص المتن | عائلة الخط. أوزانها هي fontWeight / boldFontWeight أدناه (أوزان نص المتن حين لا تُحدَّد). |
fontSize | Dimension | حجم نص المتن | حجم الخط. |
lineHeight | Dimension | ارتفاع سطر المتن | تباعد الأسطر. الوحدتان em/rem نسبيتان إلى حجم خط النمط نفسه، فالقيمة الموروثة 1.5em تضيق مع الحجم الأصغر. |
color | ColorValue | لون نص المتن | لون النص. تحتفظ المقاطع الغامقة والمائلة بألوان التوكيد في المتن ما لم تحدد boldColor / italicColor ألوان النمط الخاصة. |
textAlign | 'left' | 'justify' | 'center' | 'right' | 'start' | 'end' | محاذاة المتن | المحاذاة الأفقية. تجعل 'center' و'right' كل سطر بحافة حرّة من الجهة الأخرى: إهداء، كتلة توقيع. وفي فقرة من اليمين إلى اليسار تكون 'left' جهة بدايتها، أي اليمين. |
boldColor | ColorValue | bodyText.boldColor | لون المقاطع الغامقة (قائمة مؤلفين أسماؤهم بلون الدار). |
italicColor | ColorValue | bodyText.italicColor | لون المقاطع المائلة (…)، وفي النمط italic لون المقاطع التي تصير قائمة. لا يتبع color: النمط الملوّن الذي ينبغي أن يبقى مائله بلونه يحدد الاثنين. |
fontWeight | number | bodyText.fontWeight | وزن النص العادي (100–900): سؤال نصف غامق في ورقة عمل، أو تصدير بوزن خفيف. |
boldFontWeight | number | bodyText.boldFontWeight | وزن المقاطع الغامقة (…). |
italic | boolean | false | نضّد الفقرات بخط مائل: إرشادات المسرح، أو تصدير. والمقطع المائل … داخلها يصير قائمًا، كما في الاقتباس الكتلي. |
smallCaps | boolean | false | نضّد الفقرات بالحروف الكبيرة المصغّرة: الحروف الصغيرة حروفًا كبيرة بنسبة 70% من الحجم، والحروف الكبيرة بحجمها الكامل، وتُرسم على النحو نفسه في كل مُخرِج (انظر الحروف الكبيرة المصغّرة): قائمة الشخصيات، أو مداخل مسرد المصطلحات. |
hyphenation | boolean | تقسيم كلمات المتن | قسّم الكلمات بالواصلة عند ضبط الأسطر (يستخدم لغة المستند). |
indent | Dimension | 0 | إزاحة كل سطر عن الحافة اليسرى للعمود (أو للإطار الذي تقع فيه الفقرات)؛ ووحدة em هي حجم النمط نفسه. تُقاس إزاحة السطر الأول والإزاحة المعلّقة منها، فيستطيع سطر شعري مُزاح أن يعلّق سطره المرحَّل (turnover) أعمق من بدايته: indent: 1.5em مع hangingIndent: 2.5em يضع السطر عند 1.5 em وسطره المرحَّل عند 4 em. والقيمة السالبة تُعدّ 0. |
firstLineIndent | Dimension | إزاحة السطر الأول في المتن | إزاحة السطر الأول، مقيسة من indent. تُتجاهل حين تكون hangingIndent غير صفرية. |
hangingIndent | Dimension | 0 | إزاحة تُطبَّق على كل الأسطر ما عدا الأول، مقيسة من indent: الشكل التقليدي للمراجع ومسارد المصطلحات. |
spaceBetween | Dimension | 0 | المسافة العمودية بين الفقرات المتتالية داخل الحاوية. القيمة 0 تجعل المدخلات متلاصقة. |
marginTop | Dimension | 0 | المسافة فوق الفقرة الأولى في الحاوية. تندمج مع المسافة المعلّقة أصلًا وتختفي في رأس العمود، كأي هامش آخر. |
marginBottom | Dimension | 0 | أقل مسافة تحت الفقرة الأخيرة في الحاوية. وطريقة التقائها بمسافة الكتلة التي تلي الحاوية يحددها bodyText.paragraphContainerSpacing. |
snapToGrid | boolean | true | أعِد التدفق إلى شبكة خطوط الأساس تحت الحاوية، على أن تكون المسافة تحتها حدًّا أدنى. القيمة false تُبقي المسافة الدقيقة: يبقى النص بعد الحاوية خارج الشبكة حتى الكتلة التالية التي تُحاذى عليها (عنوان، أو نهاية قائمة، أو معادلة رياضية مستقلة)، وهذا لمستند يجري خارج الشبكة أو لمجموعة لها تباعد أسطر خاص بها. وداخل الإطار (callout)، الذي لا شبكة له، لا تغيّر شيئًا. |
textTransform | 'none' | 'uppercase' | 'none' | حالة أحرف الفقرات: 'uppercase' تنضّدها بالحروف الكبيرة (قائمة الشخصيات، سطر من الحركة المسرحية)، بما في ذلك كلمات الشارة وتسمية :ref. الطول يبقى على حاله، فتبقى خريطة المصدر في المحرّر مطابقة حرفًا بحرف: الحرف الذي صيغته الكبيرة أطول (ß) يُترك كما كُتب. تُترك الرياضيات كما هي، والترويسة التي تقرأ الفقرة علامةً ({firstMark.style}) تأخذ النص كما كُتب؛ أما نص التصميم فتنضّده بالحروف الكبيرة خاصيته textTransform. |
تنضّد المسرحية إرشادات المسرح بخط مائل وقائمة شخصياتها بالحروف الكبيرة المصغّرة:
paragraphStyles: [
{ id: 'direction', italic: true, fontSize: { value: 9, unit: 'pt' } },
{ id: 'cast', smallCaps: true, textAlign: 'center', fontWeight: 600 },
],:::paragraphs{style="direction"}
Elsinore. A platform before the castle. *Francisco* at his post.
:::تُطبع الإرشادات بخط مائل والاسم الذي داخلها قائمًا؛ وتسري الأوزان وitalic وsmallCaps الخاصة بالنمط داخل الإطارات أيضًا.
يُزيح ديوان الشعر بعض الأسطر، ويعلّق السطر المرحَّل من بيت أطول من عرض السطر أعمق من البيت نفسه. تُدخل indent كل أسطر الفقرة إلى الداخل، وتُحسب الإزاحة المعلّقة من هناك:
paragraphStyles: [
{ id: 'verse', textAlign: 'left', firstLineIndent: { value: 0, unit: 'em' }, hangingIndent: { value: 4, unit: 'em' } },
{ id: 'verse-indented', textAlign: 'left', indent: { value: 1.5, unit: 'em' }, hangingIndent: { value: 2.5, unit: 'em' } },
],يبدأ السطر في verse-indented عند 1.5 em وسطره المرحَّل عند 4 em، على مستوى الأسطر المرحَّلة في verse. ومن دون indent يستطيع النمط أن يُزيح السطر الأول أو أن يعلّق الأسطر الأخرى، لا الاثنين معًا: تُتجاهل firstLineIndent متى حُدّدت hangingIndent.
#الحاوية :::paragraphs
يفتح السطر :::paragraphs{style="<id>"} الحاوية ويغلقها سطر ::: وحده؛ وكل فقرة بينهما تأخذ النمط المسمّى، بينما تحتفظ العناوين والقوائم والكتل الأخرى داخلها بتنسيقها المعتاد. ويمكن أن تتداخل الحاويات داخل حاويات مسوّرة أخرى. ومعرّف style المجهول ليس خطأً: تُرسم الفقرات نصَّ متنٍ عاديًا.
داخل الحاوية يغادر التدفق شبكة خطوط الأساس، فالمدخل بحجم 7pt وتباعد أسطر 1.2em لا يمكن أن يستقر على شبكة 8pt/1.5em، ثم تعيد الفقرة الأخيرة التدفق إليها (الشبكة هي الغالبة؛ والمسافة تحتها حدٌّ أدنى، وهو العرف الذي تتبعه العناوين). وتنقسم المدخلات عبر الأعمدة والصفحات كفقرات المتن، مع الحماية نفسها من الأسطر اليتيمة (orphans) والأرامل (widows)؛ والعنوان الذي يسبق الحاوية مباشرة يبقى مع فقرتها الأولى.
المسافة تحت الحاوية هي الأكبر بين spaceBetween وmarginBottom الخاصتين بالنمط وتباعد الفقرات في النص المحيط (سطر واحد حين يكون bodyText.paragraphSpacing مفعّلًا)، وتندمج مع المسافة التي تحتفظ بها الكتلة التالية فوقها، كما تفعل المسافة بين فقرتين من المتن: فالعنوان بعد قائمة المراجع يقع تحت المدخل الأخير بمقدار marginTop الخاص به (أو بمسافة النمط حين تكون أكبر)، والفقرة بعد مجموعة من المدخلات الأضيق تحتفظ بتباعد فقرات النص. يعود التدفق إلى الشبكة تحت النص أولًا، وما لم تغطّه المحاذاة يُنقل في أسطر شبكة كاملة، فيقع النص بعد الحاوية على الشبكة. وحتى postext 1.4 كانت مسافة النمط توضع تحت السطر الأخير قبل المحاذاة، وتُضاف تحتها مسافة الكتلة التالية العليا، ويُهمَل تباعد الفقرات؛ والقيمة bodyText.paragraphContainerSpacing: 'add' تحتفظ بتلك القاعدة، والإعدادات المحفوظة قبلها تُقرأ بها. والنمط ذو snapToGrid: false لا يعود إلى الشبكة: يقع النص بعد الحاوية تحتها بالمسافة الدقيقة، خارج الشبكة حتى الكتلة التالية التي تُحاذى عليها. والحاوية التي تنتهي بقائمة تُنضَّد كما في 1.4 في كلتا القاعدتين: تحتفظ القائمة بمسافتها تحتها، وتتبع marginBottom تلك المسافة، مندمجةً مع مسافة الكتلة التالية.
داخل :::callout تأخذ الحاوية هوامش نمطها على النحو نفسه: تندمج marginTop وmarginBottom مع تباعد الكتل المحيطة بها (والقيمة السالبة تقرّبها)، والحاوية التي يبدأ بها الإطار لا تأخذ هامشًا علويًا، كما في رأس العمود. ولا شبكة خطوط أساس في الإطار يُعاد إليها، فالمسافة تحت الفقرة الأخيرة هي الأكبر بين marginBottom وspaceBetween وتباعد الفقرات الخاص بالإطار (body.paragraphSpacing الخاص به، أي سطر من نصه؛ ويُهمَل مع paragraphContainerSpacing: 'add')، أو الهامش العلوي للكتلة التالية حين يكون أكبر منها كلها؛ والقيمة السالبة لـ marginBottom ترفع الكتلة التالية بدلًا من ذلك. (حتى postext 1.4 كانت الحاوية داخل الإطار تتجاهل الهامشين.)
const resolved = resolveParagraphStylesConfig(config.paragraphStyles, resolvedBodyText);
// => every unset field filled from the resolved body text
const minimal = stripParagraphStylesDefaults(config.paragraphStyles);
// => undefined when the list is empty; zero margins and `name === id` dropped#أنماط الشارات
تعلن الخاصية chipStyles الأنماط المسمّاة للشارة (chip) المضمّنة :chip[text]{style="…"}، أي المربعات الملوّنة المستديرة الزوايا لبنك كلمات أو مفتاح لوحة مفاتيح أو وسم (الصيغة وقواعد كسر الأسطر الخاصة بها في مرجع صيغة المستند). يأتي نمط واحد، chip، افتراضيًا (تعبئة زرقاء شاحبة مع خط شعري باللون الرئيسي، زوايا مستديرة قليلًا، والنص كالكلمات المحيطة به)، فتعمل :chip[…] دون أي إعداد؛ وإعلان chipStyles يستبدل تلك القائمة الافتراضية. والشارة التي بلا style، أو بمعرّف لا يعلنه أي نمط، تأخذ النمط الأول.
const config: PostextConfig = {
chipStyles: [
{ id: 'chip', name: 'Word bank' },
{
id: 'key',
name: 'Keyboard key',
background: { hex: '#fff4d6', model: 'hex' },
borderColor: { hex: '#8a6d1f', model: 'hex' },
borderRadius: { value: 2, unit: 'pt' },
bold: true,
},
],
};Classify: :chip[battery] :chip[cable] :chip[switch]
Press :chip[Ctrl]{style="key"} + :chip[C]{style="key"}.| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
id | string | إلزامي | المعرّف الذي تشير إليه :chip[…]{style="…"}. |
name | string | id | اسم مقروء للبشر، لواجهات المحرّرات فقط. |
backgroundEnabled | boolean | true | ارسم تعبئة المربع. |
background | ColorValue | #e8eef7 | تعبئة المربع (قابلة للربط بلوحة الألوان). |
borderColor | ColorValue | اللون الرئيسي في لوحة الألوان | لون الخط المحيط. |
borderWidth | Dimension | 0.5pt | سُمك الخط المحيط؛ والقيمة 0 لا ترسم شيئًا. يُرسم الخط المحيط داخل حافة المربع. |
borderRadius | Dimension | 0.3em | نصف قطر الزوايا، لا يتجاوز نصف ارتفاع المربع (القيمة الكبيرة تعطي شكل كبسولة). |
paddingX | Dimension | 0.3em | المساحة بين الخط المحيط والنص، يسارًا ويمينًا. وهي جزء من مقدار تقدّم الشارة. |
paddingY | Dimension | 0.1em | المساحة فوق شريط النص وتحته. تُرسم خارج مربع السطر: لا تغيّر ارتفاع السطر أبدًا. |
paddingTop, paddingBottom | Dimension | paddingY | المساحة فوق شريط النص أو تحته، كلٌّ منهما بدلًا من paddingY. يمتد الشريط 0.8 em فوق خط الأساس و0.25 em تحته، فيقع منتصفه على ارتفاع 0.275 em فوق خط الأساس، أدنى من منتصف الحرف الكبير (نحو 0.35 em في أغلب الخطوط): فيبدو الحرف الكبير أو الرقم في شارة مستديرة (borderRadius: 1em) مرتفعًا. والحشوة العليا التي تزيد على السفلى بضعف الفرق تتوسّطه: paddingTop: 0.2em مع paddingBottom: 0.05em لخط ارتفاع حروفه الكبيرة 0.7 em. |
fontFamily | string | النص المحيط | عائلة خط نص الشارة. تتبع الأوزان النص المحيط بها. |
fontSize | Dimension | النص المحيط | حجم نص الشارة؛ ووحدة em نسبية إلى النص المحيط. |
color | ColorValue | النص المحيط | لون نص الشارة. حين لا يُحدَّد، تحتفظ المقاطع الغامقة والمائلة بألوان التوكيد. |
bold | boolean | false | نضّد نص الشارة بخط غامق، فوق تنسيقه الخاص. |
italic | boolean | false | نضّد نص الشارة بخط مائل، فوق تنسيقه الخاص. |
gap | Dimension | 0.25em | أقل مساحة تُترك بين المربع وكلمة أو شارة مجاورة عبر مسافة بين الكلمات؛ والمسافة الأضيق تُكمَّل داخل مقدار تقدّم الشارة، فلا يقتطع منها ضبط الأسطر أبدًا. ولا يُضاف شيء عند حافة السطر أو بجوار علامة ترقيم ملتصقة. |
أطوال المربع بوحدة em (paddingX، paddingY، borderRadius، borderWidth، gap) نسبية إلى حجم خط الشارة نفسها. المربع شريط يمتد 0.8 em فوق خط الأساس و0.25 em تحته، يكبر بمقدار paddingY والخط المحيط؛ ويُرسم خارج مربع السطر ولا يغيّر تباعد الأسطر أبدًا، فتبقى شبكة خطوط الأساس سليمة. وحين يصير المربع أطول من خطوة السطر، يمكن أن تصطدم الشارة بشارة في السطر الذي فوقها أو تحتها، فيُدرج Sandbox تحذير «الشارات تلامس السطر التالي» حين تتداخل شارتان في سطرين مختلفين، مع مقدار التداخل بالنقاط، لكي تُقلَّص paddingY أو الخط المحيط أو fontSize. ولا يُبلَّغ عن شارة طويلة ليس فوقها ولا تحتها شارة.
في الـ VDT الشارة مقطع سطر من kind: 'chip' يحمل حقله chip مقاطع النص (لكل منها سلسلة الخط وعرضها)، وهندسة المربع (boxWidth، ascent، descent، paddingX، borderWidth، borderRadius، وهوامش المسافة) وألوانه؛ وtext الخاص بالمقطع عنصر نائب من حرف واحد، فتعدّ إزاحات النص العادي وخرائط المصدر الشارةَ حرفًا واحدًا.
const resolved = resolveChipStylesConfig(config.chipStyles);
// => the built-in `chip` style when unset; every field filled
const minimal = stripChipStylesDefaults(config.chipStyles);
// => undefined for the built-in default; static defaults dropped
const style = pickChipStyle(resolved, 'key');
// => the `key` style, else the first one#أنماط الإطارات
تُعرِّف الخاصية calloutStyles أنماط الإطارات (callouts) المسمّاة التي يطبّقها المستند بالحاوية :::callout{type="…"}: الملاحظات والنصائح والتحذيرات وأهداف التعلّم، وكل محتوى يُفصل عن النص الجاري في إطار ملوّن الخلفية أو محاط بحدّ. يأتي افتراضيًا نمط محايد واحد هو note (خلفية رمادية فاتحة، بلا حدّ ولا شريط جانبي (stripe) ولا أيقونة ولا عنوان)، فتعمل :::callout دون أي إعداد؛ وتعريف calloutStyles يستبدل هذه القائمة الافتراضية.
const config: PostextConfig = {
calloutStyles: [
{ id: 'note', name: 'Note' },
{
id: 'objectives',
name: 'Learning objectives',
title: 'Objectives',
stripe: { enabled: true, side: 'left' },
icon: { kind: 'glyph', glyph: '✓' },
titleStyle: { textTransform: 'uppercase' },
lists: { bulletChar: '–' },
},
{
id: 'warning',
title: 'Warning',
backgroundEnabled: false,
border: { enabled: true, color: { hex: '#AA0000', model: 'hex' }, width: { value: 1, unit: 'pt' } },
borderRadius: { value: 1, unit: 'mm' },
titleStyle: { color: { hex: '#AA0000', model: 'hex' } },
},
],
};:::callout{type="objectives"}
- Describe the parts of the lantern.
- Trim the wick at dusk.
:::
:::callout{type="warning" title="Do not touch the lens"}
The glass stays hot for an hour after the flame is out.
:::| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
id | string | — | المعرّف الذي يختاره :::callout{type="…"}. والسياج (fence) الذي يحمل type غير معروف أو لا يحمله يستخدم أول نمط مُعرَّف (ويشير Sandbox إلى الأنواع غير المعروفة). |
name | string | id | اسم مقروء للبشر (لواجهة المحرّر فقط). |
title | string | '' | نص العنوان الافتراضي؛ والقيمة الفارغة تعني عدم وجود عنوان. وتتجاوزه السمة title في السياج لكل حالة على حدة. |
span | 'column' | 'page' | 'side' | 'column' | الامتداد الأفقي: العمود، أو كامل عرض المحتوى، أو العمود الجانبي المخصّص للعناصر العائمة في تخطيط العمود ونصف العمود (layout.sideColumnRole: 'floats')، وعندئذ يغادر الإطار التدفق ويتراصّ في ذلك العمود بجانب النص الذي يقطعه. يمكن تجاوزه في كل حالة بالسمة span. وفي التخطيطات متعددة الأعمدة يصبح الإطار 'page' كتلة ممتدة (span block): يقسم الصفحة إلى نطاقات أعمدة (column bands) ويحتل عمودًا خاصًا به بكامل العرض (انظر قسم الحاوية أدناه). |
placement | 'here' | 'auto' | 'top' | 'bottom' | 'fixed' | 'here' | موضع الإطار. 'here' يضعه داخل التدفق؛ و'top' / 'bottom' يعوّمانه كما يُعوَّم المورد (و'auto' يأخذ أول نطاق يخلو، في رأس الصفحة أو في قدمها): يغادر التدفق حيث يرد ويأخذ أول نطاق حرّ عند تلك النقطة أو بعدها (قدم الصفحة الحالية، أو رأس الصفحة التالية التي يفتحها التدفق أو قدمها)، ويملأ النص الذي يليه الصفحة من حوله؛ و'fixed' يثبّته عند إحداثيات في الصفحة عبر fixed أدناه، خارج تدفق الأعمدة. انظر قسم الحاوية للتفاصيل. يمكن تجاوزه في كل حالة بالسمة placement. |
sideAtColumnEnd | 'before' | 'after' | 'before' | موضع الإطار الجانبي (span: 'side') حين لا يستمر النص الذي يلي سياجه في العمود نفسه: إما لأن العمود لم يبقَ فيه متسع له، وإما لأن قواعد الفصل تنقله (فقرة تنقلها قواعد الأسطر الأرامل (widows) والأسطر اليتيمة (orphans) كاملة، أو عنوان يُبقى مع نصه). 'before' يبقيه عند سياجه، في تلك الصفحة، بجانب النص الذي قبله، ويرتفع من قدم العمود إن لم يتسع له ما تحت السياج: وهو موضع الشرح المكتوب بعد المقطع الذي يشرحه. و'after' يضعه بمحاذاة السطر الأول من النص الذي يلي السياج، في العمود الجانبي للصفحة التي يستمر فيها ذلك النص: وهو موضع رقم السطر أو العنوان الهامشي المكتوب قبل سطره. وحين يستمر النص في العمود نفسه، يضع كلاهما الإطار عند سياجه. والإطار الذي لا يليه شيء في فصله يبقى مع النص الذي قبله في الحالتين، والإطارات الجانبية المتتالية تحافظ على ترتيبها. حتى postext 1.4 كان كل إطار جانبي يتصرف كما في 'before'، وهي القيمة الافتراضية الباقية: فنمط الشروح يُبقي إطاراته في صفحة المقطع الذي تشرحه. |
fixed | | | موضع الإطار 'fixed': مرساة ElementAnchor (to: 'container' = منطقة محتوى الصفحة، معكوسة في الصفحات الزوجية؛ 'page' = صندوق حدّ القص؛ 'bleed' = صندوق النزف؛ edge: إحدى حواف الحاوية التسع) مع إزاحة اختيارية offset (بُعدا x / y). |
floatBarrier | boolean | false | يجعل الإطار حاجزًا للعناصر العائمة: كل شكل أو جدول أُشير إليه قبله يوضع قبله (في الخانات الحرّة من الصفحة، وإلا ففي صفحات تُفتح قبل الإطار)، فلا يفلت أي عنصر عائم إلى ما بعد الإطار الذي يختم الفصل (ملخّص «النقاط الرئيسية» عادةً). وصفحات افتتاح الفصول و:::part ونهاية المستند حواجز دائمًا. |
الإطار span: 'page' في صفحة متعددة الأعمدة يقطع النطاق تحت النص الذي يليه؛ والشكل الممتد بكامل العرض المشار إليه قبله يأخذ ذلك القطع أولًا: يُسوّى النص، ويقع الشكل حيث انتهى تمامًا، ويستمر الإطار تحته (أو ينتقل إلى الصفحة التالية إن لم يعد يتسع). والشكل الأطول من أن يلي النص المسوّى يفتح الصفحة التالية بدلًا من ذلك، والإطار بعده، ويبقى النطاق الذي تركه منتهيًا بأعمدة مستوية. والإطار القابل للتقسيم (keepTogether: false) يبدأ تحت النص والشكل بما يتسع من عناصره، ويستمر الباقي في الصفحة التالية. | |||
width | 'fill' | 'auto' | 'fill' | 'fill' يمتد على العرض المتاح؛ و'auto' يضيق على قدر العنوان (للاستعمال كشارة) ويتجاهل العناصر الأبناء. |
backgroundEnabled / background | boolean / ColorValue | true / #f4f4f4 | تعبئة الإطار. |
border | { enabled, color, width } | false، #cccccc، 0.5pt | حدّ الإطار، يُرسم داخل حافته كما يُرسم حدّ عنصر الصندوق (انظر عناصر الصندوق). |
borderRadius | Dimension | 0 | نصف قطر زوايا الخلفية / الحدّ (لا يتجاوز نصف عرض الإطار ونصف ارتفاعه). ويتبعه الشريط الجانبي: في الإطار مستدير الزوايا يُقصّ الشريط على حدود الإطار المستديرة، كما تقصّ CSS الخاصية border-left على border-radius. ويحتفظ لسان label بزوايا قائمة. |
padding | { top, right, bottom, left } | 0.75em لكلٍّ منها | المسافة الداخلية بين حافة الإطار ومحتواه. وقيم em نسبية إلى حجم نص الإطار. |
stripe | { enabled, side, width, color } | false، 'left'، 1.5em، اللون الرئيسي | شريط مصمت على طول إحدى الحواف. الشريط 'left' / 'right' يضيّق المحتوى؛ والشريط 'top' يدفعه إلى الأسفل. 'left' و'right' جانبان من تدفق النص (في الكتاب المكتوب من اليمين إلى اليسار يكون 'left' يمين الورقة)؛ أما 'start' و'end' فيتبعان اتجاه الإطار نفسه، فالإطار :::callout{dir=ltr} في كتاب عربي يضع الشريط 'start' على يساره. وفي الإطار ذي borderRadius تُدوَّر زواياه الخارجية مع زوايا الإطار (حتى postext 1.4 كانت تبقى قائمة وتبرز خارج التدوير). |
icon | { kind, glyph, resourceId, fontFamily, fontWeight, size, width, color, align, position, cornerSide } | 'none'، خط العناوين، 400، 1.5em، اللون الرئيسي، 'top'، 'inline'، 'right' | رمز نصي (kind: 'glyph') أو مورد نقطي / SVG (kind: 'resource' + resourceId) بجانب المحتوى. مع شريط جانبي تتوسّط الأيقونة الشريط؛ وإلا فإنها تحجز عمودًا خاصًا بها (size + titleStyle.gap). وalign: 'center' يوسّطها عموديًا على المحتوى. وتُوضع صورة المورد داخل المربع مع الحفاظ على نسبة أبعادها، أو داخل صندوق width × size عند تعيين width (شريط عريض من الأيقونات)؛ والأيقونة الأطول من المحتوى تُطيل الإطار ليتسع لها (ومع align: 'center' يُوسَّط المحتوى عليها). وposition: 'corner' يعلّق الأيقونة على زاوية علوية كشارة، نصفها خارج الحدّ، دون أن تأخذ من مساحة المحتوى؛ والأيقونة العريضة (width) تُوسَّط على الزاوية بحسب العرض الذي تُرسم به، وعلى الزاوية اليسرى يبدأ العنوان بعد نصفها الداخلي (حتى postext 1.4 كانت توضع بحسب ارتفاعها، فيتدلّى الشريط العريض خارج الإطار وفوق العنوان)؛ وcornerSide يختار الزاوية: 'right' / 'left'، أو 'outer' / 'inner' اللتان تتبعان زوجية الصفحة في الهوامش المعكوسة (الخارجية = اليمنى في الصفحة الفردية، واليسرى في الصفحة الزوجية). |
marker | { kind, glyph, resourceId, fontFamily, fontWeight, size, color, align, gap, rule } | 'none'، خط العناوين، 400، 1.5em، اللون الرئيسي، 'center'، 0.5em، الخط الفاصل معطّل (0.5pt، اللون الرئيسي، الطول 0) | أيقونة ثانية تُرسم خارج الإطار، في عمود على يساره، مع خط عمودي اختياري rule بينها وبين الإطار: اليد التي تقول «اضغط هنا» بجانب شارة التقييم الذاتي. يصبح الإطار [marker][rule][gap][box] بارتفاع أطول الثلاثة؛ وalign يوسّطها بعضها على بعض أو يحاذيها من الأعلى. وrule.length حدّ أدنى: يمتد الخط دائمًا على ارتفاع الإطار على الأقل. |
titleStyle | { fontFamily, fontSize, fontWeight, italic, color, textTransform, gap, letterSpacing, indent, lineHeight } | خط العناوين، حجم النص، 700، false، اللون الرئيسي، 'none'، 0.5em، 0، 0، 1.2em | التنسيق الطباعي للعنوان. gap هي المسافة بين العنوان وأول عنصر ابن (وفاصل عمود الأيقونة). وlineHeight هو تباعد أسطر العنوان، وتُحسب em فيه بحجم العنوان نفسه؛ ويقع خط الأساس عند 0.8 منه نزولًا في كل سطر، كما في النص الجاري، فالعنوان المضبوط على تباعد أسطر النص (lineHeight: 12pt على شبكة 12 pt) يُبقي الإطار عددًا صحيحًا من الأسطر وعنوانه على الشبكة، في حين تضيف القيمة الافتراضية 1.2 em كسرًا من سطر إلى كل إطار. وtextTransform: 'uppercase' لا يغيّر طول النص. وletterSpacing يضبط تتبّع العنوان (letterSpacing في canvas / Tc في PDF)؛ وindent يدفعه يمينًا بعيدًا عن الحافة الداخلية للإطار. والشارة المعلّقة على زاوية في جهة العنوان (الزاوية اليسرى) تحجز مكانها أولًا (نصفها الداخلي مضافًا إليه gap) فلا يصطدم بها العنوان في أي صفحة وقع؛ وindent لا يضيف إلا ما يتجاوز ذلك. |
body | { fontFamily, fontSize, lineHeight, color, boldColor, italicColor, fontWeight, boldFontWeight, italic, smallCaps, textAlign, hyphenation, paragraphSpacing, firstLineIndent } | يرث bodyText؛ italic / smallCaps false | التنسيق الطباعي للفقرات وعناصر القوائم داخل الإطار. كل حقل غير مُعيَّن يرث قيمة نص المتن؛ وitalicColor يحدّد لون المقاطع المائلة (اقتباس بارز مائل بلون الإطار). وfontWeight / boldFontWeight يحدّدان وزنَي المقاطع العادية والعريضة؛ وitalic يجعل نص الإطار مائلًا، فتستقيم مقاطع …؛ وsmallCaps يجعله بحروف كبيرة مصغّرة (small caps). انظر التنسيق الطباعي داخل الإطار لمعرفة ما يأخذه نص الإطار أيضًا. |
lists | { bulletChar, color, indent, gap, itemSpacing, bulletFontSize, bulletFontWeight } | يرث unorderedLists | التنسيق الطباعي للقوائم داخل الإطار (وتنطبق color وindent وgap وitemSpacing على القوائم المرقّمة أيضًا). وbulletFontSize / bulletFontWeight يرسمان رمز التعداد بخط نص الإطار بذلك الحجم والوزن (نقطة تعداد ملوّنة ثقيلة). |
label | { fontFamily, fontSize, fontWeight, color, background, position, height, paddingX, offset, inset, icon, rule } | غير مُعيَّن (بلا لسان) | لسان على الحافة العليا للإطار يطبع سمة label من السياج: رقم الإطار المرقّم («BOX 1-1»). يلتصق بالزاوية position ('top-right' / 'top-left')، بإزاحة داخلية قدرها inset، ويرتفع offset فوق أعلى الإطار (وهذه المساحة جزء من الكتلة، فوق marginTop، فيحتفظ بها اللسان في رأس العمود أيضًا)، ويبلغ ارتفاعه height مع paddingX على جانبي النص، ويمكن أن يحمل بجانبه مورد أيقونة icon ({ resourceId, width, gap }، في الجهة البعيدة عن الزاوية) وخطًا rule ({ enabled, color, width }) على طول الحافة العليا من الزاوية البعيدة حتى اللسان. القيم الافتراضية: خط العناوين، حجم النص، 700، أبيض على اللون الرئيسي، ارتفاع 1.4em، حشوة 0.6em. واللسان شكل قائم بذاته فوق الإطار، فيحتفظ بزواياه القائمة أيًّا كان borderRadius الإطار. |
columnGap | Dimension | 1.5em | الفاصل بين أعمدة مجموعة :::columns داخل الإطار (انظر قسم الحاوية أدناه). |
marginTop / marginBottom | Dimension | 0.75em / 0.75em | المسافة فوق الإطار (تندمج مع هامش الكتلة السابقة) والحدّ الأدنى للمسافة تحته (المسافة الدقيقة مع snapToGrid: false). والإطار العائم (placement: 'top' أو 'bottom' أو 'auto') يُبقي فاصل العناصر العائمة، وهو سطر واحد من النص، بين نطاقه والنص؛ وmarginBottom الأوسع من ذلك يحدّد المسافة تحت الإطار في نطاق علوي، وmarginTop الأوسع يحدّد المسافة فوق الإطار في نطاق سفلي، مقرّبةً إلى الأعلى على الشبكة مع النطاق. حتى postext 1.4 كان الإطار العائم يتجاهل هوامشه. |
snapToGrid | boolean | true | مع true يعود التدفق بعد الإطار إلى شبكة خطوط الأساس، فتكون المسافة تحته marginBottom مقرّبةً إلى الأعلى إلى أسطر شبكة كاملة. ومع false يحتفظ الإطار بقيمة marginBottom الدقيقة، التي تندمج مع الهامش العلوي للكتلة التالية (فالإطاران المتتاليان من هذا النمط تفصل بينهما max(marginBottom, marginTop) تمامًا)، وقد يقع النص الذي يليه خارج الشبكة حتى نقطة الالتقاط التالية (عنوان، أو نهاية قائمة)، كما بعد عنوان ذي headings.snapToGrid: false. وهو مخصّص للمستندات المؤلفة من إطارات متراصّة (أوراق العمل، الاستمارات). ينطبق على الإطارات داخل التدفق؛ أما الإطارات الممتدة على الصفحة في التخطيط متعدد الأعمدة، والإطارات العائمة والثابتة والجانبية، فتبقى على الشبكة، لأن نطاقات الأعمدة ومناطق العناصر العائمة تُخرَج عليها. ولا تتغيّر أدوات موازنة الأعمدة: الإطار الذي يختم عمودًا يُدفع مع ذلك إلى آخر خانة في شبكة العمود. |
keepTogether | boolean | true | مع true يُبقى الإطار كاملًا: الإطار الذي لا تتسع له المساحة المتبقية ينتقل كاملًا إلى العمود أو الصفحة التالية. ولا يتعذّر إبقاء الإطار كاملًا إلا إذا كان أطول من عمود فارغ كامل (أو من صفحة كاملة للإطار span: 'page'): عندئذ يُقسَم وفق قواعد false أدناه بدلًا من أن يفيض، بدءًا من موضع وروده، ثم تنتقل كاملةً التتمةُ التي يتسع لها عمود؛ والإطار العائم (placement: 'top' | 'bottom' | 'auto') بهذا الطول لا يطفو بل يبقى في التدفق حيث يرد. ومع false يمكن لأي إطار أن ينقسم بين كتله الأبناء أو بين أسطر فقرة أو عنصر قائمة: أعمق قطع يتسع يختم العمود الحالي (أو الصفحة، في الإطار span: 'page'، محاذيًا لأسفل الأعمدة) ويستمر الباقي في أعلى العمود التالي في إطار خاص به (بالحدّ والشريط نفسيهما، بلا أيقونة، وبلا عنوان ما لم يكرّره repeatTitle)، وينقسم مجددًا إن ظلّ أطول مما ينبغي. ويحتفظ نص كل جزء بالعمود الذي تأخذه الأيقونة المضمّنة في الجزء الأول، فارغًا، فيكون للإطار عرض سطر واحد في كل صفحة. والقطع داخل عنصر قائمة يترك نقطة تعداده مع الجزء الأول. ويتشارك إطار كل جزء contentIndex / containerId الخاصين بالسياج ويسجّل callout.part / callout.continued. استخدمه في إطار «النقاط الرئيسية» الطويل الذي يختم الفصل مع headings.balancing.beforeSpan، أو في نمط ملاحظات يجب ألا تدفع إطاراته شكلًا خارج الصفحة أبدًا. والإطار المتداخل (:::callout داخل إطار آخر) عنصر ابن واحد لأبيه: قد يقع القطع قبله أو بعده، ولا يقع داخله إلا إذا سمح نمطه بالتقسيم (keepTogether: false، أو كان أطول من عمود كامل)، وفق splitMinLines الخاص به. |
splitMinLines | number | 2 | أقل عدد من أسطر النص يحتفظ به كل جزء من الإطار المقسوم (keepTogether: false، أو إطار يُبقى كاملًا وهو أطول من عمود كامل) على جانبي القطع. وهو يحمي النص فقط: الجانب الذي يضم شكلًا أو جدولًا أو صيغة منفصلة أو إطارًا متداخلًا واحدًا على الأقل مقبول أيًّا كان عدد أسطره، فقد يترك إطار من الصور صورة واحدة في صفحة. والقطع داخل فقرة أو عنصر قائمة يظل يعدّ كل الأسطر على كل جانب (ويُعدّ الشكل أو الصيغة هناك سطرًا واحدًا)، ويترك أيضًا layout.boxChildSplitMinLines سطرًا على الأقل من تلك الفقرة أو العنصر على كل جانب (سطرين افتراضيًا؛ أو هذا الحدّ الأدنى نفسه إن كان أقل، فالقيمة 1 تسمح بسطر واحد). ومع القيم الافتراضية لا يُقسم عنصر من سطرين أو ثلاثة أبدًا، ولا يُقسم عنصر من أربعة أسطر إلا سطرين وسطرين. ومع القيمة الافتراضية لا ينقسم أي إطار تاركًا سطر نص وحيدًا في قدم عمود أو في رأس العمود التالي؛ وحين لا يحقّق أي قطع الحدّ الأدنى ينتقل الإطار كاملًا. (قبل postext 1.5 كان القطع داخل الفقرة يتحقّق فقط من أسطر الجانب كله، فقد ينقسم عنصر من سطرين سطرًا وسطرًا حين تكمل أسطر أخرى من الإطار الحدّ الأدنى.) |
repeatTitle | boolean | false | يكرّر العنوان في رأس كل تتمة للإطار المقسوم، متبوعًا بـcontinuedSuffix («Key points (cont.)»). ويأخذ العنوان المكرَّر نمط العنوان. والإطار الذي لا عنوان له لا يكرّر شيئًا. انظر علامات الإطار المقسوم. |
continuedSuffix | string | '(cont.)' | النص الذي يلي العنوان المكرَّر، بلغة المستند (locale، وإلا فلغة تقسيم الكلمات بالواصلة)، كما في الجدول المقسوم، ويُوصل بالعنوان كما يُوصل في الجدول. |
continuesMarkerEnabled | boolean | false | يضع continuesMarker تحت السطر الأخير من كل جزء من الإطار المقسوم له تتمة، داخل الإطار. |
continuesMarker | string | 'Continued' / 'Continúa' | نص تلك العلامة («(MORE)» في السيناريو)، بخط نص الإطار وحجمه، بحسب لغة المستند. |
continuesMarkerAlign | 'left' | 'center' | 'right' | 'right' | موضع العلامة ضمن العرض الداخلي للإطار. |
continuesMarkerItalic | boolean | true | يجعل العلامة مائلة. |
#الحاوية :::callout
يفتح السطرُ :::callout{type="<id>"} الإطار، ويغلقه سطر فيه ::: وحدها. يقبل السياج أربع سمات: type (معرّف النمط)، وtitle (يتجاوز عنوان النمط)، وspan وplacement (يتجاوزان قيمتي النمط)؛ ويُخرَج المحتوى الواقع بينهما داخل الإطار: عنوان اختياري، ثم الفقرات أو القوائم أو الاقتباسات الكتلية أو الصيغ أو الموارد المضمّنة، وكلٌّ منها يُنضَّد بتنسيق body / lists من النمط (وتحتفظ العناوين بأنماطها المعتادة). تندمج الهوامش بين العناصر الأبناء كما في النص الجاري؛ ويخرج داخل الإطار عن شبكة خطوط الأساس، ثم يعود التدفق إليها بعد الإطار بمسافة لا تقل عن marginBottom تحته (الأولوية للشبكة، والهامش حدّ أدنى، وهو العرف نفسه الذي تتبعه الموارد). أما النمط ذو snapToGrid: false فيحتفظ بقيمة marginBottom الدقيقة بدلًا من ذلك، ويبقى النص الذي يلي الإطار خارج الشبكة حتى العنوان التالي أو نهاية القائمة. والعنوان الذي يسبق الإطار مباشرةً يبقى معه.
حدود هذا الإصدار:
- يُبقى الإطار كاملًا ما لم يحدّد نمطه
keepTogether: false. فإن لم تتسع له المساحة المتبقية في العمود انتقل كاملًا إلى العمود أو الصفحة التالية، وكذلك يخرج من عمود فارغ قصّرته نطاقات العناصر العائمة أو حدّ أقصى للنطاق (band cap)، ما دام عمود كامل يتسع له. أما الإطار الأطول من عمود كامل فيُقسم بدلًا من ذلك، كالإطار القابل للتقسيم؛ ولا يوضع رغم ذلك ويفيض إلا الإطار الذي لا يقسمه أي قطع (شكل أو جدول أو مجموعة:::columnsأطول من العمود، أوsplitMinLinesلا يحقّقه أي قطع)؛ وعندئذ يسجّل الإخراج تحذيرcalloutOverflow(VDTDocument.warnings) يعرضه Sandbox. والإطار القابل للتقسيم يترك الجزء الذي يتسع (عناصر أبناء كاملة، أو أسطر فقرة بما لا يقل عنsplitMinLinesعلى كل جانب، ويكفي الشكل أو الجدول أو الصيغة المنفصلة وحده لجانب) ثم يستمر في إطار بلا أيقونة في العمود أو الصفحة التالية، بلا عنوان ما لم يكرّره النمط، ومع علامة اختيارية تحت الجزء الذي يتركه (انظر علامات الإطار المقسوم). - تُفسح العناصر العائمة المجال للإطار غير القابل للتقسيم. فحين تكون الكتلة التي تلي الإشارة إلى شكل مباشرةً إطارًا
keepTogether، لا تُؤخذ الخانة التي لا تترك للإطار عمودًا من النطاق الحالي يستقر فيه (العمود الذي يحوي الإشارة أو عمود فارغ بعده، وكان يتسع له قبل العنصر العائم): ينتقل الشكل إلى خانته التالية، وهي عادةً الصفحة التالية، ويبقى الإطار في التدفق، كما يضعه المنضِّد، بدلًا من دفع الإطار خارج الصفحة وترك العمود للشكل وحده. - في التخطيط متعدد الأعمدة يجعل
span: 'page'الإطارَ كتلة ممتدة (span block): يُخرَج بكامل عرض المحتوى ويقسم الصفحة إلى نطاقات أعمدة، فتُغلق أعمدة النص فوقه عند خط القطع، ويأخذ الإطار عمودًا خاصًا به بكامل العرض، ويُفتح تحته نطاق جديد من أعمدة النص، فيستمر التدفق تحت الإطار في كل الأعمدة. وحيث تكون الأعمدة مستوية (في أعلى الصفحة، أو مباشرةً تحت عنوان افتتاحيspan: 'page'، أو مباشرةً تحت كتلة ممتدة أخرى، أو مباشرةً تحت نطاق عائم علوي) يقطع الإطار هناك ببساطة. أما إذا ورد في منتصف الصفحة والأعمدة غير متساوية، فيُوضع كما يضعه المنضِّد: يُقطع النص فوقه قطعًا مستويًا عبر كل الأعمدة (يعيد المحرّك تنفيذ التوزيع وقد قُصّرت أعمدة النطاق إلى العدد نفسه من أسطر الشبكة، فيفيض النص من عمود إلى عمود على نحو طبيعي وتظل كل قواعد الأسطر اليتيمة والأرامل والإبقاء مع التالي سارية)، ويمتد الإطار على الصفحة، وتُستأنف الأعمدة تحته. ويتطلب القطع جولتين إضافيتين من التوزيع؛ وحين لا يترك خط القطع متسعًا للإطار مع الحدّ الأدنى لأسطر الأرملة من النص تحته، أو لا يتسع أي ترتيب بعد بضع محاولات، ينتقل الإطار إلى أعلى الصفحة التالية. ويلتزم النص فوقه بالقطع: الفقرة التي لا يمكن أن تبدأ في الأسطر القليلة التي يحتفظ بها عمود تحت شكل فوق القطع تنتقل إلى العمود التالي (فيقف الشكل وحده في عموده)، وحين تظل كتلة ما تتجاوز القطع، يُؤخذ القطع أدنى بسطر بدلًا من موضع انتهاء تلك الكتلة. والإطار المقسوم الذي يفتح النطاق يُقطع هناك أيضًا، فلا تتجاوز بقيته القطع أبدًا. ومعheadings.balancing.beforeSpan(القيمة الافتراضية) يُقطع النطاق الذي يتركه خلفه قطعًا مستويًا، كالنطاق الذي يختم الفصل، وحين يسمح النمط بالتقسيم (keepTogether: false) يختم جزءُ الإطار الذي يتسع تحت الأعمدة المسوّاة الصفحةَ ويفتح الباقي الصفحة التالية؛ ومعbeforeSpan: falseتُوازن الصفحة التي يتركها كالمعتاد، دون فرض فاصل صفحة. والعنوان الذي يسبق الكتلة الممتدة مباشرةً لا ينتقل معها. وفي التخطيطات أحادية العمود يكونspan: 'page'ببساطة داخل التدفق. placement: 'fixed'يُخرج الإطار من التدفق: يُخرَج الإطار (width: 'auto'يضيق على قدر العنوان، و'fill'يأخذ عرض عمود النص تحت نقطة الإرساء) ويُثبَّت في الصفحة التي يرد فيها ضمن التدفق، في الموضع الذي يصفهfixed.anchor/fixed.offset، وهو افتراضيًا الزاوية السفلية اليسرى من منطقة المحتوى. وتتخلى أعمدة النص التي يغطيها عن تلك المنطقة (تُقطع من الأسفل، أو من الأعلى إن كان العمود لا يزال فارغًا)، تمامًا كنطاق العناصر العائمة؛ وحين تكون المنطقة مشغولة بنص أو عنصر عائم أو كتلة ممتدة، ينتقل الإطار إلى الصفحة التالية. والإطار الثابت الذي يختم الفصل (أي تكون الكتلة التالية صفحة افتتاح فصل، أو:::part، أو إطارًا حاجزًا للعناصر العائمة، أو نهاية المستند) يسوّي أولًا الأعمدة فوقه (headings.balancing.trailing)، فتنتهي الصفحة الختامية القصيرة مستويةً مع الشارة تحتها. ويقع الإطار وأبناؤه فيpage.floatsويُرسمون خارج قصّ الأعمدة في كل مُخرِج.placement: 'top' | 'bottom'يعوّم الإطار كما يُعوَّم المورد: يغادر التدفق حيث يرد ويأخذ أول نطاق حرّ بعد تلك النقطة (قدم الصفحة الحالية مع'bottom'، أو رأس الصفحة التالية التي يفتحها التدفق أو قدمها) بعرض العمود (span: 'column') أو بكامل عرض المحتوى (span: 'page')؛ ويملأ النص الذي يليه الصفحة التي غادرها. ويذهب إطاره وأبناؤه إلىpage.floats، كما في الإطار الثابت. والإطار العائم الذي يتصدّر صفحة جديدة يوضع قبل الأشكال المنتظرة لتلك الصفحة، والشكل المذكور في صفحة سابقة الذي يتسع بعدئذ تحته يأخذ بقية تلك الصفحة حتى لو بقي أقل من ثلاثة أسطر نص (صفحة معرض: إطار وشكل، بلا نص بينهما). والإطارspan: 'side'لا يطفو أبدًا: يتراصّ بجانب النص أيًّا كان موضعه.width: 'auto'يضيق على قدر العنوان فقط، ويتجاهل العناصر الأبناء.:::calloutالمتداخل داخل إطار آخر إطار مستقل: يُخرَج بنمطه الخاص (الخلفية، والحدّ، ونصف قطر الزوايا، والحشوة، والشريط الجانبي، والعنوان، والأيقونة، والعلامة، واللسان، والتنسيق الطباعي) بكامل العرض الداخلي لأبيه ويتراصّ عنصرًا ابنًا واحدًا فيه، وتندمجmarginTop/marginBottomالخاصة به مع جيرانه. ويُتجاهلspanوplacementالخاصان به (من السياج أو من النمط)، فالإطار المتداخل يجري دائمًا داخل أبيه، ويُتجاهل كذلكfloatBarrierوsnapToGrid. وتتداخل الإطارات إلى أي عمق، ويمكن أن تقع داخل مجموعة:::columns(كلٌّ منها كاملًا في عمود واحد). وحين ينقسم الأب، يقع القطع قبل الإطار المتداخل أو بعده، أو داخله إن سمح النمط المتداخل بالتقسيم؛ ويعيد كل جزء رسم الإطارات التي يقطعها، والإطار المتداخل المستمر من الجزء السابق يُسقط عنوانه وأيقونته، كتتمة الإطار في المستوى الأعلى.- مجموعة
:::columns{count=N}…:::بين العناصر الأبناء تُخرج تلك العناصر فيNأعمدة متساوية العرض، يفصل بينهاcolumnGap، داخل الإطار: تُقطع السلسلة عند حدود الكتل أو الأسطر التي تسوّي الأعمدة على أفضل وجه (الفقرة أو عنصر القائمة المقطوع في منتصفه يستمر في رأس العمود التالي بلا نقطة تعداده)، ويبدأ كل عمود من أعلى المجموعة ويكون ارتفاع المجموعة بارتفاع أطول أعمدتها؛ وتعود العناصر التي تليها إلى كامل العرض. والإطار المقسوم (keepTogether: false) لا يُقطع أبدًا داخل مجموعة. استخدمها لملخّص من عمودين للنقاط الرئيسية، أو لوضع جداول إطار عريض جنبًا إلى جنب. - السمة الخامسة للسياج،
label، تُطبع على لسان التسمية في النمط (انظرlabelأعلاه)::::callout{type="box" label="BOX 1-1" title="The octet rule"}؛ ومن دونlabelفي النمط تُتجاهل السمة.
في VDT يكون الإطار كتلة إطار type: 'callout' تقع زخرفته (الخلفية، والشريط الجانبي، والأيقونة، والعنوان) في designOverlay، تليها كتلها الأبناء في العمود نفسه؛ ويحمل الإطار وكل ابن containerId الخاص بالسياج. والإطار المتداخل كتلة إطار type: 'callout' مستقلة بين الأبناء، تليها كتلها؛ وهي تحتفظ بـcontainerId الخاص بسياج المستوى الأعلى (فيظل التوزيع والموازنة يريان وحدة واحدة) وتضيف calloutPath، أي معرّفات حاويات الأسيجة المتداخلة المحيطة بها، من الخارج إلى الداخل (ومعرّف الإطار المتداخل نفسه هو آخر مدخل). ويعطي ملف PDF الموسوم كل إطار متداخل عنصر Div داخل عنصر أبيه. وتُحلّ صور الأيقونات كما تُحلّ صور الموارد: سجل الصور في canvas، والخيار resourceImageUrl في HTML، ومزوّد resourceBytes في PDF.
const resolved = resolveCalloutStylesConfig(config.calloutStyles, resolvedBodyText, resolvedHeadings, resolvedUnorderedLists, config.locale);
// => every inherited field filled from the resolved sections; the optional
// locale picks the language of the continuation strings
const minimal = stripCalloutStylesDefaults(config.calloutStyles);
// => undefined for the built-in `note` default; static defaults dropped#التنسيق الطباعي داخل الإطار
ينضّد الإطار محتواه بتنسيق body وlists الخاص به؛ وكل ما عدا ذلك يحتفظ بأنماط المستند:
- الفقرات تأخذ
bodyالخاص بالإطار: الخط، والحجم، وتباعد الأسطر، واللون، وألوان التوكيد، والأوزان، وitalic، وsmallCaps، والمحاذاة، وتقسيم الكلمات بالواصلة، والإزاحة البادئة، والتباعد بين الفقرات. والحقل غير المُعيَّن يرثbodyText. ولون التوكيد الموروث يحتفظ بارتباطه بلوحة الألوان، فيتغيّر النص العريض والمائل وتسميات:refداخل الإطار معcolorPaletteكما تتغيّر خارجه. (حتى postext 1.4 كان النص العريض في الإطار يبقى#295AA3أيًّا كان اللون الرئيسي.) - القوائم النقطية تأخذ
listsالخاص بالإطار (رمز التعداد، واللون، وحجم الرمز ووزنه، والإزاحة، والفاصل، والتباعد بين العناصر) فوقunorderedLists، ونصّها هو نص الإطار. وlists.bulletCharأوlists.colorالمختلف عن قيمة المستند (unorderedLists) يستبدل رمز التعداد أو اللون في كل المستويات؛ أما المكرِّر لقيمة المستند أو غير المُعيَّن فيترك لكل مستوى قيمته (unorderedLists.levels)، فتبقى الشرطات المتداخلة في الإطار. - القوائم المرقّمة تأخذ
lists.indentوgapوitemSpacing، وتأخذlists.colorمتى حدّده النمط، حتى لو كان لون رموز التعداد في المستند؛ والنمط الذي لا يحدّده يُبقي الأرقام بلونorderedLists.color. (حتى postext 1.4 لم يكن اللون يصل إلى الأرقام إلا إذا اختلف عنunorderedLists.color، فلم يكن لتعيينه على ذلك اللون نفسه أي أثر.) أما الرقم نفسه (خطه وحجمه وفاصله) فيأتي منorderedListsالعام، لأنlistsلا يضم إلا حقول رموز التعداد: نسّق أرقام الإطار هناك. :::paragraphsداخل الإطار تستخدم نمط الفقرة الخاص بها، وكذلك في الإطارات المتداخلة. والحقول التي لا يحدّدها النمط ترثbodyTextالخاص بالمستند، لاbodyالخاص بالإطار (ولا ميله ولا أحرفه الكبيرة الصغيرة).- مجموعات
:::columnsلا نمط خاصًا بها: تتشارك الأعمدة كلها تنسيق نص الإطار وقوائمه، ويحدّدcolumnGapالفاصل بينها. - الاقتباسات الكتلية تأخذ خط نص الإطار وحجمه وأوزانه (مائلة ورمادية، كما في النص الجاري)، وأحرفه الكبيرة الصغيرة. والعناوين تحتفظ بأنماط العناوين؛ والصيغ المنفصلة بإعدادات الرياضيات.
- الأشكال والجداول تحتفظ بأنماط التعليقات والجداول في المستند، بالعرض الداخلي للإطار؛ ويتبع وزناها العادي والعريض أوزانَ
bodyفي الإطار. - الشارات (chips) تحتفظ بنمط الشارة الخاص بها؛ ويُقرأ حجم
emنسبةً إلى حجم نص الإطار. :::spaceيُقاس بأسطر نص الإطار (انظر:::spaceلمعرفة المواضع التي يُسقط فيها).- الإطار المتداخل يأخذ نمطه الخاص كاملًا؛ ويُتجاهل
spanوplacementوfloatBarrierوsnapToGridالخاصة به.
#علامات الإطار المقسوم
حين ينقسم الإطار عبر الأعمدة أو الصفحات (keepTogether: false، أو إطار أطول من عمود)، يبدأ كل جزء بعد الأول بلا عنوان ولا أيقونة، ولا شيء افتراضيًا يخبر القارئ بأن الإطار مستمر. ويضيف خياران العلامات التي يستعملها الكتاب أو السيناريو:
repeatTitle: trueيكرّر العنوان في رأس كل تتمة، متبوعًا بـcontinuedSuffix، فيكون افتراضيًا «Key points (cont.)». ويأخذ العنوان المكرَّر نمط العنوان، فمعtextTransform: 'uppercase'يعطي «HAMLET (CONT'D)» كما في السيناريو. وتبقى الأيقونة ولسان التسمية في الجزء الأول.continuesMarkerEnabled: trueيضعcontinuesMarker(«Continued»، أو «Continúa» في مستند إسباني) تحت السطر الأخير من كل جزء له تتمة، داخل الإطار، بخط نص الإطار وحجمه: مائلًا ما لم يكنcontinuesMarkerItalicمساويًاfalse، ومحاذيًا لليمين ما لم يحدّدcontinuesMarkerAlignالقيمة'left'أو'center'. وتأخذ العلامة مساحة في الجزء الذي تختمه، ويُختار القطع بحيث تتسع لها.
calloutStyles: [{
id: 'speech',
keepTogether: false,
titleStyle: { textTransform: 'uppercase' },
repeatTitle: true,
continuedSuffix: "(CONT'D)",
continuesMarkerEnabled: true,
continuesMarker: '(MORE)',
continuesMarkerAlign: 'center',
continuesMarkerItalic: false,
}],:::callout{type="speech" title="Hamlet"}
A speech long enough to run over the foot of the page…
:::ينتهي الجزء الذي يختم الصفحة بـ«(MORE)»، وتبدأ الصفحة التالية بـ«HAMLET (CONT'D)». وكلاهما من لوازم ترقيم الصفحات: في ملف PDF الميسَّر هما عنصران زخرفيان (artifacts)، وفي HTML يُخفيان عن التقنيات المساعدة، فيُقرأ العنوان مرة واحدة. وفي VDT هما كتلتا نص في designOverlay تحملان العلامة artifact: true.
#الأجزاء
تضبط الخاصية parts صفحات فواصل الأجزاء التي يفتحها المستند بالحاوية :::part{number="…" title="…"}، أي صفحة «الجزء الأول: الأسس» التي تجمع سلسلة من الفصول. ويشغل الجزء دائمًا صفحة خاصة به: تنتقل الحاوية إلى صفحة جديدة بالزوجية المضبوطة، وتحوّلها إلى صفحة أحادية العمود تأتي منطقة متنها من parts.margins، وتضع تصميم الافتتاح على الصفحة كلها، ثم تنتقل مجددًا بعد السياج الختامي فيبدأ الفصل التالي (بـbreakBefore.parity الخاص به) من صفحة نظيفة؛ ومع إعدادات H1 الافتراضية ينتج ذلك الترتيب التقليدي: صفحة الجزء فردية، تليها صفحة زوجية فارغة، ثم الفصل في الصفحة الفردية التالية.
const config: PostextConfig = {
parts: {
breakBefore: { parity: 'odd' },
breakAfter: { enabled: true, parity: 'any' },
margins: { top: { value: 9, unit: 'cm' }, left: { value: 3, unit: 'cm' }, right: { value: 3, unit: 'cm' } },
design: {
elements: [
{
kind: 'text', id: 'number', content: 'Part {numberRoman}',
fontSize: { value: 12, unit: 'pt' }, fontWeight: 600, align: 'left',
placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: { value: 3, unit: 'cm' }, y: { value: 5, unit: 'cm' } }, size: { width: 'auto', height: 'auto' } },
},
{
kind: 'text', id: 'title', content: '{titleText}',
fontSize: { value: 28, unit: 'pt' }, fontWeight: 700, align: 'left', overflow: 'wrap',
placement: { anchor: { to: '#number', edge: 'below' }, size: { width: { value: 15, unit: 'cm' }, height: 'auto' } },
},
],
},
bodyStyle: { fontSize: { value: 11, unit: 'pt' }, numberColor: { hex: '#AA0000', model: 'hex' } },
},
};:::part{number="I" title="Foundations"}
1. The lantern and its parts
2. Trimming the wick
3. Reading the weather
:::
# The lantern and its parts| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
page | boolean | true | هل تفتح :::part صفحة فاصلة. مع false لا تُفتح صفحة ولا يُنضَّد متن السياج: يسري رقم الجزء وعنوانه ولوحة ألوانه بدءًا من المحتوى التالي، دون فاصل خاص بها. الاستعمال المعتاد: htmlViewer.overrides.parts.page: false، أي نسخة للشاشة بلا فواصل أقسام. |
breakBefore.parity | HeadingBreakParity | 'odd' | زوجية الصفحة التي يبدأ فيها الجزء. القيم نفسها وقواعد ملكية الصفحات الفارغة نفسها كما في breakBefore للعناوين: الصفحة الفارغة المُدرجة لبلوغ الزوجية تتبع الجزء (فـ فيها يشير بالفعل إلى الجزء الجديد)؛ أما الفاصل الإلزامي في 'always-*' فيتبع المحتوى السابق. |
breakAfter.enabled | boolean | true | ينقل المحتوى الذي يلي السياج الختامي إلى صفحة جديدة. ومع false يستمر في العمود الوحيد لصفحة الجزء. |
breakAfter.parity | HeadingBreakParity | 'any' | زوجية تلك الصفحة الجديدة. اتركها على 'any' ودع breakBefore.parity الخاص بالفصل التالي يقرّر هل تليها صفحة زوجية فارغة. ويُطبَّق الفاصل عند وضع الكتلة التالية، فالجزء الذي يختم المستند لا يترك صفحة فارغة في آخره. |
margins | PageMargins | هوامش الصفحة | منطقة المتن في صفحة الجزء، أي العمود الوحيد الذي تجري فيه الكتل الواقعة داخل السياج. كل جانب غير مُعيَّن يرث هامش الصفحة؛ وmirror يبادل الداخلي والخارجي في الصفحات الزوجية تمامًا كهوامش الصفحة. |
design | DesignSlot | فارغ | تصميم الافتتاح. حاويته هي صندوق حدّ القص للصفحة، فتتطابق مراسي الحاوية ومراسي 'page'، ويمتد 'bleed' حتى النزف حين تكون خطوط القص مفعّلة. زخرفي بحت: لا يحجز أي مساحة من المتن، فارفع margins.top لإبعاد المتن عنه. وحين يكون فارغًا، يُولَّد نص افتراضي بتنسيق H1 في أعلى يسار منطقة المتن، مع numberSeparator الخاص بـH1 بين الرقم والعنوان. |
versoDesign | DesignSlot | فارغ | تصميم الصفحة الزوجية الفارغة التي تلي صفحة الجزء (ظهر الورقة الفاصلة): الحاوية والعناصر النائبة نفسها كما في design. اتركه فارغًا لصفحة زوجية خالية. ولا يُرسم إلا حين تبقى الصفحة التي تلي صفحة الجزء فارغة، وهذا يتطلب فاصلًا بزوجية محددة: انظر تصميم الصفحة الزوجية أدناه. |
bodyStyle.fontFamily، fontSize، lineHeight، color، textAlign | كما في bodyText | ترث bodyText | التنسيق الطباعي للفقرات والاقتباسات الكتلية وعناصر القوائم داخل السياج. وتأتي الأوزان وألوان التوكيد وتقسيم الكلمات بالواصلة من نص المتن. |
bodyStyle.bulletColor | ColorValue | unorderedLists.color | لون رموز التعداد في القوائم النقطية داخل الجزء. |
bodyStyle.numberColor | ColorValue | orderedLists.color | لون الأرقام في القوائم المرقّمة داخل الجزء. وتُنضَّد الأرقام دائمًا بالوزن العريض لنص المتن، فتُقرأ قائمة الفصول كجدول محتويات. |
bodyStyle.unorderedLists | UnorderedListsConfig | — | تجاوزات جزئية تُطبَّق فوق unorderedLists الخاص بالمستند داخل الجزء، بعد bulletColor. والقيم التي تخص القائمة كلها تسري إلى المستويات التي ورثتها؛ ومدخلات levels تنطبق على مستواها فقط. |
bodyStyle.orderedLists | OrderedListsConfig | — | تجاوزات جزئية تُطبَّق فوق orderedLists الخاص بالمستند داخل الجزء، بعد numberColor والوزن العريض، مثل separator قيمته '•' مع separatorFontFamily وseparatorColor خاصين به لقائمة الفصول في صفحة افتتاح الجزء. |
#العناصر النائبة في تصميم الجزء
تحلّ خانة التصميم مجموعة العناصر النائبة (placeholders) الخاصة بالعناوين بقيم الجزء نفسه: {titleText} هو title السياج؛ و{number} هو number كما كُتب تمامًا؛ و{numberDecimal} و{numberRoman} و{numberRomanLower} و{numberAlpha} و{numberAlphaLower} تعيد تنسيقه: يُحلَّل الرقم على أنه عدد عشري أو رقم روماني أو أرقام صينية، مع الكلمات التي تحيط بها أو دونها ("IV" و"iv" و"4" و"4" و"四" و"卷四" و"第四卷" كلها تعطي {numberDecimal} = 4)، وتُحَلّ إلى '' في أي حالة أخرى. وتتوفّر أيضًا {partTitle} / {partNumber}، و{chapterTitle} / {chapterNumber} (الفصل الذي يسبق الجزء)، و{pageNumber}، و{totalPages}، و{bookTotalPages}، والعناصر النائبة للبيانات الوصفية. و{attr.<key>} يقرأ سمات H1 للفصل الحالي.
#تصميم الصفحة الزوجية
يزيّن versoDesign الصفحة التي تلي صفحة الجزء مباشرةً حين لا تحمل محتوى: ظهر الورقة الفاصلة. والجزء نفسه لا يترك تلك الصفحة فارغة أبدًا. فالقيمة الافتراضية لـbreakAfter.parity هي 'any'، لذا يبدأ المحتوى الذي يلي السياج في الصفحة التالية مباشرةً ما لم يطلب شيء زوجية محددة:
- عنوان الفصل التالي. قيمة
breakBeforeالافتراضية لـH1 هي'always-odd'، و'odd'تفعل الشيء نفسه بعد صفحة جزء فردية: ينتقل الفصل إلى الصفحة الفردية التالية وتبقى الزوجية فارغة. فيُرسم تصميم الصفحة الزوجية. parts.breakAfter: { enabled: true, parity: 'odd' }. يطلب الجزء بنفسه الصفحة الفردية التالية، أيًّا كان ما يفعله العنوان التالي. استخدمه حين يمكن أن تبدأ الفصول في أي من الجانبين (breakBefore.parity: 'any'، أوbreakBefore.enabled: false).
ومن دون أيٍّ منهما يبدأ الفصل في الصفحة الزوجية ولا يُرسم تصميمها. ومع breakAfter.enabled: false يستمر المحتوى في صفحة الجزء نفسها. وتأخذ الصفحة الزوجية لوحة ألوان الجزء، فالسمة palette="band=#…" في السياج تعيد تلوينها أيضًا.
parts: {
breakBefore: { parity: 'odd' },
breakAfter: { enabled: true, parity: 'odd' }, // always a blank verso to paint
versoDesign: {
elements: [{
kind: 'box', id: 'field',
style: { backgroundColor: { hex: '#b07d2b', model: 'hex', paletteId: 'band' } },
placement: { anchor: { to: 'page', edge: 'top-left' }, size: { width: 'fill', height: 'fill' } },
}],
},
}جزء يختم فصله. في كتاب يُخرَج فصلًا فصلًا (Sandbox، وbuildBundle) يمكن أن يكون السياج :::part فصلًا قائمًا بذاته، أو نهاية فصل. فتكون صفحة الجزء عندئذ آخر صفحات الفصل، ويتولى الفصل التالي ما بقي على الجزء: يطبّق breakAfter قبل كتلته الأولى ويرسم versoDesign في صفحته الأولى حين تبقى فارغة. فتخرج الصفحات كما لو كان الكتاب كله في مستند واحد. ولا يجوز أن يلي السياج إلا التوجيهات التي لا تضع شيئًا (:::numbering، :::space)؛ وكل ما عداها محتوى من الفصل، فيأخذ الفاصل بنفسه. والفصل الفارغ الذي يلي الجزء مباشرةً صفحة قائمة بذاتها: وتلك الصفحة هي الزوجية. والمضيف الذي يُخرج الفصول بنفسه يحصل على ذلك من continuationAfter()، التي تُبلغ afterPartPage: true للفصل الذي ينتهي بجزء؛ مرّرها في continuation الخاص بالفصل التالي.
#الحاوية :::part
يفتح السطر :::part{number="…" title="…"} الجزء، ويغلقه ::: وحدها؛ والسمتان اختياريتان (قيمتهما الافتراضية ''). تجري الكتل الواقعة بينهما (قائمة الفصول عادةً) في العمود الوحيد لصفحة الجزء بتنسيق bodyStyle، بدءًا من margins.top؛ والمتن الأطول من الصفحة يستمر في صفحات عادية. وتُصنَّف الصفحة role: 'part' (ويحمل VDTPage.partInfo الرقم والعنوان)، فيمكن لعناصر الترويسة والتذييل أن تستهدفها أو تتخطاها بـpages: 'part' / pages: 'body'؛ ويضيف مُخرِج PDF الجزء إلى المخطط (outline) فوق فصوله. والمتن الفارغ (:::part{…} يليه ::: مباشرةً) هو الحالة الشائعة، وهو ينتج الصفحة مع ذلك، فالأجزاء المتتالية لا تتشارك صفحة أبدًا. و:::part المتداخل في جزء آخر يُدمج في الجزء الخارجي.
وتعطي سمة ثالثة، palette="<id>=<hex>[, <id>=<hex>…]"، الجزءَ ألوانه الخاصة: في صفحة الجزء وفي كل صفحة تليها حتى الجزء التالي، يأخذ كل لون تصميم (الترويسة، والتذييل، ونطاق الافتتاح، وتصميما الجزء والصفحة الزوجية) مرتبط بأحد معرّفات لوحة الألوان هذه قيمةَ الجزء بدلًا من قيمة لوحة ألوان المستند. بهذا تعيد أقسام الكتاب تلوين لسان الزاوية ونقطة الترويسة ونطاق صفحة افتتاح الفصل دون تصميم ثانٍ: :::part{number="II" title="…" palette="band=#f6c297"}. ويتبعها تدفق النص أيضًا: في الصفحات نفسها، كل لون في التدفق يساوي القيمة الأساسية لمدخل متجاوَز في لوحة الألوان يأخذ قيمة الجزء، وذلك في ألوان العناوين والنص العريض والمائل والإحالات، ورموز التعداد وأرقام القوائم، وتسميات التعليقات وأشرطتها، ونص الجداول وخطوطها وتعبئاتها (الرأس، والمتن، والصفوف المتناوبة، وتعبئة الخلية الخاصة)، وإطارات callout (الخلفية، والحدّ، والشريط الجانبي، والعنوان)، والشارات (التعبئة، والحدّ الخارجي، والنص)؛ فالخاصية headings.levels[1].color المرتبطة بـband تجعل عناوين كل قسم بلونه الخاص. وتحتفظ عيّنات الألوان المضمّنة باللون المكتوب فيها. وتُفصل الأزواج بفواصل أو فواصل منقوطة أو مسافات، ويربط = أو : بين المعرّف واللون، و# اختيارية. ويظل الجزء ساريًا بعد إغلاق سياجه: يتبع {partTitle} و{partNumber} ولوحة الألوان التدفقَ إلى الفصول التي تليه، ومن خلال continuation.part الذي تُبلغ عنه continuationAfter() إلى الفصول التي تُخرَج منفردة، فيُظهر الفصل الثاني من القسم القسمَ في ترويساته تمامًا كما يفعل الأول.
قد يتشارك مدخلان من لوحة الألوان قيمة أساسية واحدة ويأخذان مع ذلك قيمتين مختلفتين في جزء ما، حين يتجاوز الجزء أحدهما دون الآخر أو يعطيهما لونين مختلفين. والقيمة وحدها لا تدل على المدخل الذي جاء منه لون التدفق، لذا يُطابَق كل لون في التدفق مع الإعدادات التي يمكن أن يأتي منها، ويأخذ القيمة التي ترتبط بها تلك الإعدادات. ويُميَّز بين ما يلي:
- ألوان نص الكتلة: لون النص، وألوان النص العريض والمائل والإحالات، وعلامة القائمة (رمز تعداد أو رقم) والفاصل الذي يلي الرقم. فإذا كان
bodyText.colorمرتبطًا بـinkوbodyText.boldColorمرتبطًا بـaccent، وكلاهما#1a1a1a، فإن الجزء ذاpalette="accent=#b8413d"يعيد تلوين المقاطع العريضة ويترك النص؛ وإذا كانunorderedLists.colorهو المرتبط بـaccent، فإنه يعيد تلوين رموز التعداد ويترك نص العناصر؛ - كل مستوى من مستويات العناوين، وكل نمط عنوان يحدّد لونًا؛
- نص صف المحتويات، ورقمه، ورقم صفحته وعنوانه الفرعي؛
- في كل نمط إطار: التعبئات (الخلفية، والشريط الجانبي، ولسان التسمية)، والحدّ، والخطوط (خط العلامة وخط التسمية)، والنص (العنوان، والأيقونة، ورمز العلامة، والتسمية)؛
- كل لون في كل نمط جدول وشارة وتعليق. ونمط الجدول المسمّى منفصل عن
tableStyle، ونمط تعليق نوع المورد منفصل عنcaptionStyle. وتعبئة الخلية الخاصة تتبع ارتباطها الخاص.
تبقى حالة واحدة تُحسم بالقيمة: إعدادات في أماكن مختلفة تحدّد اللون نفسه لكتلة ما. فـbodyText.color وbodyText.blockquote.color وcolor في نمط الفقرة وbody.color في نمط الإطار كلها تحدّد لون نص الكتلة، مثلًا. وحين يرتبط اثنان منها بمدخلين يتشاركان قيمة أساسية ويفرّق الجزء بينهما، يأخذ اللون القيمة المتجاوِزة (وآخرهما كتابةً إن تُجووز كلاهما). أعطِ هذه المدخلات قيمًا أساسية خاصة بها.
const resolved = resolvePartsConfig(config.parts, resolvedPage, resolvedBodyText, resolvedUnorderedLists, resolvedOrderedLists);
// => margins filled from the page, bodyStyle from the body / list configs
const minimal = stripPartsDefaults(config.parts);
// => undefined when only static defaults remain#أنماط العناوين
تُعرِّف الخاصية headingStyles أنماطًا مسمّاة يطبّقها المستند على عنوان بالصيغة # Title {style="<id>"}. ويفعل النمط أمرين. فهو يتجاوز تنسيق مستوى العنوان وتصميمه وترقيمه، أي كل حقل من مدخل المستوى ما عدا level (الخط، والحجم، واللون، وbreakBefore، وspan، وadvancedDesign، وtextTransform، وhidden، وnumberingTemplate…)، ويحكم القسم الذي يفتحه العنوان: تأخذ صفحاته، حتى العنوان التالي من المستوى نفسه أو مستوى أعلى، ترويسات النمط وهندسة صفحاته وتنسيق متنه ولوحة ألوانه. بهذا تقع الصفحات التمهيدية للكتاب (تمهيد في عمود عريض واحد بأرقام صفحات رومانية ونطاقات زرقاء) في دليل من عمودين مرقّم بأرقام عشرية، دون إعداد ثانٍ.
const config: PostextConfig = {
headingStyles: [
{
id: 'front-matter',
numbered: false,
breakBefore: { enabled: true, parity: 'odd' },
span: 'page',
advancedDesign: { enabled: true, minHeight: { value: 52, unit: 'mm' }, slot: { elements: [/* bands, `{titleText}` */] } },
header: { elements: [/* folio | rule | `{title}. {subtitle}` */] },
margins: { left: { value: 50, unit: 'mm' }, right: { value: 17, unit: 'mm' } },
layout: { layoutType: 'single' },
bodyStyle: { fontSize: { value: 10.5, unit: 'pt' }, textAlign: 'justify' },
palette: { band: '#547396' },
},
],
};# Preface {style="front-matter"}| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
id | string | — | المعرّف الذي يُشار إليه بـ في سطر العنوان. والمعرّف غير المعروف يترك العنوان على حاله. |
name | string | id | اسم مقروء للبشر (لواجهة المحرّر فقط). |
numbered | boolean | true | هل يُحتسب العنوان: يزيد عدّاد مستواه (أرقام numberingTemplate، و في أرقام الموارد)، والترتيب التسلسلي للفصل الذي يقوم عليه ، والرقم المطبوع في المحتويات. false للتمهيد، أو قائمة المؤلفين، أو الفهرس الأبجدي: يبقى أول فصل مرقّم بعدها الفصل 1، ويكون فارغًا في صفحاتها. |
toc | boolean | true | هل يُدرج :::toc العنوان. ويتجاوزه العنوان بـ / . |
runningChapter | boolean | true | هل يصبح عنوان المستوى الأول من هذا النمط الفصلَ الجاري: الفصل الذي تسمّيه و و وصيغها …AtTop في صفحته والصفحات التي تليها. false للوحة مصوّرة (plate) أو خريطة أو غلاف منضَّد كعنوان H1 داخل فصل: تتخطاه الترويسات، في صفحته أيضًا، وتواصل تسمية الفصل الذي يقطعه؛ ولا يحدّد كذلك كلمةً دليلية h1. ويظل العنوان يُحتسب حين يكون numbered (فتصميمه يقرأ الخاص به) ويظل :::toc يُدرجه حين يكون toc. ومع toc: false أيضًا لا يحصل على علامة مرجعية في PDF: فاللوحة المصوّرة للفصل في الصفحة التي تسبق صفحة افتتاحه تترك العلامات المرجعية للفصول. وعناوين المستويات الأخرى تتجاهله. ومع القيمة الافتراضية تطبع الصفحات التي تلي اللوحة المصوّرة عنوانها. والتمهيد أو الاستهلال ذو numbered: false يظل فصلًا قائمًا بذاته ويحتفظ بالقيمة الافتراضية. ولا يغيّر هذا الخيار إلا الفصل الذي تسمّيه العناصر النائبة: فالنمط يظل يفتح قسمًا خاصًا به، كما يفعل كل نمط عنوان، فتأخذ الصفحات حتى عنوان المستوى الأول التالي خانات الترويسة وهوامش نمط اللوحة المصوّرة وأعمدته ونمط متنه ولوحة ألوانه (وقيم المستند حيث لا يحدّد النمط شيئًا)، لا تلك التي لقسم منسَّق فتحه الفصل المقطوع. وفي الكتاب الذي يُخرَج فصلًا فصلًا، لا تنتقل الترويسات من ملف فصل إلى الذي يليه، فاللوحة المصوّرة التي تفتح ملفًا تُظهر عناصر نائبة فارغة للفصل حتى أول عنوان فصل في الملف. |
| حقول المستوى | كما في headings.levels[] | قيم المستوى | fontFamily، fontSize، lineHeight، fontWeight، italic، color، marginTop، marginBottom، snapToGrid، breakBefore، span، advancedDesign، textTransform، letterSpacing، hidden: كلٌّ منها، إذا عُيِّن، يحلّ محلّ قيمة مستوى العنوان في عناوين هذا النمط. وbreakBefore يُدمج حقلًا حقلًا فوق قيمة المستوى: النمط الذي لا يحدّد إلا parity يحتفظ بقيمة enabled الخاصة بالمستوى، والذي لا يحدّد إلا enabled: true يحتفظ بزوجية المستوى (حتى postext 1.4 كان الحقل الناقص يأتي بدلًا من ذلك من القيمة الافتراضية التي لا فاصل فيها). |
numberingTemplate | string | قيمة المستوى | القالب الذي تُرقَّم به عناوين النمط بدلًا من قالب مستواها (الرموز نفسها كما في levels[].numberingTemplate). ويبقى العدّاد عدّاد المستوى: نمط الملاحق ذو 'Appendix ' بعد خمسة فصول سيطبع Appendix F، لذا أعد بدء العدّ بـ في أول ملحق. و'' لا يطبع رقمًا مع أن العنوان يظل يُحتسب: ولا حتى الترتيب التسلسلي للفصل الذي تُظهره المحتويات و لعنوان من المستوى الأول بلا قالب. ويظهر الرقم في التدفق، وفي لتصميم النمط، وفي المحتويات، وفي . |
header، footer | DesignSlot | قيم المستند | ترويسات صفحات القسم، تحلّ هناك محلّ header / footer (وتظل مرشحات parity وpages للعناصر سارية). والخانة الفارغة تزيلها. |
margins | PageMargins | هوامش الصفحة | منطقة المتن في صفحات القسم؛ كل جانب غير مُعيَّن يرث هامش الصفحة، بما في ذلك mirror. ويسري على الصفحات التي يفتحها القسم، فاقرنه بـbreakBefore. |
layout | LayoutConfig | layout | تخطيط أعمدة صفحات القسم (layoutType، gutterWidth…): عمود عريض واحد لتمهيد منضَّد في كتاب من عمودين. ويُرسم columnRule الخاص به في صفحات القسم، وكل حقل لا يحدّده يأخذ قيمة layout.columnRule من المستند، فالقسم الذي لا يغيّر إلا أعمدته يحتفظ بالخط الفاصل للمستند (انظر الخط الفاصل بين الأعمدة). |
bodyStyle | PartsBodyStyleConfig | ترث bodyText | التنسيق الطباعي للفقرات والاقتباسات الكتلية والقوائم في القسم، بالحقول نفسها كما في parts.bodyStyle. |
palette | Record<string, string> | | تجاوزات لوحة الألوان (المعرّف ← قيمة hex) لصفحات القسم، فوق تجاوزات الجزء الحالي: الآلية نفسها التي تتبعها السمة palette في الجزء، وبالمدى نفسه. فهي لا تشمل خانات التصميم المُخرَجة في تلك الصفحات وحدها (الترويسات، ونطاق الافتتاح، وكل لون مرتبط بمعرّف متجاوَز)، بل تشمل تدفق النص أيضًا، بالقيمة: كل لون في التدفق يساوي القيمة الأساسية لمدخل متجاوَز يأخذ قيمة القسم، كما تحت الجزء، وذلك في ألوان العناوين والنص العريض والمائل والإحالات، ورموز التعداد وأرقام القوائم، وتسميات التعليقات وأشرطتها، ونص الجداول وخطوطها وتعبئاتها، وإطارات callout (الخلفية، والحدّ، والشريط الجانبي، والعنوان)، والشارات (التعبئة، والحدّ الخارجي، والنص)، بما في ذلك قاعدة المدخلين اللذين يتشاركان قيمة أساسية (انظر الحاوية :::part). وتحتفظ عيّنات الألوان المضمّنة باللون المكتوب فيها. |
يُغلق القسم عند العنوان التالي من المستوى نفسه أو مستوى أعلى: العنوان # غير المنسَّق بعد عنوان منسَّق يعيد ترويسات المستند وهندسته؛ والعنوان المنسَّق يفتح قسمه الخاص. والصفحات التي تركها القسم فارغة لأجل الزوجية تتبعه، كما تتبع عناوينَ الفصول.
فاصل الصفحة لا يحلّ محلّ فاصل العنوان نفسه. يرث النمط breakBefore الخاص بمستواه (القيمة الافتراضية للمستوى الأول { enabled: true, parity: 'always-odd' })، ويطبّقه العنوان أينما وقع، حتى بعد :::pagebreak مباشرةً. يفتح فاصل الصفحة صفحة جديدة، ثم يظل العنوان يطلب جهته من الصفحتين المتقابلتين. مع parity: 'odd'، الفاصل الذي يقع على صفحة زوجية تليه صفحة فارغة، فيبدأ العنوان في الصفحة الفردية التالية. ومع 'always-odd' تأتي الصفحة الفارغة الفاصلة أيضًا، ولا يغيّر فاصل الصفحة شيئًا، لأن العنوان كان سيفتح تلك الصفحة على أي حال. والنمط الذي يُراد أن يبدأ في الصفحة التي يفتحها فاصل يدوي، كصفحة المحتويات بعد صفحة العنوان، يعطّل فاصله الخاص:
headingStyles: [
// Starts where the text puts it: on the page the `:::pagebreak` before it opened.
{ id: 'contents', numbered: false, toc: false, breakBefore: { enabled: false } },
],ولإبقاء صفحة خاصة به دون اختيار جهة، اضبط breakBefore: { parity: 'any' } بدلًا من ذلك واحذف :::pagebreak.
إلى أي قسم تنتمي الصفحة. تُختار الترويسات ولوحة الألوان لكل صفحة، لا لكل عنوان. وتأخذ الصفحة القسم الساري بعد آخر تغيير للقسم فيها: حيث ينتهي قسم ويبدأ آخر في الصفحة نفسها (حرفان قصيران من معجم، مثلًا) تحمل الصفحة ترويسات الثاني ولوحة ألوانه؛ وحيث ينتهي قسم منسَّق في منتصف الصفحة عند عنوان غير منسَّق، تعود الصفحة إلى قيم المستند. و{chapterTitle} يتبع القاعدة نفسها: الصفحة التي يلتقي فيها فصلان تُظهر عنوان الأخير منهما. والصفحات الفارغة تتبع قاعدة عناوين الفصول: الصفحة الفارغة لأجل الزوجية (blankForParity) تتبع القسم الذي يُفتح بعدها، والصفحة الفاصلة التي يضيفها فاصل 'always-odd' / 'always-even' (blankForForce) تتبع القسم الذي قبلها. وفاصل الجزء يُغلق القسم المفتوح.
# A {style="letter"}
Aardvark, abacus.
# B {style="letter"}
Babble, badger… (runs on to the next page)يبدأ الحرفان كلاهما في الصفحة 1، فتأخذ الصفحة 1 ترويسات القسم B: لسان الإبهام (thumb tab) المضبوط في ترويسة النمط يقرأ «B» هناك، ولا تحمل أي صفحة لسان «A». أعطِ كل قسم صفحة خاصة به (breakBefore) حين يحتاج كلٌّ منها إلى لسانه.
ملاحق مرقّمة بالحروف بعد فصول مرقّمة بالأرقام، وصفحة إهداء تُدرجها المحتويات والعلامات المرجعية في PDF لكن الصفحة نفسها لا تحمل عنوانها:
headingStyles: [
{ id: 'appendix', numberingTemplate: 'Appendix {1:A}' },
{ id: 'silent', hidden: true, numbered: false },
],# Dedication {style="silent"}
For M., who read every draft.
# Method
…
# Survey instrument {style="appendix" startAt=1}
# Raw data {style="appendix"}مع numberingTemplate: '{1}.' في المستوى الأول تطبع الفصول 1. و2.…، والملاحق Appendix A وAppendix B. ويفتح الإهداء صفحته (بـbreakBefore الخاص بمستواه)، ولا يطبع إلا فقرته، ويظهر مع ذلك باسم Dedication في :::toc وفي ترويسات {chapterTitle} وفي مخطط PDF؛ أضف toc: false إلى النمط لإخراجه من المحتويات. واللوحة المصوّرة أو الخريطة المنضَّدة كعنوان H1 في منتصف فصل تحتاج عكس ما يحتاجه الإهداء: النمط ذو runningChapter: false (عادةً مع numbered: false وtoc: false) يُبقي الترويسات على الفصل الذي تقطعه.
const resolved = resolveHeadingStylesConfig(config.headingStyles, resolvedPage, resolvedBodyText, resolvedUnorderedLists, resolvedOrderedLists);
// => level overrides normalised, margins filled from the page, bodyStyle from the body
const minimal = stripHeadingStylesDefaults(config.headingStyles);
// => undefined when no style remains#جدول المحتويات
تضبط الخاصية toc ما يطبعه التوجيه :::toc (انظر صيغة المستند). تُجمَع المحتويات من مخطّط المستند (outline): كل عنوان برقمه وتسمية صفحته، وكل :::part؛ ولذلك تتبع الفصول: إن غيّرت اسم فصل، أو نقلته إلى جزء آخر، أو غيّرت مؤلفيه، تغيّرت المداخل معه. يتكوّن المدخل من رقم العنوان في عمود خاص به، ثم العنوان، ثم خط الإرشاد (leader) وتسمية الصفحة عند الحافة اليمنى، ثم سطر عنوان فرعي اختياري؛ أما الجزء فصفّ يصمّمه parts.design.
const config: PostextConfig = {
toc: {
levels: [{ level: 1, fontWeight: 700, color: { hex: '#00507b', model: 'hex' }, numberWidth: { value: 7.4, unit: 'mm' } }],
unnumbered: { color: { hex: '#000000', model: 'hex' } },
pageNumber: { fontWeight: 400, width: { value: 8, unit: 'mm' } },
leader: { char: '.', gap: { value: 1, unit: 'mm' } },
subtitle: { enabled: true, attr: 'author', italic: true, fontSize: { value: 8.5, unit: 'pt' } },
parts: {
height: { value: 23, unit: 'pt' },
marginTop: { value: 11.5, unit: 'pt' },
design: { elements: [/* a band box, 'SECTION {number}', '{titleText}', '{pageNumber}' */] },
},
},
};| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
levels | TocLevelConfig[] | المستوى 1 | مستويات العناوين المُدرَجة، ولكل منها تنضيد مدخله: fontFamily، fontSize، lineHeight (افتراضيًا تباعد أسطر المتن، فتستقر المحتويات على الشبكة)، fontWeight، italic، color، indent (للمدخل كله)، numberWidth / numberGap (عمود الرقم الذي يبدأ العنوان بعده؛ تُحاذى الأرقام فيه إلى اليمين، والرقم الأعرض من numberWidth، مثل الفصل الحادي عشر أو Chapter 12، يوسّع عمود مستواه إلى عرض أعرض الأرقام)، numberFontFamily، numberFontSize، numberFontWeight، numberColor، marginTop، marginBottom. الحقول غير المضبوطة ترث إعدادات نص المتن. يقع الرقم على خط أساس السطر الأول من العنوان، أيًّا كان محرفه وحجمه، على Canvas وفي HTML وفي PDF (حتى postext 1.4 كان الرقم يتوسّط ارتفاع الحرف x (x-height) مثل نقطة القائمة، فكان محرف العرض أو الحجم الأكبر يعلو فوق العنوان). يجد المُخرِج الذي تكتبه بنفسك خط الأساس هذا في bulletBaselineY من كتلة المدخل؛ وما زال bulletY منتصف مربّع em للرقم، كما في 1.4، فيرسم المُخرِج الأقدم من الحقل الجديد الأرقام حيث كان يرسمها دائمًا. |
unnumbered | TocEntryStyleConfig | — | قيم بديلة للعناوين التي يحمل نمطها numbered: false (كالتمهيد): لا تطبع رقمًا وتبدأ محاذية لإزاحة indent الخاصة بالمستوى. |
pageNumber | كائن | محرف المستوى 1، بوزن المتن | fontFamily، fontSize، fontWeight، italic، color لتسمية الصفحة، وwidth (افتراضيًا 2em): العمود المحجوز لها عند الحافة اليمنى، حيث تُحاذى إلى اليمين. |
leader | كائن | | يتكرّر char على امتداد الفراغ بين العنوان ورقم الصفحة، محاذى إلى اليمين لتصطفّ نقاط المداخل المتتالية ('. ' يباعد بينها)؛ وgap أقل مسافة تُترك بين العنوان وخط الإرشاد. يأخذ خط الإرشاد من المحارف ما يتّسع له المكان، مقيسًا سلسلةً كاملة بمحرفه، فالمحرف الذي يباعد بين النقاط المتتالية بتقنين الأزواج (kerning) ينال نقاطًا أقل بدل نقاط تصل إلى رقم الصفحة. (حتى postext 1.4 كان العدد يُحسب من نقطة واحدة، وفي محرف كهذا كان خط الإرشاد يمتد من العنوان إلى داخل الرقم.) والعنوان الذي لا يترك للتسمية مكانًا يلتفّ أبكر قليلًا. |
subtitle | كائن | | سطر ثانٍ تحت المدخل مأخوذ من سمة في العنوان (attr)، أي مؤلفي الفصل، وله fontFamily وfontSize وfontWeight وitalic (افتراضيًا true) وcolor خاصة به وإزاحة indent إضافية. يشارك السطر المدخل تباعد أسطره ولا ينفصل أبدًا عن عنوانه. |
parts.enabled | boolean | true | هل تنال فواصل الأجزاء صفًّا. |
parts.breakBefore | boolean | false | افتح صفحة جديدة قبل كل صف جزء ما عدا الأول، فتُدرَج فصول كل جزء في صفحة خاصة بها. |
parts.design | DesignSlot | فارغ | تصميم الصف؛ حاويته هي الصف (عرض العمود × height). العناصر النائبة: ، ، …، و (تسمية صفحة الجزء؛ ومع parts.page: false، الذي لا يفتح صفحة جزء، تسمية الصفحة التي يبدأ عليها محتوى الجزء، حيث تنتقل الترويسات إليه؛ وفي كتاب يُخرَج فصلًا فصلًا، يشير السياج الذي يُغلق فصله إلى أول صفحة محتوى في الفصل التالي). تأخذ الألوان المرتبطة بلوحة الألوان قيمَ palette الخاصة بالجزء، فيأتي صف كل قسم بلونه. وحين يكون التصميم فارغًا، يُنضَّد (وبينهما numberSeparator الخاص بعنوان H1) ورقم الصفحة بتنضيد مدخل المستوى 1. |
parts.height، marginTop، marginBottom | Dimension | 2em، 0، 0 | ارتفاع الصف والمسافة حوله. وحدة em هي حجم نص المتن، فارتفاع الصف الافتراضي ضعف حجم المتن، لا سطران من المتن: مع متن 9.5/13.5 pt يكون 19 pt. ولصفّ بارتفاع سطرين من المتن، أعطِ الارتفاع بوحدة pt (27pt في ذلك المثال). |
تسميات الصفحات هي التي يطبعها المستند. يعيد buildDocument() إخراج المستند الذي يحوي :::toc بتسميات الجولة السابقة حتى تستقر (ثلاث جولات إضافية على الأكثر)؛ أما المضيف الذي يُخرج كتابًا فصلًا فصلًا فيقدّم بدلًا من ذلك مخطّط الكتاب كله في PostextContent.outline، مجمَّعًا من contentOutline() (العناوين والأجزاء من النص وحده) وoutlineFromDoc() (المداخل نفسها بتسميات الصفحات في إخراجٍ ما)، ويعيد إخراج فصل المحتويات كلما تغيّر outlineKey() لذلك المخطّط. يحمل كل مدخل عنوانٍ الرقمَ number الذي تطبعه المحتويات، ويحمل العنوان المرقّم كذلك counter الخاص به: العدّ الجاري لمستواه بعد أي startAt، أيًّا كان ما يطبعه القالب، وهو ما يعرضه المضيف بجانب الفصل في قوائمه الخاصة.
#الفهرس الأبجدي
تضبط الخاصية index ما يطبعه التوجيه :::index (انظر صيغة المستند): المصطلحات المعلَّمة بـ :index[…] و:index{term="…"} في النص، مرتّبةً ومجمّعةً حسب الحرف الأول (وفي الصينية حسب الحرف الأول من قراءة pinyin أو حسب عدد الضربات، انظر groupBy)، ولكل منها الصفحات التي يقع فيها. يتكوّن المدخل من مصطلحه، ففاصل، فأرقام صفحاته؛ وتليه مداخله الفرعية، بخطوة إزاحة واحدة لكل مستوى، وتتدلّى أسطره الملتفّة بمقدار turnoverIndent فلا تصطفّ أبدًا مع مدخل فرعي.
const config: PostextConfig = {
headingStyles: [
// The index in two columns, under its own heading.
{ id: 'index', numbered: false, layout: { layoutType: 'double', gutterWidth: { value: 6, unit: 'mm' } } },
],
index: {
fontSize: { value: 8.5, unit: 'pt' },
lineHeight: { value: 11, unit: 'pt' },
rangeFormat: 'chicago',
groups: { fontFamily: 'Source Sans 3', fontWeight: 700, color: { hex: '#8a1c1c', model: 'hex' } },
},
};| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
fontFamily، fontSize، lineHeight، fontWeight، color | — | نص المتن | تنضيد المداخل. كل سطر في الفهرس، بما فيه رؤوس الحروف، يُنضَّد على lineHeight؛ للفهرس إيقاعه الخاص ولا يلتزم بشبكة خطوط الأساس. |
indent | Dimension | 1em | إزاحة كل مستوى من المداخل الفرعية. |
turnoverIndent | Dimension | 2em | إزاحة إضافية للأسطر الملتفّة من المدخل، فوق إزاحة مستواه. |
entrySpacing | Dimension | 0 | المسافة فوق كل مدخل رئيسي. |
separator، locatorSeparator، rangeSeparator | string | ', '، ', '، '–'؛ والأوّلان '، ' في الخط العربي | ما يُطبع بين المصطلح وأول صفحة له، وبين صفحتين، وبين طرفي النطاق. |
mergeRanges | boolean | true | اضمم الصفحات المتتالية ذات صيغة الترقيم الواحدة في نطاق: 12, 13, 14 تُطبع 12–14. لا تُضمّ الصفحات الرئيسية أبدًا. |
rangeFormat | 'full' | 'chicago' | 'full' | كيف يُكتب الرقم الثاني في النطاق: كاملًا (234–237)، أو بحذف الأرقام التي يشترك فيها مع الأول، كما يطلب The Chicago Manual of Style (9.64): 71–72، 100–104، 101–8، 321–28، 1496–500. التسميات الرومانية تُكتب كاملة دائمًا. |
main | | عريض | كيف تُنضَّد الصفحة الرئيسية (main على العلامة). |
see | | حسب اللغة، مائل (قائم في الخط العربي) | الكلمات التي تسبق الإحالة. إن لم تُضبط تتبع لغة المستند: See / See also، Véase / Véase también، Voir / Voir aussi، 见 / 另见 (見 / 另見 في الصينية التقليدية)… يضع الفهرس الصيني الإحالة بعد نقطة، بلا مسافة: 贾琏 12。见贾政. |
locale | string | لغة المستند | اللغة التي يُرتَّب بترتيبها الأبجدي المداخل (وسم BCP 47 يقرؤه Intl.Collator). في الإسبانية تأتي ñ بعد n وتتصدّر مجموعة خاصة بها؛ ولا تغيّر العلامات فوق الحروف (accents) الترتيب أبدًا. |
groupBy | 'auto' | 'letter' | 'pinyin' | 'stroke' | 'none' | 'auto' | ما تكونه رؤوس المجموعات. 'letter': الحرف الأول من مفتاح الترتيب. 'pinyin': المدخل الذي يبدأ بمقطع صيني (Han) يُصنَّف تحت الحرف اللاتيني الأول من قراءته بالـ pinyin (贾宝玉 تحت J)، ومفتاح الترتيب اللاتيني تحت حرفه، بعد المداخل الصينية لذلك الحرف (إذ يضع المرتِّب الحروف اللاتينية بعد المقاطع الصينية): sort="jia mu" يُختَم به حرف J. 'stroke': تحت عدد ضربات المقطع الأول، 一畫، 二畫… (一画… في الصينية المبسّطة). 'none': بلا رؤوس؛ تُفصَل الرموز والأرقام والكلمات بمسافة groups.marginTop وحدها. أما 'auto' فيجمّع الفهرس الصيني المبسّط (zh، zh-Hans، zh-CN) حسب pinyin، والتقليدي (zh-Hant، zh-TW، zh-HK) حسب الضربات، وكل لغة أخرى حسب الحرف. تُرتَّب المداخل بالترتيب الذي تأتي منه الرؤوس: فالفهرس zh-Hant المجمَّع حسب pinyin يُرتَّب حسب pinyin. القراءات وأعداد الضربات هي ما يعطيه المرتِّب (CLDR)؛ وحيث يقرأ مقطعًا قراءة خاطئة (重 بقراءة zhòng في 重阳، و行 بقراءة xíng في 行业)، أعطِ العلامة مفتاح sort بمقاطع ليس لها إلا القراءة التي تريدها، فيُرتَّب المدخل في موضعه: sort="崇阳" لـ 重阳، وsort="航业" لـ 行业. والمتصفح الذي لا يملك بيانات الترتيب الصيني يطبع فهرس pinyin أو فهرس الضربات بلا رؤوس. |
ignoreArticle | boolean | true في العربية | رتّب المداخل العربية وجمّعها كأن أداة التعريف ال (ٱل) في أولها غير موجودة: تُصنَّف البصرة تحت ب، بين بدر وبغداد، وتُطبع كما كُتبت. تحتفظ الله بأداة التعريف، والمدخل الذي له مفتاح sort خاص يُرتَّب بذلك المفتاح كما هو. وأيًّا كانت قيمة هذا الإعداد، يتجاهل الفهرس العربي الحركات والتطويل، ويصنّف أ إ آ ٱ تحت ا، ويرتّب ؤ كما يرتّب و، وئ وى كما يرتّب ي، وة كما يرتّب ه. |
groups.enabled | boolean | true | اطبع رأسًا فوق كل مجموعة من المداخل (A، B…، أو عدد الضربات، و0–9 للأرقام، وSymbols للبقية؛ و数字 / 數字 و符号 / 符號 في الصينية). |
groups.fontFamily، fontSize، fontWeight، italic، color | — | ما للمداخل، والوزن 700 | محرف رأس الحرف. يُنضَّد على خطوة أسطر المداخل. |
groups.marginTop | Dimension | سطر واحد من الفهرس | المسافة فوق كل مجموعة، برأس أو بدونه؛ ولا مسافة فوق المجموعة الأولى، التي تحدّد بُعدَها عن العنوان مسافةُ العنوان نفسه، ولا في أعلى العمود. |
groups.symbolsLabel، numbersLabel | string | حسب اللغة؛ '0–9'، و'数字' / '數字' في الصينية | رأسا المداخل التي تبدأ برمز والتي تبدأ برقم. |
تأتي أرقام الصفحات من مخطّط الكتاب، كما تأتي أرقام المحتويات: يُدرج computeOutline() وcontentOutline() علامات الفهرس في النص مداخلَ من النوع 'indexMark' (مع indexMark.path وsort وsee وseeAlso وmain وrange وindex)، ويعطي outlineFromDoc() كلًّا منها الصفحة التي وقعت عليها، وهي ما يسجّله البناء في doc.indexMarks ({ sourceStart, pageIndex } لكل علامة). يعيد buildDocument() إخراج المستند الذي يطبع فهرسه بنفسه حتى تستقر الأرقام؛ أما المضيف الذي يُخرج كتابًا فصلًا فصلًا فيسلّم الفصلَ الذي يحوي :::index مخطّطَ الكتاب كله في PostextContent.outline. يقسم tocOutline() وindexOutline() المخطّط إلى ما تقرؤه المحتويات وما يقرؤه الفهرس، ليتمكّن المضيف من ربط كل فصل بالجزء الذي يطبعه: فلا يعيد Sandbox إخراج فصل الفهرس إلا حين تتحرّك علامة، ولا فصل المحتويات إلا حين يتحرّك عنوان. ويخبر contentOutline() كذلك هل يطبع النص فهرسًا (hasIndex).
#الوحدات والألوان
#الأبعاد
كل القياسات الفيزيائية في Postext تستعمل النوع Dimension، وهو قيمة مقرونة بوحدة:
interface Dimension {
value: number;
unit: DimensionUnit; // 'cm' | 'mm' | 'in' | 'pt' | 'px' | 'em' | 'rem'
}الوحدات المطلقة، أي cm وmm وin وpt وpx، تُحوَّل إلى بكسلات باستعمال قيمة DPI المضبوطة. عند 300 DPI يساوي 1 cm نحو 118 px.
الوحدات النسبية، أي em وrem، تتغيّر مع حجم الخط الحالي. وحدة em نسبية إلى حجم خط العنصر نفسه؛ وrem نسبية إلى حجم خط نص المتن.
#الألوان
تُخزَّن الألوان بتمثيل ست عشري وبنموذج لوني مستهدف معًا:
interface ColorValue {
hex: string; // '#ff0000', 'transparent', etc.
model: ColorModel; // 'hex' | 'rgb' | 'cmyk' | 'hsl'
}يدلّ الحقل model على الفضاء اللوني المقصود. للعرض على الويب، القيمة المعتادة 'hex' أو 'rgb'. ولمسارات الطباعة، تحفظ 'cmyk' القصد بأن يُحدَّد اللون بنموذج CMYK عند التصدير إلى PDF.
لأن Postext يستهدف مخرجات بجودة النشر، يأتي لون نص المتن الافتراضي بـ model: 'cmyk' (#000000). أما ألوان العناوين والنص العريض والمائل والقوائم فقيمتها الافتراضية مرتبطة بلوحة الألوان: اللون الرئيسي (Main Color) (#295AA3، model: 'hex'). وخلفية الصفحة وطبقات الواجهة (شبكة خطوط الأساس، علامات القص، مؤشرات التصحيح) قيمتها الافتراضية model: 'hex'. غيّر color.model في أي حقل إن احتجت دلالة تصدير مختلفة.
#الشفافية
يمكن أن يكون اللون شفافًا جزئيًا. يقبل hex قناة ألفا، بصيغة #rgba أو #rrggbbaa. ويقبل كذلك لون rgb() / rgba()، بصيغة الفواصل أو صيغة المسافات، مع ألفا رقمًا أو نسبة مئوية. أما transparent فشفاف تمامًا:
const config: PostextConfig = {
header: {
elements: [{
kind: 'box',
id: 'veil',
placement: {
anchor: { to: 'bleed', edge: 'top-left' },
size: { width: 'fill', height: { value: 40, unit: 'mm' } },
},
style: { backgroundColor: { hex: '#ffffffb3', model: 'hex' } }, // white at 70 %
}],
},
bodyText: { color: { hex: 'rgba(0, 0, 0, 0.85)', model: 'rgb' } },
};ترسمه المُخرِجات الثلاثة بالطريقة نفسها. تأخذ Canvas وعارض HTML القيمة لونًا من ألوان CSS. ويضبط مُخرِج PDF عتامة اللون ألفا ثابتة، أي ExtGState فيه ca للتعبئة وCA للخطوط. ويشمل ذلك النص، والخطوط الفاصلة، والصناديق، وتعبئات الجداول وحدودها، والشارات، وعيّنات الألوان، والصيغ الرياضية. يتراكب اللون الشفاف جزئيًا فوق كل ما رُسم قبله. الصندوق في الترويسة أو التذييل يُرسم أخيرًا، فيحجب النص تحته؛ أما الصندوق في شريط صفحة الافتتاح فيُرسم أولًا، فيصبغ الصفحة تحت النص. وحين يُفرَض على PDF فضاء لوني آخر (pdfGeneration.forceColorSpace مع colorSpace: 'cmyk' أو 'grayscale')، يُحوَّل اللون وتُحفظ قناة ألفا. وفي Sandbox، يكتب منزلق العتامة في منتقي الألوان هذه القيم بصيغة #rrggbbaa، ويقرأ المنتقي الصيغ الأخرى أيضًا.
#الخطوط المخصّصة
يبحث Postext عن كل سلسلة fontFamily في فهرس Google Fonts وفي قائمة customFonts الخاصة بالمستند معًا. وعند تطابق الأسماء تتقدّم الخطوط المخصّصة: إن صرّحت بـ customFonts: [{ name: 'Roboto', … }]، يستعمل Postext الملف الذي رفعته بدل خط "Roboto" من Google Fonts.
استعمل الخطوط المخصّصة حين:
- يحتاج المستند محرفًا خاصًا بعلامة تجارية أو محرفًا مرخّصًا غير موجود في Google Fonts.
- لا تستطيع البيئة الوصول إلى شبكة توزيع Google Fonts (دون اتصال، أو شبكة داخلية، أو بيئة حساسة للخصوصية).
- يلزمك إبقاء ملف الخط خاصًا وعدم رفعه إلى طرف ثالث.
#مخطط الإعدادات
type CustomFontFormat = 'woff2' | 'woff' | 'ttf' | 'otf';
type CustomFontStyle = 'normal' | 'italic';
interface CustomFontVariant {
weight: number; // CSS font-weight, 100..900
style: CustomFontStyle;
fileId: string; // opaque id of the binary in out-of-band storage
format: CustomFontFormat;
fileName?: string; // original upload filename (optional, shown in UI)
}
interface CustomFontFamily {
name: string; // used anywhere a Google Font family name fits
variants: CustomFontVariant[];
}
interface PostextConfig {
// ...
customFonts?: CustomFontFamily[];
}ملف كل شكل من أشكال الخط لا يُضمَّن في الإعدادات نفسها. لا تحمل الإعدادات إلا مؤشرات fileId؛ أما البايتات فتعيش خارجها. في Sandbox يعني ذلك IndexedDB (مخزن مفاتيح وقيم، في المتصفح وحده، خاص بالمستند). ومن يدمج Postext في مضيف آخر حرّ في تحديد مصدر fileId كما يشاء: نقطة نهاية على خادم، أو ذاكرة تخزين لعامل خدمة (service worker)، أو أي شيء آخر، ما دامت البايتات تصل إلى الخيط الرئيسي قبل تشغيل buildDocument.
#إدارة الخطوط المخصّصة في Sandbox
افتح لوحة الخطوط من شريط النشاط الأيسر (بين الموارد والتصميم). تعرض قائمتها المحارف في هذا الكتاب كل عائلة يستعملها التصميم، مع دورها، وهل تأتي من Google Fonts أم من ملفك. وتحت ملفات خطوطك، لكل عائلة:
- أضف عائلة خط: تُنشئ عائلة فارغة؛ غيّر اسمها في مكانه.
- ارفع ملفًا: اختر وزنًا (100–900) وأسلوبًا (عادي / مائل)، ثم اختر ملفًا واحدًا أو عدة ملفات بصيغة
.woff2أو.woffأو.ttfأو.otf. يصبح كل ملف شكلًا مستقلًا مربوطًا بالزوج (الوزن، الأسلوب) المختار حاليًا؛ ويُحفظ اسم الملف المرفوع ويظهر في الصف لتميّز بين الأشكال. ويمكنك تعديل وزن الشكل أو أسلوبه من قوائمه المنسدلة في أي وقت. - الأشكال المكرّرة مسموح بها. إن وقع ملفان على الخانة نفسها (الوزن، الأسلوب)، يُحتفظ بالاثنين ويظهر تحذير شكل خط مكرّر لتعرف أن عليك التمييز بين إعدادات الملفات الزائدة.
- احذف الشكل أو احذف العائلة: يزيل المدخل من الإعدادات والبايتات المخزّنة من IndexedDB.
بعد التصريح بعائلة، يجمعها كل منتقي خطوط تحت مخصّصة، فوق قائمة Google Fonts. واختيارها يربط العائلة بكل حقل عائلة خط تطبّقها عليه.
#سلوك العرض
في الداخل:
- حين يتغيّر
customFonts، تُسجَّل كل عائلة مصرَّح بها تلقائيًا مداخلَFontFaceفيdocument.fonts، فيلتقط عارض HTML، وإطار عرض Canvas (الذي يقيس عبرdocument.fonts)، وأي إشارة CSS مباشرة، الخطَّ المخصّص دون أن يحتاج المستخدم إلى فتح منتقي الخطوط أولًا. - يتسلّم عامل الإخراج (layout worker) كائنات ArrayBuffer نفسها عبر مسار نقل حمولة الخطوط القائم، فيُنتج القياس (
buildFontString، pretext) مقاييس مطابقة لما يُنتجه مع Google Fonts. - تغيير شكل أو إزالته يُسقط الخط المخزَّن مؤقتًا لتلك العائلة في العامل ويعيد تسجيله في البناء التالي، فتبقى المعاينات متوافقة مع مجموعة الأشكال الحالية.
- تصدير PDF: تمرّ الملفات المرفوعة عبر مسار
PdfFontProviderنفسه. تُفكّ ضغط ملفات.woff2؛ وتمرّ.ttfو.otfكما هي. أما.woffفيُرفض برسالة خطأ واضحة (لا تستطيع pdf-lib تضمين WOFF الخام؛ أعد رفعه بصيغة.woff2/.ttf/.otf). ويُضمَّن OpenType بنكهة CFF (ملف.otfبالتوقيعOTTO) دون اقتطاع مجموعة فرعية، لأن مقتطِع CFF في pdf-lib يمرّ على كل محرف عندsave()وقد يتوقّف دقائق مع خطوط حقيقية؛ وتخطّي الاقتطاع يقايض ملف PDF أكبر قليلًا بأوقات إخراج ثابتة.
#تحذيرات الخطوط المفقودة
تُدرج لوحة الفحوص في Sandbox ثلاث حالات إخفاق خاصة بالخطوط المخصّصة ضمن مجموعتها الخطوط (وكلها يفعّلها المفتاح نفسه debug.warnings.missingFont الذي يتحكّم أصلًا في التحذير العام "لم يُحمَّل"):
- عائلة خط غير معروفة: يشير
fontFamilyإلى اسم ليس خطًا معروفًا من Google Fonts ولا عائلة مخصّصة مصرَّحًا بها حاليًا. ويظهر هذا التحذير فورًا كذلك حين تحذف عائلة مخصّصة ما زال حقلfontFamilyما يشير إليها، بدل انتظار أن يلاحظ DOM ذلك. - شكل خط مفقود: العائلة موجودة، لكن خانة واحدة على الأقل من خانات الوزن/الأسلوب القياسية (400 / 700، عادي / مائل) لا ملف مرفوعًا لها. ويُدرج التحذير التركيبات المفقودة بعينها.
- شكل خط مكرّر: ملفان مرفوعان أو أكثر يتشاركان الخانة نفسها (الوزن، الأسلوب) داخل عائلة واحدة. لا يُستعمل عند العرض إلا ملف واحد؛ ويدفعك التحذير إلى إعادة ضبط المداخل الباقية.
النقر على أي من هذه التحذيرات يفتح لوحة الخطوط لترفع الشكل المفقود، أو تعيد إضافة العائلة، أو تميّز بين المكرّرات.
#لوحة الألوان
تتيح لك الخاصية colorPalette في PostextConfig تعريف مجموعة قابلة لإعادة الاستعمال من الألوان المسمّاة والإشارة إليها من أي ColorValue في الإعدادات. هي مقابل خصائص CSS المخصّصة أو لوحة العيّنات (swatches) في InDesign في Postext: غيّر مدخل اللوحة مرة واحدة، فيتحدّث كل لون يشير إليه في المستند كله.
interface ColorPaletteEntry {
id: string; // stable identifier — referenced by ColorValue.paletteId
name: string; // human label shown in sandbox UIs
value: ColorValue;
}#لوحة الألوان الافتراضية
يأتي Postext بلوحة ألوان افتراضية من مدخل واحد اسمه اللون الرئيسي (Main Color) (id: 'main-color'، القيمة الست عشرية #295AA3). عدة قيم افتراضية، منها لون العناوين، ولون النص العريض/المائل في المتن، ولون :ref، وألوان النقاط وعلامات الترقيم، تشير إلى هذا المدخل عبر paletteId: 'main-color'، فتغيير هذه العيّنة الواحدة يعيد تلوين كل جزء من المستند يستعملها.
يمكنك فحص لوحة الألوان الافتراضية أو استنساخها أو المقارنة بها عبر ثلاثة تصديرات:
import {
DEFAULT_COLOR_PALETTE,
cloneDefaultColorPalette,
isDefaultColorPalette,
} from 'postext';
// Read-only snapshot of the shipped palette.
DEFAULT_COLOR_PALETTE;
// => [{ id: 'main-color', name: 'Main Color', value: { hex: '#295AA3', model: 'hex' } }]
// Independent copy — mutate this, not DEFAULT_COLOR_PALETTE.
const palette = cloneDefaultColorPalette();
// Detect whether a user has customised the palette at all.
isDefaultColorPalette(palette); // trueتقع لوحة الألوان في المستوى الأعلى من الإعدادات:
const config: PostextConfig = {
colorPalette: [
{ id: 'ink', name: 'Ink', value: { hex: '#0a0a0a', model: 'cmyk' } },
{ id: 'accent', name: 'Accent', value: { hex: '#b8860b', model: 'hex' } },
],
bodyText: { color: { hex: '#000000', model: 'cmyk', paletteId: 'ink' } },
headings: { color: { hex: '#000000', model: 'hex', paletteId: 'accent' } },
};#الإحالة إلى مدخل في لوحة الألوان
يمكن لأي ColorValue في الإعدادات أن يحمل حقلًا اختياريًا paletteId يشير إلى مدخل في colorPalette: خلفية الصفحة، وألوان نص المتن (ومنها لون :ref)، وألوان العناوين، والخطوط الفاصلة بين الأعمدة، وألوان القوائم، وألوان الجداول والتعليقات والشارات والأطر (الصندوق، والشريط، والأيقونة، والعلامة، والتسمية، والعنوان، والمتن)، وألوان علامات القص وشبكة خطوط الأساس، ومؤشرات التصحيح، وكل لون في أي تصميم: الترويسات، وصفحات افتتاح العناوين والتصاميم داخل العمود، وتصاميم أنماط العناوين وترويساتها، وصفحات الأجزاء وصفوف الأجزاء في المحتويات (النص، والخط الفاصل، وتعبئة الصندوق وحدّه، والمحيط، والحرف الاستهلالي). وحين يوجد هذا الحقل، تتقدّم قيمتا hex / model من مدخل اللوحة على القيمتين البديلتين hex / model المخزّنتين بجانبه. ولا تُستعمل القيمة البديلة المضمّنة إلا إذا كانت اللوحة مفقودة أو فارغة أو لا تحوي ذلك المعرّف، وهذا مفيد حين تشحن إعدادات ستقرؤها أداة لا تفهم لوحات الألوان.
تغيّر في postext 1.5. حتى postext 1.4 كانت لوحة الألوان لا تبلغ إلا قائمة ثابتة من الإعدادات: ألوان التصاميم (الترويسات، وصفحات الافتتاح، وأنماط العناوين، والأجزاء، وصفوف المحتويات)، وbodyText.referenceColor، وألوان تسميات الأطر، وألوان النص العريض / المائل في متون الأطر كانت تحتفظ بقيمة hex المخزّنة بجانب paletteId الخاص بها. والمستند الذي تختلف قيمته المخزّنة عن مدخل اللوحة (كل مستند عُدِّل في Sandbox بعد تغيّر المدخل، وكل :ref حين لا يكون اللون الرئيسي #295AA3) يطبع الآن لون اللوحة، كما يقتضي الربط. ولتُبقي لونًا كما كان، احذف paletteId الخاص به.
#كيف تُطبَّق لوحات الألوان
يشغّل buildDocument لوحة الألوان في موضعين، لتعمل الألوان المُحال إليها في القيم التي كتبتها صراحة وفي القيم الافتراضية التي تُملأ لاحقًا:
applyPaletteToConfig(config): يحلّ كلColorValueفي إعدادات المستخدم الخام يحملpaletteId. مفيد حين تريد فحص ما سيراه المحرّك فعلًا.applyPaletteToResolvedConfig(resolved, palette): يعمل بعد حلّ القيم الافتراضية ويعيد كتابة القيم الافتراضية المرتبطة باللوحة (لون العناوين، ولون النص العريض/المائل في المتن، ولون:ref، وألوان القوائم، وألوان التصاميم الافتراضية) لتطابق لوحة الألوان النشطة.
كلاهما يمرّ على الإعدادات كلها، فلا يبقى لون مرتبط باللوحة دون معالجة. معظم ألوان تدفّق النص (نص المتن، والعناوين، والقوائم، والجداول، والتعليقات، والشارات، وصندوق الإطار وعنوانه ومتنه) تخرج قيمًا عادية. وكل لون آخر (ألوان التصاميم، ولون :ref، وتسميات الأطر) يأخذ hex / model من اللوحة ويحتفظ بـ paletteId الخاص به. فهذا الربط هو ما تتجاوزه سمة palette في الجزء، وتجاوز palette في نمط العنوان، على صفحاتهما (انظر الأجزاء)، ولذلك يجب أن يبقى. أما htmlViewer.overrides فيُترك كما كُتب: يدمجه عارض HTML أولًا، ولوحة الألوان التي يحملها تنطبق حينئذ على كل شيء، ومنه التصاميم.
نادرًا ما تحتاج إلى استدعاء هاتين الدالتين بنفسك، لكن كلتيهما مُصدَّرة لتتمكّن من فحصهما أو إعادة استعمالهما:
import {
applyPaletteToConfig,
applyPaletteToResolvedConfig,
resolveColorValue,
} from 'postext';
const flat = applyPaletteToConfig(config);
// Every ColorValue with a paletteId in the raw config now carries the
// palette entry's hex/model (a design colour keeps its paletteId).
// `applyPaletteToResolvedConfig` is typically handled by buildDocument; use it
// directly if you build a ResolvedConfig yourself and want the palette applied.الدالة resolveColorValue(value, palette, fallback) هي النسخة الخاصة بقيمة واحدة، وهي مفيدة حين تركّب الإعدادات برمجيًا وتحتاج إلى حلّ لون واحد في كل مرة.
#تعديل لوحة الألوان
اللون الذي لا يسمّي paletteId الخاص به أي مدخل يطبع قيمتي hex / model المخزّنتين، وقد تكونان أقدم من اللون الذي أعطاه إياه المدخل. لذلك، قبل حذف مدخل، أعد كتابة كل ColorValue مرتبط به لونًا عاديًا يحمل القيمة الحالية للمدخل. يفعل قسم لوحة الألوان في Sandbox (التصميم › الألوان) ذلك حين تحذف مدخلًا، أينما كان اللون (ومنه التصاميم وتسميات الأطر)، ويُدرج مربع التأكيد فيه كل إعداد يستعمل المدخل: باسمه، أو بمساره في الإعدادات (header.elements[2].color).
#عارض HTML
تتحكّم الخاصية htmlViewer في الطريقة التي يرتّب بها مُخرِج HTML الصفحات على الشاشة. لا تنطبق إلا حين تعرض بـ renderToHtml / renderToHtmlIndexed؛ أما مسارا Canvas وPDF فيتجاهلانها تمامًا، إذ يستعملان page.width وpage.height وpage.dpi المضبوطة مباشرة.
interface HtmlViewerConfig {
maxCharsPerLine?: number; // Target column width, in characters of the body font.
columnGap?: number; // Horizontal gap between columns in multi-column mode (px).
optimalLineBreaking?: boolean; // Use Knuth–Plass inside the HTML viewer instead of greedy.
overrides?: HtmlViewerOverrides; // Screen-only partial config merged over the document config.
}
type HtmlViewerOverrides = Omit<PostextConfig, 'htmlViewer'>;| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
maxCharsPerLine | number | 70 | طول السطر المستهدف لكل عمود معروض، معبَّرًا عنه بعدد محارف خط المتن. يأخذ إطار العرض عيّنة نثرية تمثيلية بذلك الطول ليستخرج العرض الفعلي بالبكسل، فتتكيّف النتيجة مع أي تركيبة من خط متناسب وحجم خط. |
columnGap | number | 50 | الفاصل الأفقي، ببكسلات CSS، بين الأعمدة حين يكون العارض في وضع الأعمدة المتعددة. يُتجاهل في وضع العمود الواحد. |
optimalLineBreaking | boolean | false | فعّل تقسيم الأسطر بخوارزمية Knuth–Plass في عارض HTML. معطّل افتراضيًا لأن العارض يعيد الإخراج عند كل تغيير في الحجم أو في حجم الخط، وخوارزمية الملاءمة الأولى الجشعة سريعة بما يكفي لتبدو فورية. فعّله حين تريد التقسيمات المثلى نفسها التي يستعملها مُخرِج Canvas. |
overrides | HtmlViewerOverrides | — | إعدادات مستند جزئية لا تنطبق إلا على الشاشة. يدمجها عارض HTML فوق إعدادات المستند قبل الإخراج (applyHtmlViewerOverrides)؛ ويتجاهلها Canvas وPDF. تُدمج الكائنات تكراريًا؛ والمصفوفة levels (العناوين، القوائم، المحتويات) تُدمج مدخلًا مدخلًا حسب level؛ وكل مصفوفة أخرى، مثل elements في خانة تصميم، وcalloutStyles، وcolorPalette…، تحلّ محلّ المصفوفة الأصلية كاملة. الاستعمال المعتاد: صفحة افتتاح فصل بلا أشرطة الطباعة، أو صفحة جزء يلتفّ عنوانها بمحاذاة الرقم بدل عرض ثابت لصندوق القص. يحرّرها Sandbox بصيغة JSON. |
const config: PostextConfig = {
headings: { levels: [{ level: 1, span: 'page', breakBefore: { enabled: true } }] },
htmlViewer: {
// On screen, chapters flow on without the page-span opener.
overrides: { headings: { levels: [{ level: 1, span: 'column', breakBefore: { enabled: false } }] } },
},
};يتبع المُحلِّل والمُجرِّد النمط نفسه المتّبع في الأقسام الأخرى:
import {
DEFAULT_HTML_VIEWER_CONFIG,
resolveHtmlViewerConfig,
stripHtmlViewerDefaults,
} from 'postext';
const resolved = resolveHtmlViewerConfig(config.htmlViewer);
// => { maxCharsPerLine: 70, columnGap: 50, optimalLineBreaking: false }
const minimal = stripHtmlViewerDefaults(config.htmlViewer);
// => undefined when everything matches the defaultsانظر دمج عارض HTML أدناه لمثال كامل من البداية إلى النهاية.
#إنتاج PDF (الإعدادات)
تتحكّم الخاصية pdfGeneration في الطريقة التي يُصدر بها مُخرِج PDF المستند النهائي. تستهلك الحزمة postext-pdf هذه الإعدادات عند التصدير؛ ويتجاهلها عارضا Canvas وHTML.
يحملها buildDocument في VDT، باسم doc.config.pdfGeneration، ويأخذ renderToPdf كل إعداد من أول موضع يعطيه:
- خياراته الخاصة (
outlines،accessible،colorSpace)؛ - إعدادات
pdfGenerationفي أول مستند يعرضه (في الكتاب، تنطبق إعدادات الفصل الأول على الملف كله)؛ - القيم الافتراضية: الإشارات المرجعية والوسوم مفعّلة، والألوان بنموذج RGB.
لذلك يتبع renderToPdf(doc, { fontProvider }) الإعدادات، والخيار المُمرَّر إلى renderToPdf يتقدّم في ذلك الإعداد وحده. يمثّل forceColorSpace وcolorSpace معًا الخيار colorSpace: ينطبق colorSpace من الإعدادات ما دام forceColorSpace مفعّلًا، ويكون PDF بنموذج RGB حين يكون معطّلًا. كانت الإصدارات السابقة من postext-pdf لا تقرأ إلا الخيارات؛ أما الآن فالإعدادات التي تضبط pdfGeneration تغيّر PDF الذي ينتجه المستدعي الذي لا يمرّر أي خيارات.
type PdfColorSpace = 'rgb' | 'cmyk' | 'grayscale';
interface PdfGenerationConfig {
outlines?: boolean; // Emit PDF bookmarks from the heading tree.
forceColorSpace?: boolean; // Convert every colour to `colorSpace`.
colorSpace?: PdfColorSpace; // Target space used when `forceColorSpace` is true.
accessible?: boolean; // Tagged, PDF/UA-oriented output (structure tree, alt text, language).
}| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
outlines | boolean | true | أصدِر مخطّطات PDF (الإشارات المرجعية) من تسلسل العناوين، ليقفز القرّاء مباشرة إلى أي عنوان من الشريط الجانبي في عارض PDF. عطّله في المستندات التي لا معنى لشجرة عناوينها (مثل الملصقات ذات الصفحة الواحدة). |
forceColorSpace | boolean | false | حين تكون قيمته true، يُحوَّل كل لون في PDF المُنتَج إلى colorSpace عند التصدير. اتركه معطّلًا لملفات PDF الموجّهة إلى الشاشة أولًا حين تكون ألوان المدخلات في الفضاء المطلوب أصلًا؛ وفعّله لتضمن فضاءً لونيًا واحدًا مع مصادر مختلطة. |
colorSpace | 'rgb' | 'cmyk' | 'grayscale' | 'cmyk' | الفضاء اللوني المستهدف حين يكون forceColorSpace مفعّلًا. استعمل 'cmyk' للطباعة الأوفست، و'rgb' لملفات PDF المخصّصة للشاشة وحدها، و'grayscale' لبروفات الطباعة بالأبيض والأسود. لا أثر له حين تكون قيمة forceColorSpace هي false. |
accessible | boolean | true | أصدِر ملف PDF موسومًا وميسَّر الوصول موجّهًا إلى PDF/UA-1: شجرة بنية منطقية بترتيب القراءة (عناوين لا تتخطّى مستوى أبدًا، وفقرات، وقوائم، واقتباسات كتلية، وأطر، وجداول بخلايا رأس، وأشكال بنصها البديل وتعليقاتها، وصيغ رياضية، وإحالات قابلة للنقر بوصفها روابط، ومحتويات :::toc بوصفها TOC واحدًا فيه TOCI لكل صف: رقم الصف Lbl، وعنوانه وصفحته Reference يحمل الرابط)، وعنوان المستند ولغته (locale في المستوى الأعلى)، وتعريف PDF/UA في بيانات XMP الوصفية، وكل علامة زخرفية (خلفية الصفحة، والخطوط الفاصلة، وشبكة خطوط الأساس، والترويسات والتذييلات، وعلامات القص، ورؤوس الجداول المكرّرة، والعنوان المكرّر وعلامة الاستمرار في الإطار المقسوم) موسومة بوصفها عنصرًا زخرفيًا (artifact) لتتخطّاها قارئات الشاشة. والشكل الذي لا يملك altText يرجع إلى تعليقه، ثم إلى تسميته. ويُقرأ الشكل أو الجدول العائم مباشرة بعد النص الذي يستشهد به أولًا، أو النص الذي يسبق سطر ::resource الخاص به، ويُقرأ الصندوق العائم بعد النص الذي يسبق سياجه، حتى حين يوضع العنصر العائم في صفحة لاحقة؛ والقائمة أو المحتويات التي تستمر بعد عنصر عائم تبقى عنصرًا واحدًا. لا تعطّله إلا في النسخ الأصلية للطباعة حيث لا تُرغب البنية الإضافية. |
pdfGeneration: {
outlines: true,
accessible: true,
forceColorSpace: true,
colorSpace: 'cmyk',
}يطابق المُحلِّل والمُجرِّد ما في الأقسام الأخرى:
import {
DEFAULT_PDF_GENERATION_CONFIG,
resolvePdfGenerationConfig,
stripPdfGenerationDefaults,
} from 'postext';
const resolved = resolvePdfGenerationConfig(config.pdfGeneration);
// => { outlines: true, forceColorSpace: false, colorSpace: 'cmyk', accessible: true }
const minimal = stripPdfGenerationDefaults(config.pdfGeneration);
// => undefined when everything matches the defaultsانظر إنتاج ملفات PDF أدناه لوصفة التصدير الكاملة.
#عارض Folio (الإعدادات)
تضبط الخاصية folio طريقة عرض عارض Folio (postext-folio) للكتاب المطبوع بالأبعاد الثلاثة: زاوية النظر، والورق، والتجليد، والسطح الذي يستقر عليه الكتاب، والإضاءة. يتجاهلها الإخراج، وكذلك مخرجات Canvas وHTML وPDF. يحمل buildDocument الإعدادات المحلولة في VDT باسم doc.config.folio حين تضبط الإعدادات أيًّا منها، فيحتفظ المستند الذي لا يضبطها ببصمة إخراجه.
interface FolioConfig {
tilt?: number; // Degrees from straight above, 0–70.
yaw?: number; // Degrees round the book, −180–180.
paper?: {
type?: 'uncoated' | 'bookWove' | 'coatedMatte' | 'coatedSilk' | 'coatedGloss'
| 'bible' | 'newsprint' | 'cardStock' | 'board';
grammage?: number; // g/m²
bulk?: number; // cm³/g; caliper µm = grammage × bulk
finish?: 'auto' | 'uncoated' | 'matte' | 'silk' | 'gloss';
texture?: 'auto' | 'smooth' | 'vellum' | 'wove' | 'laid' | 'linen' | 'felt';
textureStrength?: number; // 0–2
shade?: ColorValue;
showThrough?: boolean;
};
binding?: {
type?: 'hardcover' | 'paperback' | 'sewn' | 'layflat' | 'saddleStitch';
cover?: 'case' | 'pages';
coverMaterial?: 'auto' | 'cloth' | 'paper' | 'leather';
coverColor?: ColorValue;
spineImage?: string; // resource id
};
surface?: {
type?: 'oak' | 'walnut' | 'linen' | 'felt' | 'leather' | 'marble' | 'plain' | 'none';
color?: ColorValue;
};
lighting?: {
environment?: 'studio' | 'daylight' | 'lamp' | 'overcast' | 'night';
intensity?: number; // 0.25–2
shadows?: boolean;
};
}| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
tilt | number | 22 | زاوية ميل النظر عن الاتجاه العمودي من الأعلى، بالدرجات، محصورة في 0–70. عند 0 يُرى الكتاب المفتوح مسطّحًا من فوق؛ والزاوية الأكبر تقرّب ذيل الصفحات وتُظهر سُمك كتلة الكتاب. |
yaw | number | 0 | مقدار دوران النظر حول الكتاب، بالدرجات، مردودًا إلى المدى −180–180. عند 0 يُرى الكتاب من جهة ذيل صفحاته؛ والزاوية الموجبة تدير العين إلى يمينه، والسالبة إلى يساره. وهو مع tilt المنظر الذي يُفتح عليه العارض، والذي يعود إليه resetView() بتدرّج. |
paper.type | FolioPaperType | 'uncoated' | نوع الورق. يزوّد الحقول الخمسة التالية بقيمها الافتراضية (انظر جدول أنواع الورق). cardStock ورق مقوّى للأغلفة؛ وboard لوح صلب، كما في كتب الأطفال الكرتونية، وتنقلب أوراقه دون أن تنثني. |
paper.grammage | number | قيمة نوع الورق | الوزن بالغرام في المتر المربع، 20–2500. الورق الأثقل أسمك وأصلب وأكثر عتامة: تنثني الورقة في منحنى أوسع ويظهر منها قدر أقل من وجهها الخلفي. |
paper.bulk | number | قيمة نوع الورق | السُّمك لكل وحدة وزن، بوحدة cm³/g، 0.5–3. سُمك الورقة بالميكرومتر يساوي الوزن × معامل السُّمك، ومنه ومن عدد الصفحات ينتج سُمك كتلة الكتاب. |
paper.finish | FolioPaperFinish | 'auto' | غير مطلي (ألياف، بلا لمعان)، أو مطلي ومصقول بالأسطوانات ليكون مطفأً، أو حريريًا (لمعان خفيف)، أو لامعًا. 'auto' يأخذ قيمة نوع الورق. |
paper.texture | FolioPaperTexture | 'auto' | بروز السطح: smooth (مصقول بالأسطوانات)، vellum (خشونة دقيقة)، wove (النسيج المنتظم لمعظم ورق الكتب، المتشكّل على شبكة سلكية منسوجة)، laid (خطوط متقاربة تقطعها خطوط سلسلة أعرض، تتركها أسطوانة التعليم)، linen (نسيج متقاطع بارز)، felt (آثار لبّاد غير منتظمة). 'auto' يأخذ قيمة نوع الورق. |
paper.textureStrength | number | 1 | مقدار ظهور النسيج في الضوء، 0–2. |
paper.shade | ColorValue | قيمة نوع الورق | لون الورق قبل الطباعة (أبيض، طبيعي، كريمي). تُطبع الصفحات عليه. |
paper.showThrough | boolean | true | يظهر الوجه الخلفي للصفحة باهتًا عبر الورق الرقيق. |
binding.type | FolioBindingType | 'hardcover' | hardcover: تجليد بغلاف صلب، ألواحه أكبر قليلًا من الصفحات. paperback: تجليد بالغراء (تُفرز الكعوب وتُلصق)، ينفتح أقل استواءً. sewn: غلاف ليّن بملازم مخيطة. layflat: ينفتح مستويًا، بلا انخفاض عند الفاصل الداخلي. saddleStitch: أوراق مطوية مدبّسة عبر الطيّة، كالمجلة أو الكتيّب؛ بلا كعب مسطّح. |
binding.cover | FolioCoverSource | 'case' | الغلافان. 'case' يرسم غلافًا حول الصفحات. 'pages' يأخذ الصفحة الأولى من الكتاب لوحًا أماميًا، وصفحته الأخيرة، حين تكون صفحة زوجية، لوحًا خلفيًا: يبقى الكتاب مغلقًا حتى يُقلَب الغلاف، وتنقلب الألواح صلبة (في التدبيس عبر الطيّة يكون الغلاف ورقة أثقل قليلًا من الصفحات وينقلب كما تنقلب)، ولا يُرسم غلاف خارجي. |
binding.coverMaterial | FolioCoverMaterial | 'auto' | 'auto' قماش في الغلاف الصلب، وورق مقوّى ('paper') في أنواع التجليد الأخرى. |
binding.coverColor | ColorValue | أزرق داكن (#2c3e57) | لون مادة الغلاف. |
binding.spineImage | string | لا شيء | معرّف مورد نقطي أو SVG يُطبع على الكعب: الكعب كما يُرى والكتاب قائم، رأسه إلى الأعلى والغلاف الأمامي إلى اليمين. يُوائَم ليغطي الكعب، في الوسط. يُتجاهل في التدبيس عبر الطيّة. |
surface.type | FolioSurfaceType | 'oak' | ما يستقر عليه الكتاب. 'none' يترك خلفية المضيف. |
surface.color | ColorValue | لا شيء | يصبغ السطح؛ وفي 'plain' هو لون السطح. |
lighting.environment | FolioEnvironment | 'studio' | المحيط الذي ينعكس على الورق المطلي واللامع، مقرونًا بالضوء الرئيسي الذي يُلقي الظلال. |
lighting.intensity | number | 1 | شدة الإضاءة (التعريض)، 0.25–2. |
lighting.shadows | boolean | true | الظلال التي يُلقيها الضوء الرئيسي. |
أنواع الورق والقيم التي يزوّدها كل منها (FOLIO_PAPER_STOCKS)، وهي قيم نموذجية من نشرات بيانات المصانع:
| نوع الورق | الوزن | معامل السُّمك | السُّمك | التشطيب | النسيج | اللون |
|---|---|---|---|---|---|---|
uncoated (أوفست خالٍ من لبّ الخشب) | 90 g/m² | 1.25 | 113 µm | غير مطلي | wove | #fcfbf8 |
bookWove (كريمي، عالي السُّمك) | 80 g/m² | 1.6 | 128 µm | غير مطلي | wove | #f6efdc |
coatedMatte | 115 g/m² | 1.0 | 115 µm | مطفأ | smooth | #fdfdfc |
coatedSilk | 115 g/m² | 0.9 | 104 µm | حريري | smooth | #ffffff |
coatedGloss | 115 g/m² | 0.8 | 92 µm | لامع | smooth | #ffffff |
bible | 40 g/m² | 1.1 | 44 µm | غير مطلي | vellum | #f9f6ee |
newsprint | 48 g/m² | 1.5 | 72 µm | غير مطلي | wove | #ebe7dc |
cardStock | 250 g/m² | 1.2 | 300 µm | غير مطلي | vellum | #fbfaf6 |
board | 1250 g/m² | 1.6 | 2000 µm | حريري | smooth | #ffffff |
رواية على ورق كتب كريمي، مجلّدة بغلاف ليّن، على مكتب من خشب الجوز تحت مصباح قراءة:
folio: {
paper: { type: 'bookWove' },
binding: { type: 'paperback', coverColor: { hex: '#8a2b1f', model: 'hex' } },
surface: { type: 'walnut' },
lighting: { environment: 'lamp' },
}تتبع الألوان روابط لوحة الألوان (paletteId) مثل أي لون آخر في الإعدادات. ويطابق المُحلِّل والمُجرِّد ما في الأقسام الأخرى؛ ويحذف المُجرِّد قيم الورق المساوية لقيم نوع الورق المختار:
import { FOLIO_PAPER_STOCKS, DEFAULT_FOLIO_CONFIG, resolveFolioConfig, stripFolioDefaults } from 'postext';
resolveFolioConfig({ paper: { type: 'bible' } }).paper;
// => { type: 'bible', grammage: 40, bulk: 1.1, finish: 'uncoated', texture: 'vellum', textureStrength: 1, shade: { hex: '#f9f6ee', … }, showThrough: true }
stripFolioDefaults({ paper: { type: 'bible', grammage: 40 } });
// => { paper: { type: 'bible' } }في Sandbox، هذه الإعدادات هي مجموعة Folio في لوحة التصميم (التصميم › Folio › عارض Folio (ثلاثي الأبعاد))، ويعرضها تبويب Folio وأنت تغيّرها، دون إعادة إخراج الكتاب. انظر كتاب ثلاثي الأبعاد للعارض نفسه، وصيغة المستند › :::paper لسلسلة صفحات على نوع ورق آخر.
#التصحيح
تجمع الخاصية debug نوعين من أدوات المساعدة في التأليف: طبقات مرئية تُبقي النص المصدري والإخراج المعروض متوافقين، ومجموعة تحذيرات تكشف المشكلات الطباعية أو البنيوية في لوحة الفحوص في Sandbox. ولا يؤثر أيّ منهما في المخرجات المصدَّرة.
| الخاصية | النوع | الوصف |
|---|---|---|
cursorSync | SyncIndicatorConfig | مؤشر كتابة منعكس في الإخراج المعروض؛ انظر الطبقات المرئية. |
selectionSync | SyncIndicatorConfig | التحديد في المصدر مُبرَزًا على الصفحة؛ انظر الطبقات المرئية. |
looseLineHighlight | LooseLineHighlightConfig | طبقة فوق الأسطر المضبوطة المتخلخلة؛ انظر الطبقات المرئية. |
pageNegative | | صورة سالبة عالية التباين للصفحة؛ انظر الطبقات المرئية. |
warnings | WarningsToggleConfig | قيمة منطقية لكل نوع من تحذيرات التأليف المعروضة في المحرّر؛ انظر التحذيرات. |
#الطبقات المرئية
| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
cursorSync.enabled | boolean | true | يُظهر مؤشر كتابة في الإخراج المعروض يعكس موضع المؤشر في المصدر. |
cursorSync.color | ColorValue | #2563eb | لون ذلك المؤشر. |
selectionSync.enabled | boolean | true | يُبرز المدى المعروض المطابق للتحديد في المصدر. |
selectionSync.color | ColorValue | #fde04780 | لون الإبراز، وهو أصفر شفاف جزئيًا افتراضيًا. |
looseLineHighlight.enabled | boolean | false | يرسم طبقة فوق الأسطر المضبوطة التي يتجاوز تباعد كلماتها threshold ضعفًا من عرض المسافة العادية. |
looseLineHighlight.color | ColorValue | #ff000040 | لون تلك الطبقة. |
looseLineHighlight.threshold | number | 3 | مضاعف عرض المسافة العادية الذي يُعدّ السطر المضبوط فوقه متخلخلًا. ويستعمل التحذير looseLines العتبة نفسها. السطر المضبوط الذي تتمدّد مسافاته إلى أكثر من 3× يُنضَّد غير مضبوط، فلا تكاد الطبقة والتحذير يجدان شيئًا في النص الجاري عند القيمة الافتراضية؛ اخفضها (1.5 أو 2) لترى الأسطر المتخلخلة التي ما زالت مضبوطة. |
pageNegative.enabled | boolean | false | يرسم طبقة سالبة عالية التباين فوق الصفحة، وهي مفيدة لتدقيق الشكل العام للصفحتين المتقابلتين بصريًا (كثافة النص، وتوازن الأعمدة، والبياض) بنظرة واحدة، دون أن تشتّتك تفاصيل الحروف. |
كل SyncIndicatorConfig هو { enabled: boolean; color?: ColorValue }. وLooseLineHighlightConfig هو { enabled: boolean; color?: ColorValue; threshold?: number }. أما pageNegative فمفتاح تبديل بسيط { enabled: boolean }.
debug: {
cursorSync: { enabled: true, color: { hex: '#ff0066', model: 'hex' } },
selectionSync: { enabled: false, color: { hex: '#fde04780', model: 'hex' } },
looseLineHighlight: { enabled: true, color: { hex: '#ff000040', model: 'hex' }, threshold: 3 },
pageNegative: { enabled: true },
}يرسم Sandbox هذه الطبقات فوق معاينة Canvas الخاصة به. وهي ليست جزءًا من الصفحة: لا يرسمها renderPage ولا مخرجات HTML ولا PDF أبدًا.
#الأسطر المتخلخلة في Canvas خاص بك
يصدّر المحرّك إبراز الأسطر المتخلخلة (loose lines) في دالتين مساعدتين، للصفحة التي ترسمها بنفسك:
import { buildDocument, renderPageToCanvas, drawLooseLines, findLooseLines } from 'postext';
const doc = buildDocument(content, config);
const canvas = document.querySelector('canvas')!;
renderPageToCanvas(doc.pages[0], doc, canvas, { scale: 0.5 });
drawLooseLines(canvas.getContext('2d')!, doc.pages[0], doc, { threshold: 2.5 });
// The same lines as data: a report, an SVG overlay, a count per page.
for (const { ratio, line, block } of findLooseLines(doc, { threshold: 2.5 })) {
console.log(`page ${block.pageIndex + 1}: ${ratio.toFixed(2)}× — ${line.text}`);
}findLooseLines(doc, { threshold?, pageIndex? })يعيد كل سطر مضبوط تتجاوز قيمةjustifiedSpaceRatioفيهthreshold، بترتيب القراءة:{ block, line, ratio, x, y, width, height }. المستطيل، ببكسلات الصفحة، هو الشريط الذي يغطيه الإبراز: عرض الكتلة كاملًا على امتداد السطر. وهذه هي الأسطر التي يُبرزها Sandbox ويُبلغ عنها بوصفهاlooseLineفي لوحة الفحوص.drawLooseLines(ctx, page, doc, { threshold?, color? })يملأ تلك الأشرطة في صفحة واحدة ويعيد الأسطر التي رسمها. يرسم ببكسلات الصفحة تحت التحويل الحالي للسياق، فاستدعه مباشرة بعدrenderPageأوrenderPageToCanvasعلى اللوحة نفسها: كلاهما يترك السياق مُحجَّمًا على مقاس الصفحة. وcolorأي نمط تعبئة يقبله canvas.- القيم الافتراضية. تستعمل الدالتان العتبة الافتراضية (3) واللون الافتراضي (
#ff000040)، لاdebug.looseLineHighlightالخاص بالمستند: فذلك الإعداد يخص Sandbox. ولتتبع الإعدادات، مرّرresolveDebugConfig(config.debug).looseLineHighlight.thresholdو.color.hex.
#التحذيرات
يتحكّم debug.warnings في مشكلات التأليف التي تظهر في لوحة الفحوص في Sandbox (وتُحرَّر في التصميم › متقدّم › التحذيرات). كل مفتاح مفتاح تبديل منطقي مستقل؛ اضبط أحدها على false لإسكات ذلك التحذير بعينه دون تعطيل البقية.
هذه المفاتيح لا تصفّي إلا لوحة Sandbox. أما التحذيرات التي يسجّلها المحرّك نفسه، أي الصناديق التي تفيض عن عمودها في doc.warnings، ومعرّفات الموارد والتوجيهات والمضمَّنات ومعرّفات الأنماط غير المعروفة وشبكات الجداول غير المنتظمة في doc.contentWarnings، فموجودة أيًّا كانت قيم المفاتيح، وتُبلغ المُخرِجات عن الصور التي ترسمها عناصرَ نائبة؛ انظر التحذيرات في المستند.
interface WarningsToggleConfig {
missingFont?: boolean;
looseLines?: boolean;
headingHierarchy?: boolean;
consecutiveHeadings?: boolean;
listAfterHeading?: boolean;
designIssues?: boolean;
}| الخاصية | النوع | القيمة الافتراضية | الوصف |
|---|---|---|---|
missingFont | boolean | true | أبلغ حين يفشل تحميل خط تشير إليه الإعدادات في المتصفح. يكشف الأخطاء الإملائية في fontFamily وحزم @fontsource/... المفقودة مبكرًا، قبل أن تظهر في المخرجات بديلًا صامتًا بخط احتياطي. |
looseLines | boolean | true | أبلغ عن الأسطر المضبوطة التي يتجاوز تباعد كلماتها debug.looseLineHighlight.threshold. يعمل مع الطبقة: يعدّدها التحذير في اللوحة، وتُظهرها الطبقة في مكانها. |
headingHierarchy | boolean | true | أبلغ عن مستويات العناوين التي تتخطّى رتبة، مثل H1 يليه H3 مباشرة. تدلّ الفجوات البنيوية في العناوين عادة على خطأ في عمق العنوان أو على سوء فهم لمخطّط المستند. |
consecutiveHeadings | boolean | false | أبلغ حين يلي عنوانًا عنوانٌ آخر مباشرة دون فقرة أو قائمة بينهما. معطّل افتراضيًا لأن العناوين المتراصّة مشروعة في كثير من القوالب (عنوان + عنوان فرعي، فصل + تصدير)؛ فعّله في المخطوطات التي يُفترض أن يقدّم فيها كل عنوان نثرًا. |
listAfterHeading | boolean | false | أبلغ حين تبدأ قائمة مباشرة بعد عنوان دون فقرة تمهيدية. معطّل افتراضيًا لأن المواد المرجعية تفعل ذلك كثيرًا؛ فعّله في الكتابة السردية حيث ينبغي أن يؤطّر النثر كل قائمة. |
designIssues | boolean | true | أبلغ عن مشكلات السلامة في خانات التصميم: ترويسات الصفحات، والتذييلات، وصفحة افتتاح الجزء والصفحة الزوجية الفارغة بعدها، وصفوف الأجزاء في المحتويات، وخانات التصميم المتقدّم للعناوين، وتصميم كل نمط عنوان وترويسات قسمه. يشمل سلاسل الإرساء الدائرية والإحالات إلى نقاط إرساء غير موجودة (عنصر مُرسى إلى #id لم يعد موجودًا)، والعنوان الممتد على الصفحة الذي عُطّل فيه breakBefore، والتصميم المتقدّم المفعّل الذي لا ترسم عناصره أبدًا. |
debug: {
warnings: {
missingFont: true,
looseLines: true,
headingHierarchy: true,
consecutiveHeadings: true,
listAfterHeading: false,
designIssues: true,
},
}إلى جانب هذه، تُدرج اللوحة دائمًا التحذيرات التي يُطلقها الإخراج نفسه (VDTDocument.warnings)، مثل الإطار الذي يفيض عن عموده (calloutOverflow)، وقيم الإعدادات التي استبدلها المحرّك (VDTDocument.configWarnings، أو collectConfigWarnings(config)؛ انظر تحذيرات الإعدادات أدناه).
#تحذيرات الإعدادات
ستة أخطاء في الإعدادات نفسها لا تمرّ أبدًا بصمت، ولا يخفيها أي مفتاح تبديل. لا يتعطّل المحرّك عند أيّ منها؛ بل يستبدل قيمة، أو يُسقط الإعداد، ويُعلن ذلك:
- صيغة ترقيم غير معروفة: قيمة
numberFormatلقائمة مرقّمة، أوpage.pageNumbering.format، أوcounterFormatلنوع مورد، ليست أيًّا من تهجئات صيغ الترقيم. يُرقَّم بالأرقام العشرية. - قائمة خطوط في عائلة خط: قيمة
fontFamily(أو أي…FontFamily) تحوي مكدّس خطوط CSS. يُنضَّد النص بالعائلة الأولى في المكدّس (انظر عائلة واحدة لكلfontFamily). - عمود جانبي بلا مساحة: قيمة
sideColumnPercentلتخطيط'oneAndHalf'(للمستند، أو لـlayoutخاص بنمط عنوان) تترك أحد العمودين دون 1% من عرض المحتوى، أو ليست رقمًا. يُقطَع العمودان عند أقرب قيمة يحتملها كلاهما، ويسمّيهاused(sideColumnPercentClamped؛ انظر تخطيط'oneAndHalf'). - شبكة الحروف أكبر من اللازم:
cjk.gridبعدد محارف في السطر أو أسطر في الصفحة أكبر مما تتّسع له الهوامش. تُضبط الشبكة بأكبر عدد يتّسع، ويسمّيusedذلك العدد (cjkGridClamped؛ انظر شبكة الحروف). - إعداد عنوان غير معروف: مفتاح لا يوجد في
headings، أوheadings.balancing، أو مستوى عنوان، أو نمط عنوان، أو نمط فقرة: مثلletterSpacngمكتوبًا خطأً، أوtrackingمستعارًا من أداة أخرى، أوlevelفي نمط عنوان، أوfontStyle: 'italic'في نمط فقرة (الذي يأخذitalic: true). يتجاهله المحرّك (وحتى postext 1.4 كان يفعل ذلك دون أي إشعار).valueهو المفتاح، وusedفارغ، وsuggestionيسمّي الإعداد الأقرب إليه، حين يبعد عنه حرفًا أو حرفين أو لا يختلف عنه إلا في حالة الأحرف (unknownConfigKey). - قيمة إعداد غير معروفة: إعداد يأخذ كلمة من بضع كلمات يحمل كلمة أخرى، مثل
direction: 'right'(الذي يأخذautoأوltrأوrtl). يقرأ المحرّك القيمة الافتراضية بدلًا منها، ويسمّيusedما آلت إليه: فيdirection، اتجاه لغة المستند (unknownConfigValue).
يُدرجها Sandbox في لوحة الفحوص مع مسار الإعداد. وفي الشيفرة، يضعها buildDocument على المستند باسم configWarnings (وتغيب حين تكون الإعدادات سليمة)، ويعيدها collectConfigWarnings(config) دون إخراج أي شيء:
import { buildDocument, collectConfigWarnings } from 'postext';
// Plain JavaScript: in TypeScript, 'roman' does not type-check to begin with.
const config = { bodyText: { fontFamily: 'EB Garamond, serif' }, orderedLists: { numberFormat: 'roman' } };
const doc = buildDocument({ markdown }, config);
doc.configWarnings;
// [{ kind: 'fontFamilyStack', path: 'bodyText.fontFamily', value: 'EB Garamond, serif', used: 'EB Garamond' },
// { kind: 'unknownNumberFormat', path: 'orderedLists.numberFormat', value: 'roman', used: 'arabic' }]
collectConfigWarnings(config); // the same listوتُفحَص كذلك كل إعدادات جزئية متداخلة: أنماط العناوين، والقوائم داخل الأجزاء، وhtmlViewer.overrides، وعناصر التصميم.
يصفها formatWarning (انظر التحذيرات في المستند) كذلك، مع مسار الإعداد أولًا: bodyText.fontFamily: font stack "EB Garamond, serif" — set in "EB Garamond"، headingStyles[0].letterSpacng: unknown setting "letterSpacng" — ignored (did you mean "letterSpacing"?)، ليتمكّن المضيف من تسجيل القوائم الثلاث التي يعيدها البناء في حلقة واحدة.
#الاستخدام البرمجي
المسار الموصى به: استخدم عامل الويب (Web Worker). في المتصفح، ينبغي أن تُشغّل الغالبية العظمى من عمليات التكامل خطَّ معالجة الإخراج (pipeline) عبر
createLayoutWorker()منpostext/worker، لا باستدعاءbuildDocumentمباشرة على الخيط الرئيسي (main thread). يُبقي العامل واجهة المستخدم سريعة الاستجابة أثناء البناء، ويحفظ قياسات النص في ذاكرة مؤقتة عبر عمليات إعادة البناء التدريجية، ويُفعّل الإلغاء بقاعدة «الأحدث يفوز» (last-wins)، فتُلغي كل ضغطة مفتاح جديدة أي بناء قديم لا يزال قيد التنفيذ. انتقل مباشرة إلى تشغيل الإخراج في Web Worker لتجد الطريقة المعتمدة. كل ما يرد في بقية هذا القسم (استدعاءbuildDocumentمباشرة، ودوال الاستكمال، ودوال الحذف، والذاكرات المؤقتة) يظل مفيدًا، فالعامل يقبل المدخلات نفسها تمامًا ويُعيد المخرجات نفسها، لكن غلاف العامل هو نقطة البداية الصحيحة لشيفرة واجهة المستخدم. لا تلجأ إلى استدعاءbuildDocumentعلى الخيط الرئيسي إلا للتصدير الذي يجري مرة واحدة، أو للعرض على جانب الخادم (Node)، أو للاختبارات.
#بناء مستند
تُشغّل الدالة buildDocument خطّ معالجة الإخراج كاملًا وتُعيد شجرة المستند الافتراضية (Virtual Document Tree، VDT) بإحداثيات دقيقة لكل عنصر. هذه هي نقطة الدخول الأدنى مستوى؛ وعلى شيفرة واجهة المستخدم أن تُفضّل غلاف Web Worker، الذي يستدعي buildDocument داخل خيط عامل مخصّص بالمعاملات نفسها.
import { buildDocument } from 'postext';
const content = {
markdown: '# Chapter One\n\nThe story begins here...',
};
const config = {
page: { sizePreset: '17x24' },
layout: { layoutType: 'double' },
bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 9, unit: 'pt' } }, // 9 pt overrides the 8 pt default
};
// Build the layout — produces a VDT with one entry per page in `vdt.pages`
const vdt = buildDocument(content, config);
console.log(`Document has ${vdt.pages.length} pages`);#التحذيرات في المستند
لا تتوقف buildDocument عند مرجع خاطئ أو نمط مجهول: بل تضع قيمة بديلة وتسجّل ما فعلته في doc.contentWarnings. أما الصناديق التي اضطر الإخراج إلى فرضها فتجدها في doc.warnings، التي تحتفظ بالشكل الذي كان لها في postext 1.4: كل مدخل فيها من نوع calloutOverflow ومعه pageIndex وcolumnIndex وoverflowPx. يغيب كل حقل حين لا يكون هناك ما يُبلَّغ عنه. لكل مدخل kind. وتحمل أنواع المحتوى نطاق المصدر للبنية المعنية، أي sourceStart / sourceEnd، وهما إزاحتان داخل نص markdown الذي مرّرته، بما في ذلك البيانات التمهيدية (frontmatter)، ومعهما pageIndex إن وقعت البنية في صفحة.
| النوع | متى يُطلَق | ما الذي يفعله الإخراج |
|---|---|---|
calloutOverflow | صندوق :::callout لا يتسع له أي عمود ولا يمكن لأي قطع أن يقسمه. | يُوضع على أي حال، فيتجاوز عموده بمقدار overflowPx (في pageIndex / columnIndex). وهو النوع الوحيد المُدرج في doc.warnings؛ أما الأنواع التالية فتقع في doc.contentWarnings. |
unknownResourceId | تضمين ::resource (usage: 'embed') أو :ref سطري ('ref') أو صورة في خلية جدول ('cellImage') يذكر معرّفًا لا يحمله أي مورد. | يُحذف التضمين، ويطبع المرجع ? (أو تسميته في text=) بلا رقم ولا رابط، وتبقى الخلية نصًا فقط. يذكر inResource المورد الذي يحوي تعليقُه أو ملاحظته أو خليته ذلك المرجع. |
unknownDirective | سطر :::name ليس اسمه توجيهًا ولا حاوية. | يُنضَّد السطر نصًا. |
malformedEmbed | سطر ::name ليس تضمينًا سليم البنية قائمًا وحده: ::resource بمعرّف غير محاط بعلامات تنصيص أو محاط بعلامات تنصيص مفردة أو بسمة أخرى، أو سطر ملتصق تحت فقرة دون سطر فارغ. | يُنضَّد السطر نصًا. |
fullwidthMarkup | سطر يحوي ترميزًا كُتب بطريقة إدخال صينية أو يابانية: سياج :::، أو عنوان #، أو علامة حاشية [^…]، أو سمات {…} بعد سياج أو عنوان، أو خط عريض **…**. يحمل typed الترميز كما كُتب، وascii الشكل الذي ينبغي كتابته. تحذير واحد لكل سطر. | يُنضَّد السطر نصًا؛ ولا يُحوَّل شيء. |
attributeKeyInvalid | مفتاح سمة يحوي حروفًا من خارج ASCII (作者=曹雪芹)؛ ويشير التحذير إلى المفتاح. | تُتجاهَل السمة. |
unknownParagraphStyle | :::paragraphsstyle يذكر نمط فقرة غير موجود. | تُنضَّد الفقرات نصًا أساسيًا. |
unknownCalloutType | :::callouttype لا يذكر أيًا من calloutStyles، ولا يُطلَق إلا بعد ضبط بعضها. | يأخذ الصندوق أول نمط إطار. |
unknownChipStyle | :chip[…]style يذكر نمط شارة غير موجود. | تأخذ الشارة أول نمط شارة. |
undefinedFootnote | علامة حاشية [^id] لا تعرّفها أي فقرة [^id]: (id هو معرّف الحاشية). | يُطبع الرقم؛ وتبقى الحاشية فارغة. |
unusedFootnote | تعريف حاشية [^id]: لا تستشهد به أي علامة. | لا تُنضَّد الحاشية. |
indexMarkInvalid | علامة فهرسة بلا مصطلح: :index، أو سمات بلا term على علامة ليس لها نص بين قوسين معقوفين. | لا تُفهرس العلامة شيئًا. |
indexSeeUnknown | هدف see أو seealso (target) ليس مدخلًا في فهرسه (index، و'' للفهرس الرئيسي). يشير إلى سطر :::index. | تُطبع الإحالة على أي حال. |
indexRangeUnclosed | علامة range="start" بلا range="end" مقابلة، أو العكس (يبيّن missing أي الطرفين مفقود؛ ويذكر term المدخل). يشير إلى سطر :::index. | يطبع النطاق صفحته الوحيدة. |
unknownHeadingStyle | عنوان يذكر في style="…" نمط عنوان غير موجود (level هو مستوى العنوان). | يحتفظ العنوان وقسمه بإعدادات المستوى نفسه. |
unknownTableStyle | مورد جدول يذكر في table.styleId اسمًا ليس مدخلًا في tableStyles. | يُنضَّد الجدول بـtableStyle. |
raggedTableGrid | شبكة جدول ليست مستطيلة بعد احتساب دمج خلاياها (انظر بناء نماذج الجداول). | تنزاح الخلايا فوق خلية مدموجة أو تترك فراغًا. يحدّد reason ('spanOverlap' / 'missingCells') وrow وcol موضع المشكلة الأولى؛ ويبيّن count عدد المشكلات. |
التحذيرات المتعلقة بمورد ما، سواء نمط جدوله أو شبكته أو مرجع داخل تعليقه أو ملاحظته أو خلاياه، تشير إلى أول تضمين لذلك المورد أو أول مرجع إليه في النص، ويُدرج كل منها مرة واحدة لكل مورد. ولا تُفحص إلا الموارد التي يستخدمها المستند: ففصل من كتاب يُبلّغ عن الجداول التي يستشهد بها، لا عن كل جدول في الكتاب.
import { buildDocument, formatWarning } from 'postext';
const doc = buildDocument({ markdown: 'See :ref{id="fig-map"}.\n\n:::sidebar\nNotes.' }, config);
for (const w of [...(doc.warnings ?? []), ...(doc.contentWarnings ?? []), ...(doc.configWarnings ?? [])]) console.warn(formatWarning(w));
// Unknown resource id "fig-map" in :ref — it prints "?" (or its text= label), with no number or link (page 1, offset 4)
// Unknown directive ":::sidebar" — the line is set as text (page 1, offset 25)
// Narrow on `kind` to read the fields of a kind.
const missing = (doc.contentWarnings ?? []).flatMap((w) => (w.kind === 'unknownResourceId' ? [w.resourceId] : []));تُعيد formatWarning(w) وصفًا بالإنجليزية في سطر واحد. أما المضيف الذي يترجم رسائله فيتفرّع بحسب kind بدلًا من ذلك، ويحتفظ بفرع افتراضي، لأن الإصدارات الفرعية قد تضيف أنواعًا. وتُعيد collectContentWarnings(markdown, config, resources) تحذيرات المحتوى دون إخراج أي شيء (القائمة التي يضيفها البناء، دون pageIndex)، لمحرّر يفحص النص أثناء الكتابة. وتفحص collectHeadingDesignCuts(doc) إخراجًا مكتملًا بحثًا عن تصاميم عناوين يتجاوز نصها أسفل صفحتها أو عمودها (kind: 'headingDesignCut'؛ انظر الارتفاع المحجوز)، وهو ما لا يُبلّغ عنه الإخراج نفسه، وتصف formatWarning نتائجها كذلك. وتعرضها لوحة الفحوص في Sandbox جميعًا.
تُبلّغ المُخرِجات (renderers) عمّا لا تستطيع رسمه كما طُلب منها عبر خيار onWarning: renderPageToCanvas وrenderPage وrenderToCanvas (RenderPageOptions)، وrenderToHtml وrenderToHtmlIndexed (RenderHtmlOptions)، وrenderToPdf (RenderToPdfOptions، وعبر عامل PDF أيضًا). يوجد اليوم نوع واحد من تحذيرات العرض هو missingImage: الصورة التي لا يوجد ما يُرسم لها، سواء أكانت شكلًا أم صورة في خلية جدول أم أيقونة إطار أم صورة تصميم، تُرسم عنصرًا نائبًا محايدًا ويُبلَّغ عنها مرة واحدة لكل fileId ولكل استدعاء عرض، ومعها pageIndex، وresourceId حين يعرفه الراسم (للأشكال وصور الخلايا)، وdocumentIndex في PDF عند عرض عدة مستندات. ويعني «لا يوجد ما يُرسم» غياب registerResourceImage للمعرّف fileId في Canvas، وغياب عنوان URL من resourceImageUrl في HTML، وغياب البايتات من resourceBytes في PDF، أو وجود بايتات يتعذّر فكّ ترميزها. أما المورد النقطي أو SVG الذي لا يذكر أي fileId فليس لديه ما يطلبه: فيُرسم عنصرًا نائبًا دون تبليغ. ولا تُخزَّن تحذيرات العرض في VDT، لأن ما يستطيع المضيف توفيره يتغيّر بعد الإخراج.
import { buildDocument, renderPage, type RenderWarning, type Resource } from 'postext';
const map: Resource = {
id: 'fig-map', typeId: 'figure', kind: 'bitmap', caption: 'The route.', createdAt: 0, updatedAt: 0,
bitmap: { fileId: 'map-file', format: 'png', width: 1200, height: 800 },
};
const doc = buildDocument({ markdown: 'See :ref{id="fig-map"}.', resources: [map] }, config);
const warnings: RenderWarning[] = [];
const canvas = renderPage(doc.pages[0], doc, { onWarning: (w) => warnings.push(w) });
// Until 'map-file' is registered with registerResourceImage:
// [{ kind: 'missingImage', fileId: 'map-file', resourceId: 'fig-map', pageIndex: 0 }]#رسم صفحة في صورة نقطية
يمكن تحويل كل صفحة إلى صورة نقطية (bitmap) على حدة. استخدم renderPage(page, doc) للحصول على HTMLCanvasElement لرقم صفحة معيّن، وCanvas صورة نقطية مقاسها يطابق تمامًا أبعاد الصفحة بالبكسل (بدقة DPI المضبوطة)، فيمكنك عرضها أو تصديرها أو تمريرها إلى أي سلسلة لمعالجة الصور:
import { buildDocument, renderPage } from 'postext';
const vdt = buildDocument(content, config);
// Render page 3 (zero-indexed) to a bitmap canvas
const pageNumber = 2;
const page = vdt.pages[pageNumber];
if (!page) throw new Error(`Page ${pageNumber} does not exist`);
const canvas = renderPage(page, vdt);
// canvas.width / canvas.height are the page bitmap size in pixels
// Show it in the DOM
document.body.appendChild(canvas);
// …or export it as a PNG data URL
const pngDataUrl = canvas.toDataURL('image/png');
// …or get a Blob for download / upload
canvas.toBlob((blob) => {
if (blob) saveAs(blob, `page-${pageNumber + 1}.png`);
}, 'image/png');
// …or grab raw RGBA pixels
const ctx = canvas.getContext('2d')!;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);إذا كنت تفضّل الرسم في لوحة تملكها مسبقًا (مثلًا لوحة مُلحقة بـ DOM ضمن تخطيط معيّن)، فاستخدم renderPageToCanvas(page, doc, canvas)، فهي تغيّر مقاس اللوحة التي تمرّرها وترسم فيها، بدلًا من إنشاء لوحة جديدة.
لرسم كل الصفحات، كرّر على vdt.pages:
const bitmaps = vdt.pages.map((page) => renderPage(page, vdt));مثال حيّ: صفحة في صورة
كل ما سبق يعمل في المتصفح. يستورد المثال على CodePen أحدث إصدار من postext من شبكة توصيل المحتوى (CDN)، وينتظر خطوط الويب، ويُخرج مستندًا قصيرًا من عمودين، ويرسم صفحته الأولى في لوحة، ويتيح تلك الصورة النقطية بصيغة PNG. اضغط شغّل على CodePen لتحميل المحرّر وتغيير نص markdown أو الإعدادات؛ وتُعاد الصفحة رسمها مع كل تعديل.
import { buildDocument, renderPage } from 'https://esm.sh/postext';
const markdown = `# The Lantern
The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.
## Two columns
Postext lays this text out in **two columns**, breaking each paragraph with the *Knuth–Plass* algorithm and hyphenating with TeX patterns. Widows and orphans are avoided, and the columns are balanced on the last page.
The light it gave was small, but it was enough to find the step. The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.
The light it gave was small, but it was enough to find the step. The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.`;
const config = {
// 150 dpi: crisp enough for a preview, light enough to paint instantly.
page: { sizePreset: '17x24', dpi: 150 },
layout: { layoutType: 'double' },
bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 10, unit: 'pt' } },
};
// Postext measures text with the fonts the browser has loaded,
// so wait for every face the document uses before laying it out.
await Promise.all([
document.fonts.load('16px "EB Garamond"'),
document.fonts.load('bold 16px "EB Garamond"'),
document.fonts.load('italic 16px "EB Garamond"'),
document.fonts.load('bold 16px "Open Sans"'), // the default heading face
]);
// The whole layout: one entry per page in doc.pages, with exact coordinates.
const doc = buildDocument({ markdown }, config);
// Rasterise the first page. The canvas is sized to the page at the configured dpi.
const canvas = renderPage(doc.pages[0], doc);
document.getElementById('page').replaceChildren(canvas);
document.getElementById('status').textContent =
`${doc.pages.length} page(s) · page 1 is ${canvas.width} × ${canvas.height} px`;
// The same bitmap as a PNG file.
canvas.toBlob((blob) => {
const link = document.getElementById('download');
link.href = URL.createObjectURL(blob);
link.hidden = false;
}, 'image/png');index.html
<p id="status">Laying out…</p>
<a id="download" download="page-1.png" hidden>Download page 1 as PNG</a>
<div id="page"></div>style.css
body {
margin: 16px;
font-family: system-ui, sans-serif;
background: #e8e8e8;
}
#page canvas {
display: block;
max-width: 100%;
height: auto;
margin-top: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}يحمّل محررًا تفاعليًا من codepen.io. يستورد المثال أحدث إصدار من postext عبر شبكة توصيل المحتوى (CDN).
#React
يصدّر postext/react الدالة createLayout(content, config?): وهي مكوّن يُخرج المستند مرة واحدة عند تركيبه، ويعرض كل صفحة على هيئة <canvas> داخل <div>.
import { createLayout } from 'postext/react';
const Article = createLayout(
{ markdown: '# Hello\n\nThe first paragraph of the article.' },
{ page: { sizePreset: '17x24' } },
);
export function ArticlePage() {
return <Article className="pages" style={{ maxWidth: 480 }} />;
}- على الخيط الرئيسي، مرة واحدة. تُرسم الصفحات بدقة المستند وتُحجَّم إلى عرض الحاوية. يُثبَّت
contentوconfigعند استدعاءcreateLayout؛ فأنشئ مكوّنًا آخر لعرض شيء مختلف. وللمعاينة الحيّة، ابنِ في Web Worker وارسم باستخدامrenderPageToCanvas، كما في مثال React هناك. - الخطوط والصور أولًا. حمّل خطوط الويب الخاصة بالمستند قبل تركيب المكوّن، وسجّل صوره باستخدام
registerResourceImage. وحين يحوي نص markdown الرمز$، يشغّل المكوّن محرّك الرياضيات بنفسه. - يبقى React خارج نقطة الدخول الرئيسية. لا يستورد
postextمكتبة React أبدًا؛ وحدهpostext/reactيفعل. ولا تزالcreateLayoutمُصدَّرة منpostextكي تستمر الشيفرة الحالية في العمل، لكنها مُهمَلة (deprecated): فهي تحمّلpostext/reactعند استدعائها، ويُعلَّق المكوّن (suspend) حتى يصل (ثم يعيد React عرضه من تلقاء نفسه). استوردها منpostext/react. - المكوّن المُهمَل يُعلَّق. إلى أن يصل
postext/react، تحتاجcreateLayoutالمستوردة منpostextإلى جذر متزامن (createRoot) أو حدّ<Suspense>فوقها. وفي جذرReactDOM.renderالقديم، أو فيrenderToString، دون حدّ، يُبلّغ React عن خطأ بدلًا من ذلك. ويبقىreactتبعية نظيرة (peer dependency) إلزامية، كي تستطيع أدوات التجميع حلّ ذلك الاستيراد الكسول.
#استكمال القيم الافتراضية
تملأ دوال الاستكمال (resolvers) القيم الافتراضية في كائنات الإعدادات الجزئية. ويفيد ذلك حين تحتاج إلى إعدادات كاملة لفحصها أو مقارنتها:
import { resolvePageConfig, resolveBodyTextConfig } from 'postext';
const fullPage = resolvePageConfig({ sizePreset: '21x28' });
// => { sizePreset: '21x28', width: { value: 21, unit: 'cm' }, height: { value: 28, unit: 'cm' },
// margins: { top: { value: 2, unit: 'cm' }, ... }, dpi: 300, cutLines: { enabled: false, ... }, ... }
const fullBody = resolveBodyTextConfig({ fontFamily: 'Inter' });
// => { fontFamily: 'Inter', fontSize: { value: 8, unit: 'pt' }, lineHeight: { value: 1.5, unit: 'em' }, ... }دوال الاستكمال المتاحة، واحدة لكل قسم من المستوى الأعلى: resolvePageConfig، resolveLayoutConfig، resolveBodyTextConfig، resolveHeadingsConfig، resolveHeadingStylesConfig، resolveTocConfig، resolvePartsConfig، resolveUnorderedListsConfig، resolveOrderedListsConfig، resolveMathConfig، resolveTableStyleConfig، resolveCaptionStyleConfig، resolveDiagramStyleConfig، resolveParagraphStylesConfig، resolveCalloutStylesConfig، resolveHeaderFooterConfig، resolveDebugConfig، resolveHtmlViewerConfig، resolvePdfGenerationConfig، إضافة إلى resolveDesignSlot لخانة تصميم واحدة. أما لوحات الألوان فتُطبَّق على حدة عبر applyPaletteToConfig(config) وapplyPaletteToResolvedConfig(resolved, palette) وresolveColorValue(value, palette, fallback)؛ انظر لوحة الألوان.
دوال الاستكمال التي تتسلسل قيمها الافتراضية من قسم آخر تأخذ ذلك القسم، مستكمَلًا، معاملًا إضافيًا. فتأخذ resolveUnorderedListsConfig وresolveOrderedListsConfig النص الأساسي المستكمَل، لأن القيم الافتراضية للقوائم في fontFamily وcolor تتسلسل منه؛ وتأخذ resolveCalloutStylesConfig النص الأساسي والعناوين والقوائم غير المرقّمة مستكمَلة (انظر المثال في أنماط الإطارات)، وتأخذ resolveHeadingStylesConfig الصفحة والنص الأساسي وقسمَي القوائم مستكمَلة. راجع تصريحات الأنواع في الحزمة لمعرفة التوقيع الدقيق لكل منها:
import { resolveBodyTextConfig, resolveUnorderedListsConfig } from 'postext';
const body = resolveBodyTextConfig({ fontFamily: 'Inter' });
const lists = resolveUnorderedListsConfig({ bulletChar: '—' }, body);
// => lists.fontFamily === 'Inter' (inherited)وتُصدَّر أيضًا مجموعات القيم الافتراضية الثابتة، أي القيم المستخدمة حين لا يدخل أي تسلسل في الحساب: DEFAULT_PAGE_CONFIG، DEFAULT_CUT_LINES، DEFAULT_PAGE_NUMBERING، PAGE_SIZE_PRESETS، DEFAULT_LAYOUT_CONFIG، DEFAULT_COLUMN_RULE، DEFAULT_COLUMN_BALANCING، DEFAULT_BODY_TEXT_CONFIG، DEFAULT_HYPHENATION_CONFIG، DEFAULT_HEADINGS_CONFIG، DEFAULT_UNORDERED_LISTS_STATIC، DEFAULT_ORDERED_LISTS_STATIC، DEFAULT_PARAGRAPH_STYLES، DEFAULT_CALLOUT_STYLES، DEFAULT_CALLOUT_STYLE_STATIC، DEFAULT_PARTS_CONFIG، DEFAULT_HEADING_STYLES، DEFAULT_TOC_CONFIG، DEFAULT_MATH_CONFIG، DEFAULT_DIAGRAM_STYLE_CONFIG، DEFAULT_DEBUG_CONFIG، DEFAULT_HTML_VIEWER_CONFIG، DEFAULT_PDF_GENERATION_CONFIG، DEFAULT_COLOR_PALETTE، DEFAULT_MAIN_COLOR، DEFAULT_MAIN_COLOR_ID، DEFAULT_MAIN_COLOR_NAME، DEFAULT_MAIN_COLOR_HEX، إضافة إلى القيم الافتراضية لعناصر الترويسة والتذييل (DEFAULT_HEADER_FOOTER_SLOT، DEFAULT_HEADER_SLOT، DEFAULT_FOOTER_SLOT، DEFAULT_TEXT_ELEMENT، DEFAULT_RULE_ELEMENT، DEFAULT_BOX_ELEMENT) والدالة defaultResourceTypes(locale) التي تراعي اللغة (انظر أنواع الموارد).
#حذف القيم الافتراضية
عند حفظ الإعدادات (في localStorage أو في ملف مثلًا)، استخدم stripConfigDefaults لإزالة القيم المطابقة للقيم الافتراضية. فتبقى الإعدادات المخزَّنة في حدّها الأدنى، ولا يُحفظ إلا ما غيّرته عن قصد:
import { stripConfigDefaults } from 'postext';
const minimal = stripConfigDefaults(fullConfig);
// Only properties that differ from defaults remainتتوفر أيضًا دوال حذف (strippers) منفردة، واحدة لكل دالة استكمال: stripPageDefaults، stripLayoutDefaults، stripBodyTextDefaults، stripHeadingsDefaults، stripHeadingStylesDefaults، stripTocDefaults، stripPartsDefaults، stripUnorderedListsDefaults، stripOrderedListsDefaults، stripMathDefaults، stripTableStyleDefaults، stripCaptionStyleDefaults، stripDiagramStyleDefaults، stripParagraphStylesDefaults، stripCalloutStylesDefaults، stripHeaderFooterDefaults، stripDesignSlotDefaults، stripDebugDefaults، stripHtmlViewerDefaults، stripPdfGenerationDefaults.
#التحليل
يتيح المحرّك مُقطِّع markdown الخاص به وقارئ البيانات التمهيدية. استخدمهما لفحص مستند قبل بنائه، أو لتغذية أدوات أخرى ببنية الكتل نفسها التي يراها Postext:
import { parseMarkdown, extractFrontmatter } from 'postext';
const source = '---\ntitle: Chapter One\n---\n\n# Opening\n\nThe story begins here.';
const { metadata, content } = extractFrontmatter(source);
// metadata.title === 'Chapter One'
const blocks = parseMarkdown(content);
// => [ { type: 'heading', level: 1, text: 'Opening', … },
// { type: 'paragraph', text: 'The story begins here.', … } ]راجع صفحة صيغة المستند للاطلاع على القائمة الكاملة لبنى markdown التي يتعرّف عليها Postext.
#ذاكرة القياس المؤقتة
قياس النص هو الخطوة المكلفة في الإخراج. يحافظ نوعان من الذاكرة المؤقتة على انخفاض كلفتها، ويُفرَّغ كل منهما بطريقة مختلفة:
- ذاكرة كتل تملكها أنت. تُعيد
createMeasurementCache()كائنMeasurementCacheيتذكّر كل فقرة قيست، مفهرسةً بنصها وخطوطها وعرضها وخيارات تقسيم الأسطر وقاموس تقسيم الكلمات بالواصلة النشط. مرّره معاملًا ثالثًا إلىbuildDocument(أوbuildDocumentAsync) لإعادة استخدام القياسات عبر جولات التقارب وعبر عمليات البناء: فالمحرّر الذي يُخرج المستند مع كل ضغطة مفتاح لا يقيس حينئذ إلا الفقرات التي تغيّرت. ومن دونها، تقيس كل جولة كل كتلة من جديد. والفقرة المقروءة من الذاكرة المؤقتة مطابقة لفقرة قيست من جديد، فالبناء الذي يستخدم ذاكرة مؤقتة ينضّد كل سطر كما ينضّده بناء من دونها؛ وفي postext 1.4.1 كانت الفقرة المخزّنة تفقد علامة السطر الأخير ذي الكلمة المعزولة، فكان تضييق الكلمة المعزولة وموازنة الأعمدة قد يقسمانها تقسيمًا مختلفًا. ويحتفظ عامل الإخراج بذاكرة واحدة عبر عمليات البناء ويستبدلها حين تتغيّر الخطوط. - ذاكرات العرض العامة. تُخزَّن عروض الكلمات لكل سلسلة خط في حالة على مستوى الوحدة (module) تشترك فيها كل عمليات البناء في الصفحة، وللمكتبة pretext ذاكرتها المؤقتة الخاصة. وتصبح هذه الذاكرات قديمة حين يصل خط ويب بعد أن قيس النص بخط بديل.
import { buildDocument, createMeasurementCache, clearMeasurementCache } from 'postext';
import type { MeasurementCache } from 'postext';
let cache: MeasurementCache = createMeasurementCache();
let doc = buildDocument(content, config, cache);
// A web font finished loading: widths measured with the fallback are stale.
await document.fonts.ready;
clearMeasurementCache(); // no argument: clears the global width caches
cache = createMeasurementCache(); // a block cache has no clear(); start a new one
doc = buildDocument(content, config, cache);لا تأخذ clearMeasurementCache() أي معامل ولا تمسّ MeasurementCache: فكتلها قيست بالعروض القديمة أيضًا، لذا تخلّص منها وأنشئ واحدة جديدة. أما إعادة البناء بعد تحميل خط دون تفريغ الذاكرة فتعطي تقسيم الأسطر نفسه المقيس بالخط البديل.
وللتطبيقات التي تقيس النص قطعة قطعة، تأخذ cachedMeasureBlock(text, font, maxWidthPx, lineHeightPx, options, cache) وcachedMeasureRichBlock(spans, normalFont, boldFont, italicFont, boldItalicFont, maxWidthPx, lineHeightPx, options, cache) معاملات measureBlock وmeasureRichBlock إضافة إلى الذاكرة المؤقتة في آخرها.
#الحالة العامة المشتركة في صفحة واحدة
يعيش جزء من حالة Postext في متغيّرات على مستوى الوحدة. ويشترك فيها كل ما يستورد postext ضمن نطاق JavaScript نفسه (realm): فالصفحة ونصوصها البرمجية تتشارك نسخة واحدة، بينما لكل إطار iframe ولكل عامل نسخته الخاصة. ولا يلاحظ ذلك من يضع مستندًا واحدًا في الصفحة. أما من يضع عدة مستندات في صفحة واحدة، كمعاينتين حيّتين أو معرض أمثلة، فيلاحظه:
- صور الموارد. تملأ
registerResourceImage(fileId, image)سجلًا واحدًا مفهرسًا بـfileId، تقرؤهrenderPageوrenderPageToCanvas. فإذا سجّل مستندان كلاهماfigure.svgتشاركا ذلك المدخل: ويفوز آخر تسجيل، لكليهما. أعطِ معرّفات الملفات بادئة خاصة بكل مستند، واستدعِunregisterResourceImage(fileId)أوclearResourceImages()حين يزول مستند. والصور النقطية التي يخزّنها مُخرِج Canvas مفهرسة بالطريقة نفسها وتُحذف مع الصورة. - قياسات النص. تُخزَّن العروض المقيسة لكل سلسلة خط ونص على مستوى النطاق كله. والنص الذي قيس قبل اكتمال تحميل خط ويب يحتفظ بعروض الخط البديل، في كل مستند، حتى تفرّغ
clearMeasurementCache()(بلا معاملات) الذاكرات: استدعها بعد تحميل الخطوط، ثم ابنِ من جديد. - الإعدادات المستكمَلة. يُستكمل كل كائن إعدادات مرة واحدة وتُخزَّن النتيجة مقرونة بذلك الكائن. فالإعدادات التي تُعدَّل في مكانها ثم يُعاد بناؤها تُخرَج بإعداداتها القديمة: مرّر كائنًا جديدًا مع كل تغيير (
{ ...config, … }أوstructuredClone(config)). - لغة تقسيم الكلمات. يضبط كل بناء لغة تقسيم الكلمات بالواصلة على مستوى العملية كلها لتكون
bodyText.hyphenation.localeالخاصة بمستنده. وتستخدمhyphenateText(text)المُصدَّرة وlayoutDesignSlotلغة آخر بناء ما لم تمرّر لغة: استدعِhyphenateText(text, 'es'). - محرّك الرياضيات. هناك محرّك MathJax واحد وذاكرة مؤقتة واحدة للصيغ المرسومة في النطاق كله؛ وتشغّله
initMathEngine()للجميع.
أبسط طريقة للعزل نطاقٌ لكل مستند: إطار iframe لكل مثال حيّ (وتضمين CodePen واحد منها)، أو عامل إخراج لكل مستند للقياسات وتقسيم الكلمات (أما الصور فلا تزال تُسجَّل في الصفحة).
#تشغيل الإخراج في Web Worker
هذه هي الطريقة الموصى بها لاستخدام Postext في المتصفح. إذا كنت تبني أي شيء تفاعلي، كمعاينة حيّة أو محرّر أو عارض يستجيب لتغيّر المقاس أو ساحة تجريب على غرار Sandbox، فشغّل خطّ المعالجة عبر createLayoutWorker() من postext/worker. لا تستدعِ buildDocument مباشرة على الخيط الرئيسي في شيفرة واجهة المستخدم.
استدعاء buildDocument على الخيط الرئيسي يُشغّل خطّ المعالجة كاملًا، من تحليل وقياس وسبع جولات وما يصل إلى خمس دورات تقارب، على الخيط الذي استدعاه أيًّا كان. وهذا مقبول للتصدير الذي يجري مرة واحدة. أما في واجهة تفاعلية فهو الخيط الخطأ: فإخراج يستغرق 150 ms يحجب أحداث الإدخال، فتتراكم ضغطات المفاتيح ويتقطّع التمرير. والعامل ينقل كل واحدة من تلك الأجزاء من الثانية إلى خيط في الخلفية.
يأتي Postext بنقطة دخول مخصّصة لعامل الويب هي postext/worker، تُخرج خطّ المعالجة من الخيط الرئيسي. وهي المسار الذي نتوقع أن تستخدمه غالبية عمليات التكامل: فمساحات عرض Canvas وHTML وPDF في Sandbox تتشارك جميعها مقبض createLayoutWorker() نفسه عبر خطّاف useLayoutWorker واحد (packages/postext-sandbox/src/worker/useLayoutWorker.ts) وتشغّله بإلغاء «الأحدث يفوز»، فتُلغي ضغطة المفتاح الجديدة البناء الجاري قبل أن ينتهي.
باختصار، التكامل المعتمد هو:
- أنشئ عاملًا مرة واحدة لكل مساحة عرض باستخدام
createLayoutWorker(). - سجّل الخطوط مرة واحدة لكل عائلة بإرسال كائنات
ArrayBufferقابلة للنقل عبرregisterFonts(payloads). - ابنِ باستخدام
build(content, config, { signal })، ممرّرًاAbortSignalجديدًا في كل استدعاء كي يمكن إلغاء عمليات البناء القديمة. - استبدل أي بناء سابق بإلغاء إشارته قبل بدء البناء التالي؛ وهذا هو نمط «الأحدث يفوز».
- تخلّص من العامل حين يُفكّ تركيب المكوّن الذي يملكه.
ويغذّي كائن VDTDocument نفسه العائد من build(...) كل المُخرِجات اللاحقة: renderPage/renderPageToCanvas لـCanvas، وrenderToHtmlIndexed لـ HTML، وrenderToPdf (من postext-pdf) لـ PDF. فتبني مرة واحدة في العامل وتحوّل إلى صور نقطية بقدر ما تحتاج الواجهة على الخيط الرئيسي.
#ما يقدّمه لك العامل
- يبقى الخيط الرئيسي حرًّا. يجري التحليل والقياس وحلقة التقارب ذات الجولات السبع كلها داخل العامل. ولا يُمسّ الخيط الرئيسي إلا حين يُرسَل
VDTDocumentالمكتمل إليه. - إلغاء «الأحدث يفوز». يمرّر
build(content, config, { signal })إشارةAbortSignalإلى العامل. والإلغاء قبل الاكتمال يُطلقAbortErrorفي الجانب الرئيسي؛ وداخل العامل يرمي خطّ المعالجةBuildCancelledErrorعند نقطة فحص الإلغاء التالية الخاصة بكل كتلة ويتوقف فورًا. - ذاكرة قياس مؤقتة لكل عامل. يحتفظ العامل بكائن
MeasurementCacheواحد طوال عمره. وعمليات البناء اللاحقة التي تتشارك الخط والنص والعرض تعيد استخدام قياسات الأسطر المخزّنة، فكتابة حرف واحد في مستند طويل لا تعيد قياس إلا الكتل التي تغيّرت مدخلاتها فعلًا. - مقاييس مطابقة لمقاييس الخيط الرئيسي. تُرسَل الخطوط إلى العامل على هيئة كائنات
ArrayBufferقابلة للنقل وتُسجَّل عبرnew FontFace(...)فيFontFaceSetالخاص بالعامل. ويقيس العامل بمقاييس خطوط Canvas نفسها التي كان الخيط الرئيسي سيستخدمها، فتتطابق مواضع تقسيم الأسطر وارتفاعات الأعمدة بايتًا ببايت. - تبقى ذاكرة الصور النقطية للرياضيات قائمة عبر عمليات البناء في العامل. يأتي مُخرِج الرياضيات بذاكرة صور نقطية مفهرسة بالمحتوى إلى جانب الذاكرة المفهرسة بالهوية، إذ إن الاستنساخ البنيوي لكائن
MathRenderعبر حدود العامل كان سيُفوّت ذاكرة الهوية في كل إعادة بناء.
#الواجهة البرمجية العامة
يقع عميل العامل في المسار الفرعي postext/worker، وهو مجموعة قليلة من الأسماء:
createLayoutWorker(opts?): LayoutWorkerHandle: ينشئ عاملًا مخصّصًا (أو يغلّف عاملًا تمرّره عبرopts.worker، أو يشغّل مدخل العامل الموجود فيopts.url) ويُعيد مقبضًا محدّد الأنواع. انظر تحميل العامل من CDN.LayoutWorkerHandle.registerFonts(faces: FontPayload[]): Promise<void>: يرسل بايتات الخطوط إلى العامل. تُنقل المخازن (buffers)، فاحتفظ بنسخة جديدة على الخيط الرئيسي إن احتجت إلى إعادة إرسالها لاحقًا.LayoutWorkerHandle.build(content, config?, { signal? }): Promise<VDTDocument>: يشغّل خطّ المعالجة. وإلغاء الإشارة يُلغي البناء الجاري.LayoutWorkerHandle.dispose(): void: يُنهي العامل ويرفض أي عمليات بناء معلّقة بـAbortError.FontPayload:{ family, weight, style, unicodeRange?, buffer: ArrayBuffer }.weightسلسلة وزن CSS ('700'،'bold')؛ وتقبلهregisterFontsأيضًا رقمًا (700). ويُنقلbufferإلى العامل حين تستدعيregisterFonts.BuildCancelledError(يُعاد تصديره منpostext): ما ترميهbuildDocumentداخليًا حين تُعيدoptions.shouldCancelالقيمةtrue. لا تراه عادة على الخيط الرئيسي: فبروتوكول العامل يحوّله إلىAbortErrorقبل أن يصل إلى شيفرتك.
وتنشر الحزمة أيضًا مسار postext/worker/entry الذي يشير إلى سكربت العامل المُصرَّف. وتحلّ createLayoutWorker() عنوان URL هذا تلقائيًا؛ ولا تحتاج إلى الإشارة إليه صراحة إلا حين تتطلب أداة التجميع لديك استدعاء new Worker(new URL(...), { type: 'module' }) مكتوبًا يدويًا، أو حين تقدّم المدخل بنفسك (opts.url).
#التكامل الأدنى
import { createLayoutWorker } from 'postext/worker';
import type { FontPayload, LayoutWorkerHandle } from 'postext/worker';
import type { PostextConfig, VDTDocument } from 'postext';
// 1. Create the worker once and keep the handle for the lifetime of your viewport.
const layout: LayoutWorkerHandle = createLayoutWorker();
// 2. Register fonts once per family (transferable ArrayBuffers).
// getConfigFontFamilies(config) is a helper that lists the families your config will render.
const payloads: FontPayload[] = await collectFontPayloadsForFamilies([
'EB Garamond',
'Open Sans',
]);
await layout.registerFonts(payloads);
// 3. Drive builds with last-wins cancellation: abort the previous signal
// before starting a new build. A stale build is thrown away inside the worker.
let pending: AbortController | null = null;
async function rebuild(
markdown: string,
config: PostextConfig,
): Promise<VDTDocument | null> {
pending?.abort();
pending = new AbortController();
try {
return await layout.build({ markdown }, config, { signal: pending.signal });
} catch (err) {
if ((err as { name?: string } | null)?.name === 'AbortError') return null;
throw err;
}
}
// 4. Dispose when the component that owns the worker unmounts.
// Pending builds reject with AbortError.
layout.dispose();وحين يُغلَّف في مكوّن React يكون شكله:
import { useEffect, useRef } from 'react';
import { createLayoutWorker } from 'postext/worker';
import type { LayoutWorkerHandle } from 'postext/worker';
import { renderPageToCanvas } from 'postext';
import type { PostextConfig } from 'postext';
export function CanvasPreview({
markdown,
config,
}: {
markdown: string;
config: PostextConfig;
}) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const workerRef = useRef<LayoutWorkerHandle | null>(null);
const pendingRef = useRef<AbortController | null>(null);
// Mount: spin up the worker and ship the fonts once.
useEffect(() => {
const handle = createLayoutWorker();
workerRef.current = handle;
(async () => {
const payloads = await collectFontPayloadsForFamilies(
getConfigFontFamilies(config),
);
await handle.registerFonts(payloads);
})();
return () => {
pendingRef.current?.abort();
handle.dispose();
};
}, []); // fonts registered once; re-register only when the family set changes
// Every keystroke or config change: supersede the in-flight build and kick a new one.
useEffect(() => {
const handle = workerRef.current;
if (!handle) return;
pendingRef.current?.abort();
const ac = new AbortController();
pendingRef.current = ac;
(async () => {
try {
const vdt = await handle.build({ markdown }, config, { signal: ac.signal });
const canvas = canvasRef.current;
if (!canvas || !vdt.pages[0]) return;
renderPageToCanvas(vdt.pages[0], vdt, canvas); // rasterise on the main thread
} catch (err) {
if ((err as { name?: string } | null)?.name !== 'AbortError') throw err;
}
})();
}, [markdown, config]);
return <canvas ref={canvasRef} />;
}النمط واحد دائمًا: أنشئ مرة واحدة، وسجّل الخطوط مرة واحدة، وابنِ مع AbortSignal مرات كثيرة، وتخلّص من العامل عند فكّ التركيب.
#تحميل العامل من CDN
يجب أن يأتي سكربت العامل من أصل الصفحة نفسه، لذا لا تستطيع نسخة من postext/worker تقدّمها شبكة توصيل محتوى (CDN) أن تشغّل الملف layout.worker.js المجاور لها. وتعالج createLayoutWorker() ذلك:
- esm.sh، دون خيارات. حين يكون
postext/workerنفسه قد حُمّل من esm.sh (ويكون عنوان URL لوحدته مثلhttps://esm.sh/postext@1.5.0/es2022/worker.mjs)، يشغّل العميلhttps://esm.sh/postext@1.5.0/worker/entryالمقابل عبر وحدة blob من سطر واحد ومن الأصل نفسه تستورده. ويصحّ الأمر نفسه على الاستيرادات التي تضيف?deps=أو?external=أو?alias=، وعلى الصيغةhttps://esm.sh/*postext@1.5.0/worker. ويحصل العامل دائمًا على البناء العادي لذلك الإصدار، لأن العامل لا يملك خريطة استيراد (import map) يحلّ بها التبعيات الخارجية. - أي خادم آخر، مع
url. لا تُكتشف شبكات CDN الأخرى، مثل jsDelivr (/+esm) أو unpkg. وتشغّلcreateLayoutWorker({ url })وحدة مدخل العامل الموجودة فيurl: مباشرة إن كان العنوان من الأصل نفسه، وعبر غلاف blob نفسه إن كان من أصل آخر. ويجب أن يسمح ذلك الخادم بالطلبات عبر الأصول (CORS). - أدوات التجميع لا تغيّر شيئًا. مع Vite أو webpack أو Next.js، واصل استدعاء
createLayoutWorker()دون خيارات: فهي تُخرج العامل جزءًا (chunk) من تطبيقك.
import { createLayoutWorker } from 'https://esm.sh/postext/worker';
const layout = createLayoutWorker();
const face = async (weight, style) => ({
family: 'EB Garamond',
weight,
style,
buffer: await (await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/eb-garamond@5/files/eb-garamond-latin-${weight}-${style}.woff2`)).arrayBuffer(),
});
await layout.registerFonts(await Promise.all([face(400, 'normal'), face(700, 'normal'), face(400, 'italic')]));
const doc = await layout.build({ markdown }, { bodyText: { fontFamily: 'EB Garamond' } });لا يرى العامل خطوط الصفحة. فله مجموعة خطوط خاصة به لا تحوي إلا الأوجه المرسلة عبر registerFonts والخطوط المثبّتة على النظام. وحين يُنضّد بناءٌ نصًا بعائلة لا يجدها العامل، يُقاس ذلك النص بخط بديل، فلا تتطابق مواضع تقسيم أسطره مع الصفحة. ويطبع العميل حينئذ تحذيرًا واحدًا في وحدة التحكم لكل عائلة ("EB Garamond" is not available inside the layout worker…)، ويُدرج العائلات في BuildStats.missingFonts، التي يتلقاها رد النداء onStats الخاص بـbuild.
#جمع بيانات الخطوط (Fontsource / Google Fonts)
تأخذ registerFonts بايتات الخطوط الخام. والخيط الرئيسي هو المكان المناسب لجلبها، لأن Google Fonts لا تُعيد WOFF2 إلا لسلاسل User-Agent الشبيهة بالمتصفحات، ولأن ذاكرة مؤقتة مركزية تتيح لعدة نُسخ من العامل أن تتشارك البايتات نفسها.
ودالة collectFontPayloadsForFamilies في Sandbox (packages/postext-sandbox/src/controls/fontLoader.ts) تطبيق مرجعي جاهز للاستخدام المباشر. وهي:
- تستعلم
https://api.fontsource.org/v1/fonts/{family-id}لمعرفة الأوزان المتاحة وما إذا كانت العائلة تأتي بمحور متغيّر. - تبني عنوان URL من نوع Google Fonts CSS2 يغطي كل وزن ونمط تعلنه العائلة.
- تجلب ورقة أنماط
@font-faceالمولَّدة، وتستخرج كل تصريحsrc: url(...) format('woff2')، وتنزّل البايتات الخام. - تُعيد
FontPayload[]يكون فيهاbufferكائنArrayBufferجديدًا في كل استدعاء، وهذا مهم لأنregisterFontsتنقل المخزن وتترك النسخة في جانب المُرسِل منفصلة (detached).
واقرنها بـgetConfigFontFamilies(config) للحصول على قائمة العائلات التي سيرسمها إعداد PostextConfig معيّن فعلًا (النص الأساسي، والعناوين، ورموز القوائم النقطية، وأرقام القوائم المرقّمة).
#الإلغاء التعاوني داخل المحرّك
إذا كنت تشغّل buildDocument بنفسك، داخل عامل مخصّص مثلًا، فإن خطّ المعالجة يتيح خطّاف shouldCancel يمكنك استخدامه مباشرة:
import { buildDocument, BuildCancelledError } from 'postext';
let superseded = false;
try {
const vdt = buildDocument(content, config, cache, {
shouldCancel: () => superseded,
});
} catch (err) {
if (err instanceof BuildCancelledError) return; // a newer build took over
throw err;
}تُستدعى shouldCancel مرة واحدة لكل كتلة من المستوى الأعلى أثناء التوزيع. والخطّاف تعاوني عن قصد: فهو لا يستطيع إيقاف استدعاء الإخراج الخاص بمكتبة pretext في منتصف سطر، لكنه يُبقي دقة الإلغاء صغيرة بما يكفي (أجزاء من الثانية) كي لا ينتظر مستخدمٌ سريع الكتابة بناءً قديمًا أبدًا.
#تصدير PDF انطلاقًا من العامل
يأخذ مُخرِج PDF كائن VDTDocument جاهزًا ويحوّله إلى بايتات PDF. وهو لا يعيد تشغيل الإخراج. ويعني ذلك أن مسار PDF المعتمد في المتصفح يقترن بالعامل اقترانًا طبيعيًا: ابنِ VDT في العامل (بعيدًا عن الخيط الرئيسي، وقابلًا للإلغاء، ومعيدًا استخدام الذاكرة المؤقتة)، ثم استدعِ renderToPdf على الخيط الرئيسي على VDT نفسه.
import type { LayoutWorkerHandle } from 'postext/worker';
import { renderToPdf } from 'postext-pdf';
import type { PostextConfig } from 'postext';
import { createPdfFontProvider } from './pdfFontProvider';
const fontProvider = createPdfFontProvider();
export async function exportPdf(
layout: LayoutWorkerHandle,
markdown: string,
config: PostextConfig,
): Promise<Uint8Array> {
// 1. Build the VDT in the worker — UI stays responsive during the layout passes.
const vdt = await layout.build({ markdown }, config);
// 2. Rasterise to PDF on the main thread. renderToPdf is fast once the VDT exists
// because it is walking precomputed coordinates, not remeasuring text.
return renderToPdf(vdt, {
fontProvider,
// pdfGeneration config on `vdt.config` is honoured automatically.
});
}إذا كنت تحتفظ أصلًا بمقبض عامل للمعاينة الحيّة، فأعد استخدامه للتصدير بدلًا من تشغيل عامل ثانٍ، فذاكرة القياس المؤقتة داخل العامل تجعل تصدير PDF الذي يلي معاينة على الشاشة شبه مجاني.
وفي الكتاب الطويل، تستغرق كتابة ملف PDF نفسه ثواني أيضًا؛ ويشغّل postext-pdf/worker تلك الخطوة على عامل خاص بها (انظر رسم PDF على عامل).
#متى تستخدم العامل ومتى لا تستخدمه
استخدم العامل في:
- المعاينات الحيّة والمحرّرات وساحات التجريب. أي حالة يُعاد فيها بناء المستند استجابةً لإدخال المستخدم.
- عارضات HTML التي تستجيب لتغيّر المقاس وتعيد تشغيل الإخراج مع كل نبضة من
ResizeObserver. - تصدير PDF داخل المتصفح حين يُطلق من واجهة فيها معاينة حيّة أصلًا؛ أعد استخدام مقبض العامل الموجود كي يستفيد التصدير من ذاكرة القياس المؤقتة.
- تبويبات إخراج متعددة تحتاج جميعها إلى VDT نفسه (مساحات عرض Canvas / HTML / PDF في Sandbox تتشارك مقبض عامل واحدًا لكل تركيب لمساحة العرض).
واستغنِ عن العامل في:
- التوليد على جانب الخادم: لا يملك Node كائن
FontFaceSetالخاص بالمتصفح، وأنت تتحكم في الخيط على أي حال. - عمليات التصدير المعزولة التي تجري مرة واحدة (أداة سطر أوامر، أو سكربت تصدير بلا واجهة، أو Cloud Function) حيث لا توجد واجهة تفاعلية يمكن حجبها. فاستدعاء
buildDocumentمباشرة أبسط ويتجنّب كلفة النقل الأولي للخطوط.
#دمج عارض HTML
عارض HTML هو مُخرِج Postext المصمَّم للشاشة أولًا. فبدلًا من تحويل الصفحات إلى صورة نقطية، يُنتج عُقد DOM ذات مواضع مطلقة تحدّد هندستَها سلسلةُ المعالجة نفسها التي تُنتج مخرجات الطباعة. وهذا يجعله الخيار المناسب حين تريد في المتصفح تنضيدًا مقروءًا وقابلًا للتحديد ويستجيب لتغيّر المقاس، في تطبيق قراءة أو معاينة داخل منتج أو واجهة توثيق مضمّنة، دون الحاجة إلى عارض PDF.
العناصر الأساسية من الواجهة البرمجية العامة:
buildDocument(content, config, cache?): يشغّل خطّ معالجة الإخراج كاملًا ويُعيدVDTDocument.renderToHtmlIndexed(doc, options): يحوّل VDT إلى سلسلة HTML واحدة مع تفصيل لكل صفحة ولكل كتلة. ويتيح هذا التفصيل ترقيع DOM بكلفة زهيدة حين لا تتغيّر بين عمليتَي عرض إلا كتل قليلة.resolveHtmlViewerConfig(partial): يملأ القيم الافتراضية لعارض HTML (maxCharsPerLine،columnGap،optimalLineBreaking).buildFontString+measureGlyphWidth+dimensionToPx: أدوات قياس أولية تُستخدم لاستنتاج عرض عمود فعلي بالبكسل من عدد حروف مستهدف.createMeasurementCache/clearMeasurementCache: ذاكرات مؤقتة قابلة للتوصيل تتيح لك إعادة استخدام القياسات عبر عمليات إعادة الإخراج.
#مثال حيّ: سلسلة HTML
الدورة الكاملة بلغة JavaScript المجرّدة، قبل التكامل مع React أدناه: ابنِ المستند، وسلّم VDTDocument إلى renderToHtml، وضع السلسلة في حاوية. يرصّ mode: 'single' الصفحات عموديًا؛ ويمنحها background لونًا، لأن الصفحات شفافة افتراضيًا. ويطبع المثال على CodePen أيضًا الترميز المولَّد، فترى الأسطر ذات المواضع المطلقة التي يُنتجها المُخرِج، والتي يرسمها المتصفح دون أن يعيد تدفّقها أبدًا.
import { buildDocument, renderToHtml } from 'https://esm.sh/postext';
const markdown = `# The Lantern
The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.
## Two columns
Postext lays this text out in **two columns**, breaking each paragraph with the *Knuth–Plass* algorithm and hyphenating with TeX patterns. Widows and orphans are avoided, and the columns are balanced on the last page.
The light it gave was small, but it was enough to find the step. The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.
The light it gave was small, but it was enough to find the step. The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.`;
const config = {
// 96 dpi: page pixels are CSS pixels, so the HTML shows at its real size.
page: { sizePreset: '17x24', dpi: 96 },
layout: { layoutType: 'double' },
bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 10, unit: 'pt' } },
};
// Postext measures text with the fonts the browser has loaded,
// so wait for every face the document uses before laying it out.
await Promise.all([
document.fonts.load('16px "EB Garamond"'),
document.fonts.load('bold 16px "EB Garamond"'),
document.fonts.load('italic 16px "EB Garamond"'),
document.fonts.load('bold 16px "Open Sans"'), // the default heading face
]);
const doc = buildDocument({ markdown }, config);
// One HTML string for the whole document. Every line is an absolutely
// positioned element, so the browser never reflows the text.
const html = renderToHtml(doc, { mode: 'single', background: '#ffffff' });
document.getElementById('viewer').innerHTML = html;
document.getElementById('source').textContent = html;
document.getElementById('status').textContent =
`${doc.pages.length} page(s) · ${(html.length / 1024).toFixed(1)} KB of HTML`;index.html
<p id="status">Laying out…</p>
<div id="viewer"></div>
<details>
<summary>Generated HTML</summary>
<pre id="source"></pre>
</details>style.css
body {
margin: 16px;
font-family: system-ui, sans-serif;
background: #e8e8e8;
}
/* The page is wider than this pane: let it scroll instead of clipping it.
The renderer centres pages with an inline style, hence the !important. */
#viewer {
overflow: auto;
}
#viewer .pt-doc {
align-items: flex-start !important;
}
/* Each page is a .pt-page block; the renderer positions every line inside it. */
#viewer .pt-page {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
details {
margin-top: 16px;
}
#source {
max-height: 240px;
overflow: auto;
padding: 8px;
background: #fff;
font-size: 11px;
white-space: pre-wrap;
word-break: break-all;
}يحمّل محررًا تفاعليًا من codepen.io. يستورد المثال أحدث إصدار من postext عبر شبكة توصيل المحتوى (CDN).
#التكامل الأدنى
المقتطف أدناه أقصر تكامل مفيد: ابنِ المستند بمقاس مساحة العرض الحالي، وارسمه في حاوية، وأعد التشغيل عند تغيّر المقاس.
import { useEffect, useRef } from 'react';
import {
buildDocument,
renderToHtmlIndexed,
resolveHtmlViewerConfig,
buildFontString,
measureGlyphWidth,
dimensionToPx,
createMeasurementCache,
} from 'postext';
import type { PostextConfig, MeasurementCache } from 'postext';
// Screen-friendly DPI: at 144 DPI, an 8pt body size resolves to 16 px.
const HTML_DPI = 144;
const PADDING_PX = 24;
// Prose sample used to measure the target column width. Proportional fonts
// make "N × average width" unreliable, so we measure a representative string.
const SAMPLE =
'The quick brown fox jumps over the lazy dog. Sphinx of black quartz, judge my vow.';
function sampleForChars(n: number): string {
let s = SAMPLE;
while (s.length < n) s += ' ' + SAMPLE;
return s.slice(0, n);
}
export function PostextHtmlViewer({
markdown,
config,
mode = 'multi',
}: {
markdown: string;
config: PostextConfig;
mode?: 'single' | 'multi';
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const cacheRef = useRef<MeasurementCache>(createMeasurementCache());
useEffect(() => {
const host = hostRef.current;
if (!host) return;
const relayout = () => {
const rect = host.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
const viewer = resolveHtmlViewerConfig(config.htmlViewer);
const fontFamily = config.bodyText?.fontFamily ?? 'EB Garamond';
const fontWeight = config.bodyText?.fontWeight ?? 400;
const fontSize = config.bodyText?.fontSize ?? { value: 8, unit: 'pt' as const };
const fontSizePx = dimensionToPx(fontSize, HTML_DPI);
// Measure the *actual* column width for N characters of body prose.
const targetColumnPx = measureGlyphWidth(
sampleForChars(viewer.maxCharsPerLine),
buildFontString(fontFamily, fontSizePx, String(fontWeight), 'normal'),
);
const inner = Math.max(rect.width - PADDING_PX * 2, 100);
let columnWidthPx: number;
if (mode === 'single') {
columnWidthPx = Math.min(targetColumnPx, inner);
} else {
// Fit as many columns as we can at the target width.
const count = Math.max(
1,
Math.floor((inner + viewer.columnGap) / (targetColumnPx + viewer.columnGap)),
);
columnWidthPx = (inner - viewer.columnGap * (count - 1)) / count;
}
columnWidthPx = Math.max(Math.floor(columnWidthPx), 80);
// Single mode uses one very tall page; multi mode uses the viewport
// height so each VDT "page" becomes one column.
const pageHeightPx =
mode === 'single' ? Math.max(rect.height * 20, 200_000) : Math.max(rect.height - PADDING_PX * 2, 400);
const override: PostextConfig = {
...config,
page: {
...config.page,
dpi: HTML_DPI,
width: { value: columnWidthPx, unit: 'px' },
height: { value: pageHeightPx, unit: 'px' },
margins: {
top: { value: 0, unit: 'px' },
bottom: { value: 0, unit: 'px' },
left: { value: 0, unit: 'px' },
right: { value: 0, unit: 'px' },
},
},
layout: { ...config.layout, layoutType: 'single' },
bodyText: {
...config.bodyText,
optimalLineBreaking: viewer.optimalLineBreaking,
},
};
const doc = buildDocument({ markdown }, override, cacheRef.current);
const { html } = renderToHtmlIndexed(doc, {
mode,
columnGap: viewer.columnGap,
padding: PADDING_PX,
background: 'transparent',
});
host.innerHTML = html;
};
relayout();
const ro = new ResizeObserver(() => relayout());
ro.observe(host);
// Re-measure when web fonts land so glyph widths aren't taken from fallbacks.
const onFontsDone = () => relayout();
document.fonts?.addEventListener?.('loadingdone', onFontsDone);
return () => {
ro.disconnect();
document.fonts?.removeEventListener?.('loadingdone', onFontsDone);
};
}, [markdown, config, mode]);
return <div ref={hostRef} style={{ width: '100%', height: '100%', overflow: 'auto' }} />;
}بعض الملاحظات حول ما يفعله هذا المثال:
- قياس العمود بدلًا من تقديره. لأن
maxCharsPerLineقيمة مستهدفة معبَّر عنها بالحروف، فإن العرض الفعلي بالبكسل يتوقف على خط النص الأساسي. وتعطيmeasureGlyphWidthقياسًا حقيقيًا بالخط المختار، فيبقى طول السطر ثابتًا عند تبديل الخطوط. - إعادة كتابة الصفحة. يعامل عارض HTML كل «صفحة» من VDT عمودًا واحدًا على الشاشة. ويستبدل المثال
page.widthبعرض العمود المقيس، ويجعل الهوامش صفرًا (فالحشو يقع خارج الصفحة في عنصر div.pt-docالمغلِّف)، ويستخدمHTML_DPI = 144كي يصبح النص الأساسي8ptمساويًا16px. - مراعاة تحميل الخطوط. يُطلَق
document.fonts.loadingdoneحين يصل خط ويب طُلب حديثًا. ومن دون إعادة الإخراج، يستخدم العرض الأول مقاييس خط بديل ثم يقفز حين يصل الخط الحقيقي. - إعادة استخدام ذاكرة القياس المؤقتة. إنشاء الذاكرة مرة واحدة لكل مكوّن يعني أن تغيّر المقاس وتغيّر حجم الخط يعيدان استخدام قياسات العرض السابق بدلًا من إعادة قياس كل فقرة.
#أبعد من ذلك
المثال أعلاه مبسّط عن قصد. وعادة ما تضيف عمليات التكامل في بيئة الإنتاج ما يلي:
- العزل عبر Shadow DOM: ارسم داخل
host.attachShadow({ mode: 'open' })كي لا يتسرّب أي CSS من الصفحة الخارجية إلى العارض. - الترقيع التدريجي: تُعيد
renderToHtmlIndexedالقيمةpages[i].blocks، لكل منهاidثابت وHTML الخارجي للكتلة. وحين لا تختلف بين عمليتَي عرض إلا كتل قليلة، يمكنك استبدال أغلفة تلك الكتل في مكانها بدلًا من إعادة بناءinnerHTML. - الطبقات العلوية: وضع SVG ذي موضع مطلق فوق كل
.pt-pageللمؤشرات أو التحديدات أو شبكات خطوط الأساس. - الروابط: تُغلَّف كلمات رابط Markdown في
<a href="…" rel="noopener noreferrer">، الذي يأخذ لون النص بلا خط تحته؛ انظر صيغة المستند › الروابط. وفي عارض شبيه بالمحرّر، اعترض النقرات علىa[href]التي لا تبدأ بـ#وافتحها في تبويب جديد (فروابط:refتشير إلى مواضع داخل المستند). - الصور أحادية الحبر: حين يكون
diagramStyle.singleInkمفعّلًا، تحصل عناصر<img>من نوع SVG على مرشّح CSS ما لم تمرّرsingleInk: falseلعناوين URL المُعاد تلوينها أصلًا؛ انظر الحبر الواحد في Canvas وفي HTML.
ويطبّق المكوّن HtmlPreview في Sandbox (packages/postext-sandbox/src/viewport/HtmlPreview/index.tsx) كل ذلك فوق الواجهة البرمجية نفسها المعروضة هنا، ويمكن استخدامه مرجعًا. وهو يمرّر أيضًا كل عملية بناء عبر عامل إخراج مشترك (انظر تشغيل الإخراج في Web Worker) كي لا تحجب التعديلات الحيّة وتغيّرات المقاس الخيطَ الرئيسي أبدًا؛ استبدل استدعاء buildDocument(...) المباشر في المقتطف أعلاه بـlayoutWorker.build(...) حين تكون مستعدًا لنقل الإخراج بعيدًا عن الخيط الرئيسي.
#كيف يختلف إخراج HTML عن Canvas وPDF
تضع renderToHtml كل سطر وشكل وعنصر تصميم في الموضع نفسه تمامًا الذي يضعه فيه Canvas وPDF، لكنها ترسم حولها أقل:
| الميزة | Canvas (renderPage) | HTML (renderToHtml) | PDF (renderToPdf) |
|---|---|---|---|
| خلفية الصفحة | بيضاء، مع page.backgroundColor فوق حدّ القص والنزف. | شفافة، ما لم تمرّر background أو تضبط page.backgroundColor (الذي يملأ حينئذ صندوق الصفحة كله، بما في ذلك الحاشية الخارجية خلف النزف). | بيضاء، مع page.backgroundColor فوق حدّ القص والنزف. |
شبكة خطوط الأساس (page.baselineGrid) | تُرسم | لا تُرسم | تُرسم |
الخط الفاصل بين الأعمدة (layout.columnRule) | يُرسم | لا يُرسم | يُرسم |
علامات القص (page.cutLines) | تُرسم | لا تُرسم؛ لكن صندوق الصفحة لا يزال يضم الحاشية الخارجية المحيطة بحدّ القص. | تُرسم |
| الصفحة السالبة | الخيار pageNegative | غير متاحة | الخيار pageNegative |
| النص | بكسلات | نص قابل للتحديد في عناصر ذات مواضع مطلقة، منضَّد بعائلات خطوط CSS: يجب أن تحمّل الصفحة الأوجه نفسها. | خطوط مضمّنة من fontProvider الخاص بك؛ والنص قابل للتحديد والبحث وموسوم. |
النص العمودي (layout.writingMode: 'vertical-rl') | تُرسم الحروف خلية خلية، مُعادةً إلى وضعها القائم؛ والأشكال العمودية عبر وجه توأم (loadVerticalAlternates). | يُدار التدفق في صندوق واحد ربع دورة؛ ويُعاد كل سطر إلى وضعه القائم ويُنضَّد بـwriting-mode: vertical-rl، فيأخذ المتصفح الأشكال العمودية ويُقيم الحروف؛ والأرقام القصيرة في text-combine-upright: all؛ والشرطة أو علامة الحذف أو النقطة الوسطى أو الشرطة المموّجة في صندوق خاص بخليتها (فالمتصفح كان سيقدّمها بعرضها الأفقي)، والشرطة تُمدّ لتملأه بحسب flow.dashAdvances. | حروف قائمة عبر توأم Identity-V لكل خط؛ انظر النص العمودي في PDF. |
| الصور | registerResourceImage | الخيار resourceImageUrl(fileId)؛ ومن دونه صندوق نائب رمادي. | الخيار resourceBytes(fileId). |
| الصيغ | مسارات متجهية | <svg> سطري | مسارات متجهية |
| الروابط | لا شيء | استشهادات :ref ترتبط بمواردها؛ أما صفوف المحتويات فلا. | استشهادات :ref وصفوف المحتويات، إضافة إلى المخطط التفصيلي (الإشارات المرجعية). |
تهمّ الصفحة الشفافة في موقع داكن: فالمعاينة دون background تُظهر نصًا أسود على الخلفية الداكنة للموقع. مرّر renderToHtml(doc, { background: '#ffffff' })، أو أعطِ المستند page.backgroundColor.
تبقى أنماط النص في الصفحة المضيفة خارجًا. يُنضّد كل سطر بالعروض التي قاسها المحرّك، لذا فإن letter-spacing أو word-spacing أو text-transform أو font-variant يرثه الإخراج من الصفحة المحيطة به سيوسّع سلاسل الحروف ويجعل الأسطر تتراكب. ولذلك يعيد الجذر .pt-doc ضبط خصائص النص الموروثة، أي تباعد الحروف والكلمات، وحالة الأحرف، والمسافة البادئة، والمسافات البيضاء، ونمط الخط ومتغيّره ووزنه وعرضه، والميزات وتقنين الأزواج (kerning)، وارتفاع السطر، والمحاذاة، وظل النص والتوكيد، والواصلات، والاتجاه، ونمط الكتابة، وحدّ النص وتعبئته، وتضخيم النص على الأجهزة المحمولة، قبل تصريحات التخطيط الخاصة به، فيبدو الإخراج متطابقًا داخل جذر ظل (shadow root) أو تحت عنصر منسَّق. وتُصدَّر هذه القائمة باسم HTML_TEXT_RESET، وهي سلسلة من تصريحات CSS: والمضيف الذي يركّب innerHtml الخاص بالصفحات (من renderToHtmlIndexed) في حاويات خاصة به يضبطها على جذرها. وحتى postext 1.4 لم يكن الجذر يعيد ضبط أي شيء؛ وكان الحل البديل غلافًا يحمل all: initial.
#توليد ملفات PDF
يقع إخراج PDF في حزمة منفصلة، postext-pdf، كي لا تتحمّل عمليات الدمج المخصّصة للويب وحده كلفة pdf-lib و@pdf-lib/fontkit. لا يعيد مُخرِج PDF قياس النص: فهو يستهلك VDTDocument نفسه تمامًا الذي تمرّره إلى renderToCanvas أو renderToHtml، ويحوّل إحداثياته المقيسة بالبكسل إلى نقاط PDF. لذلك تتطابق المخرجات الثلاثة حتمًا في فواصل الأسطر وارتفاعات الأعمدة ومواضع الموارد.
في المتصفح، ابنِ شجرة VDT عبر عامل الويب (Web Worker). الدالة
renderToPdfنفسها سريعة متى وُجدت شجرة VDT، فالجزء المكلف هو مسار الإخراج الذي أنتجها. تشغيل هذا المسار في العامل يُبقي الواجهة مستجيبة، ويتيح لتصدير PDF أن يعيد استخدام ذاكرة القياس المؤقتة نفسها التي سبق أن هيّأتها المعاينة الحية. راجع قيادة تصدير PDF من العامل لترى المسار الموصى به. الأمثلة أدناه، التي تعمل على الخيط الرئيسي، هي المرجع لـمعنى الوسائط؛ أما في شيفرة الواجهة فابنِ شجرة VDT في العامل أولًا، ولا تستدعِ مباشرةً إلاrenderToPdf.
#التثبيت
npm install postext postext-pdfpostext تبعية نظيرة (peer dependency) للحزمة postext-pdf. يحتاج كل إصدار من postext-pdf إلى إصدار postext الذي صدر معه، أو إلى إصدار لاحق من الإصدار الرئيسي نفسه (نطاقه النظير هو ^ متبوعة بذلك الإصدار، أي ^1.5.0 للإصدار 1.5.0)، لأنه يستورد دوالّ مساعدة أضافتها postext في ذلك الإصدار. حدّث الحزمتين معًا، وثبّتهما على شبكة CDN على الإصدار نفسه.
#الواجهة البرمجية العامة
تعرض الحزمة نقطة دخول واحدة وعددًا قليلًا من الأنواع:
renderToPdf(doc, options): Promise<Uint8Array>— تأخذVDTDocument(أو فصول كتاب في مصفوفة منها) وتعيد بايتات PDF الخام.PdfFontProvider— توقيع دالة الاستدعاء(family, weight, style, request?) => Promise<Uint8Array | Uint8Array[]>التي تستخدمهاrenderToPdfلطلب بايتات الخط حين تحتاج إلى تضمين تركيبة جديدة من العائلة والوزن والنمط. يحملrequest.codePointsالحروف التي تنضّدها الصفحات بوجه الخط (face) ذاك؛ والجواب ملف واحد، أو عدة ملفات تشكّل الوجه معًا (انظر خطوط الصينية واليابانية والكورية).RenderToPdfOptions—{ fontProvider, resourceBytes?, outlines?, accessible?, colorSpace?, pageNegative?, characterGrid?, onProgress?, onWarning?, rasterizeSvg?, harfbuzzWasm? }. تأخذoutlinesوaccessibleوcolorSpaceقيمتها منpdfGenerationفي المستند حين تُترك (انظر توليد PDF (الإعدادات)). يُشرحresourceBytesفي بايتات الموارد والنسخ الأصلية للطباعة، وonWarningفي الوجوه التي يُسأل عنها المزوّد والتحذيرات في المستند. يطبعcharacterGrid: trueالشبكة التي يرسمهاcjk.grid.showعلى الشاشة، والتي يُسقطها ملف PDF في غير ذلك (انظر شبكة الحروف). ويحدّدharfbuzzWasmالمكان الذي يُحمَّل منه الملفharfbuzz.wasmالخاص بـ HarfBuzz (عنوان URL نسبي إلى الصفحة، أو بايتات الملف) لمستند فيه نص من اليمين إلى اليسار أو نص متصل الحروف؛ وإن تُرك، فالنسخة الموجودة بجانب وحدة postext-pdf، ثم إصدار harfbuzzjs نفسه من jsDelivr ثم من esm.sh.PdfWarning— مشكلة غير قاتلة يُبلَّغ عنها عبرonWarning؛ ميّز نوعها بالحقلkind:'fontFallback'(PdfFontFallbackWarning)، وجه نُضّد بوجه آخر من عائلته؛'missingGlyph'(PdfMissingGlyphWarning)، حروف لا يحوي أي ملف من ملفات الوجه رسمًا (glyph) لها؛'variableFontDefaultInstance'(PdfVariableFontWarning)، خط متغيّر طُلب بوزن غير وزن نسخته الافتراضية؛'cffEmbeddedWhole'(PdfCffEmbeddedWholeWarning)، وجه CFF يتجاوز 2 MB ضُمِّن كاملًا؛'complexShapingUnavailable'(PdfComplexShapingWarning)، نص من اليمين إلى اليسار أو متصل الحروف رُسم دون HarfBuzz لتعذّر تحميله (يسردreasonالأماكن التي بُحث فيها عنه)، فتقع العلامات العربية في غير مواضعها؛ أو'missingImage'، صورة بلا بايتات رُسم مكانها عنصر نائب (لا يُبلَّغ عنه إلا إلىonWarningتمرّره أنت؛ أما تحذيرات الخطوط فتذهب، في غيابها، إلىconsole.warn).decompressWoff2(bytes): Uint8Array— دالة مساعدة تحوّل ملف WOFF2 إلى بايتات TTF، وهي الصيغة التي تستطيعpdf-libتضمينها مباشرةً.createPdfWorker(options?)، منpostext-pdf/worker— الإنتاج نفسه في عامل ويب؛ انظر إنتاج ملف PDF في عامل.
#مثال مختصر
import { buildDocument } from 'postext';
import { renderToPdf } from 'postext-pdf';
const vdt = buildDocument(
{ markdown: '# Chapter One\n\nThe story begins here…' },
{
page: { sizePreset: '17x24' },
layout: { layoutType: 'double' },
bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 9, unit: 'pt' } }, // 9 pt overrides the 8 pt default
},
);
const pdfBytes = await renderToPdf(vdt, {
fontProvider: async (family, weight, style) => {
// Return TTF bytes for this family/weight/style.
// See the "Font provider" section below for a real implementation.
const res = await fetch(`/fonts/${family}-${weight}${style === 'italic' ? 'i' : ''}.ttf`);
return new Uint8Array(await res.arrayBuffer());
},
});
// `pdfBytes` is a Uint8Array — save, download, or stream it.
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
window.open(url);#لماذا مزوّد الخطوط؟
تضمّن pdf-lib ملفات خطوط حقيقية داخل ملف PDF: فالخطوط المثبّتة في المتصفح غير متاحة وقت الإنتاج، والخط الذي حمّلته لقياس النص على الشاشة فقط لا يكفي وحده لإنتاج ملف PDF مكتفٍ بذاته. تمسح renderToPdf الصفحات بحثًا عن كل وجه ترسمه (وجه لكل تركيبة family|weight|style؛ انظر الوجوه التي يُسأل عنها المزوّد) وتستدعي المزوّد مرة واحدة لكل تركيبة فريدة. يعيد المزوّد Uint8Array من بايتات TTF أو OTF، أو قائمة منها لوجه يُقدَّم في عدة ملفات (انظر خطوط الصينية واليابانية والكورية)؛ وتقتطع pdf-lib من مخططات TrueType المجموعة الفرعية (subset) المستخدمة، وتضمّن ملفات CFF (.otf) كاملة. والوجوه التي يجيب عنها المزوّد بالملف نفسه، كوجه عادي يحلّ محل الوجه العريض الذي تفتقر إليه العائلة، تتشارك خطًا مضمَّنًا واحدًا. أما الوجه الذي لا ترسم به أي صفحة في النهاية، كوجه نص شكل SVG حين يُرسم الشكل صورةً، فيُستبعد من الملف.
استخدم خطوطًا ثابتة لكل وزن، لا خطًا متغيّرًا واحدًا. كثيرًا ما تقدّم Google Fonts ملف WOFF2 متغيّرًا واحدًا لكل عائلة يغطي محور الوزن كله. ولا تستطيع pdf-lib أن تضمّن من الملف المتغيّر إلا نسخته الافتراضية، فتُطبع الفقرة العريضة بالوزن العادي. تنشر Fontsource ملفات WOFF2 ثابتة لكل وزن تحل هذه المشكلة حلًا نظيفًا، وهذا هو النمط الذي يتبعه Sandbox، بيئة التجربة في المتصفح. ويُبلَّغ عن الملف المتغيّر المطلوب بوزن غير وزن نسخته الافتراضية بتحذير variableFontDefaultInstance.
كل كلمة من النص تقع حيث وضعها الإخراج. في الفقرات وعناصر القوائم والاقتباسات والإطارات وغيرها من النص المتدفق، تبدأ كل كلمة في الموضع الذي قاسته شجرة VDT، فلا يتراكم على امتداد السطر أي فرق بين عروض المتصفح وعروض الوجه المضمَّن. يُرسم السطر الخالي من التنسيق الداخلي كائنًا نصيًا واحدًا يحرّك القلم بين الكلمات؛ أما الأسطر المضبوطة والمتوسّطة والأسطر ذات التنسيق فتُرسم كلمةً كلمة. والحرف الذي ليس للوجه رسم له، والذي قاسه المتصفح بخط آخر ويرسمه ملف PDF مربعَ الرسم المفقود في ذلك الوجه، لا يُزيح أيًّا من الكلمات التي تليه، ويُبلَّغ عنه مرة واحدة لكل وجه بتحذير missingGlyph. والمسافة التي يفتقر إليها الوجه، كالمسافة الضيقة غير القابلة للكسر أو مسافة الأرقام، تأخذ العرض الذي أعطاها إياه المتصفح، ولا تُرسم الحروف غير المرئية كرابط الكلمات (word joiner) والمسافة الصفرية العرض. أما الواصلة غير القابلة للكسر (U+2011) التي يفتقر إليها الوجه فتُرسم بواصلة الوجه (U+2010)، أو بعلامة الواصلة والناقص (hyphen-minus) إن لم تكن فيه واصلة أيضًا، كما يعرضها المتصفح؛ ومن الخطوط التي تفتقر إلى الاثنتين Open Sans وOutfit. ولا تُعدّ أيّ من الحالتين رسمًا مفقودًا. وثمة استثناءان. فالسطر الذي فيه حروف من اليمين إلى اليسار يُرسم سلسلةً واحدة على أي حال (انظر اللغات وأنظمة الكتابة). والنص الذي يضعه التصميم (الترويسات العلوية والسفلية، وصفحات الافتتاح، وعناوين الإطارات، وغيرها من عناصر التصميم) يُنضَّد بعروض الوجه المضمَّن نفسه، فالرسم الذي يفتقر إليه الوجه هناك يظل يُزيح باقي سطره.
#الوجوه التي يُسأل عنها المزوّد
تمرّ renderToPdf على الصفحات بالطريقة التي ترسمها بها، ولا تسأل المزوّد إلا عن الوجوه التي ترسم بها:
- الوجه العادي لكل كتلة تنضّد أي سطر، والوجه العريض أو المائل أو العريض المائل لكل سلسلة نُضّدت فعلًا بتلك الطريقة؛
- سلاسل الشارات (chips)، وعلامات القوائم، ونص خانات التصميم (الترويسات، وأرقام الصفحات، وأشرطة صفحات الافتتاح والأجزاء)؛
- نص التعليق والملاحظة وخلايا الجداول لكل مورد؛
- الوجوه التي يسمّيها
<text>في شكل SVG، حين يُضمَّن الشكل. والعائلة التي لا يستطيع المزوّد توفيرها إطلاقًا يُنتقل منها إلى العائلة التالية في قائمةfont-familyفي ملف SVG.
فلا يُسأل أبدًا عن الوجه المائل لعائلة عناوين لا يُميلها أحد، ولا يحتاج شكل بلا ملاحظة إلى وجوه الملاحظات.
حين يرفض المزوّد وجهًا، يستمر الإنتاج. يُضمَّن مكانه وجه آخر من العائلة نفسها، ويُبلَّغ عن PdfWarning. والبديل هو أول وجه يُحمَّل، بتجربة الأوزان القياسية التسعة (من 100 إلى 900) بالترتيب الذي تتبعه مطابقة الخطوط في CSS، وهو أيضًا الوجه الذي يعرضه المتصفح في المعاينة:
- النمط نفسه أولًا. للوزن من 400 إلى 500، تأتي الأوزان حتى 500 أولًا، ثم الأخف بدءًا من الأقرب نزولًا، ثم الأثقل بدءًا من 600 صعودًا. وللوزن الأقل من 400، تأتي الأوزان الأخف أولًا بدءًا من الأقرب نزولًا، ثم الأثقل. وللوزن الأكبر من 500، تأتي الأوزان الأثقل أولًا، ثم الأخف؛
- ثم النمط الآخر، المائل بدل القائم والقائم بدل المائل، بالوزن المطلوب ثم بالأوزان الأخرى بالترتيب نفسه.
لذلك تنضّد العائلة التي لا مائل فيها سلاسلها المائلة قائمةً، وتأخذ العائلة التي لا تقدّم إلا 400 و700 الوزن 600 على أنه 700، وتنضّد العائلة التي لا تقدّم إلا وجهًا واحدًا كل شيء به. ويُسأل المزوّد عن وجه واحد في كل مرة، بذلك الترتيب، ولا يُسأل مرتين عن الوجه نفسه، فلا يُضمَّن أبدًا وجه لا يستخدمه شيء. والعائلة التي لا يستطيع المزوّد توفيرها إطلاقًا يُسأل عن كل وجه من وجوهها الثمانية عشر هذه قبل أن يفشل الإنتاج.
const bytes = await renderToPdf(doc, {
fontProvider,
onWarning: (w) => {
// { kind: 'fontFallback', family: 'Oswald', weight: 700, style: 'italic',
// fallback: { weight: 700, style: 'normal' }, reason: '…', message: '…' }
console.info(w.message);
},
});في غياب onWarning، تذهب الرسالة إلى console.warn. يحتفظ النص بمواضعه، وهي مأخوذة من شجرة VDT وقيست بالوجوه التي كانت لدى المتصفح، لذا قد يبدو البديل ذو العروض المختلفة متراصًّا أو متخلخلًا. وفّر الوجه الحقيقي لإصلاح ذلك. ولا يفشل الإنتاج (postext-pdf: failed to load font(s): …) إلا حين لا يستطيع المزوّد توفير أي وجه لعائلة ما بوزن قياسي، قائمًا كان أو مائلًا.
#مزوّد الخطوط في المتصفح (Fontsource + WOFF2)
يأتي Sandbox مع createPdfFontProvider() (packages/postext-sandbox/src/viewport/pdfFontProvider.ts)، ويمكنك نسخها إلى أي تطبيق يعمل في المتصفح. هذه أساسياتها:
import type { PdfFontProvider } from 'postext-pdf';
import { decompressWoff2 } from 'postext-pdf';
const bytesCache = new Map<string, Promise<Uint8Array>>();
function fontsourceId(family: string): string {
return family.toLowerCase().replace(/\s+/g, '-');
}
function fontsourceWoff2Url(
family: string,
weight: number,
style: 'normal' | 'italic',
): string {
const id = fontsourceId(family);
return `https://cdn.jsdelivr.net/npm/@fontsource/${id}@latest/files/${id}-latin-${weight}-${style}.woff2`;
}
export function createPdfFontProvider(): PdfFontProvider {
return async (family, weight, style) => {
const key = `${family}|${weight}|${style}`;
const cached = bytesCache.get(key);
if (cached) return cached;
const promise = (async (): Promise<Uint8Array> => {
const url = fontsourceWoff2Url(family, weight, style);
const res = await fetch(url, { mode: 'cors' });
if (!res.ok) throw new Error(`font fetch failed: ${res.status} ${url}`);
// pdf-lib needs TTF bytes, so decompress the WOFF2 wrapper client-side.
return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
})();
bytesCache.set(key, promise);
return promise;
};
}ينبغي لنسخة الإنتاج أيضًا أن:
- تستعلم عن الأوزان المتاحة (عبر
https://api.fontsource.org/v1/fonts/{id}) وتقرّب الوزن المطلوب إلى أقرب وزن تقدّمه العائلة فعلًا، فينجح طلبweight: 600على عائلة لا تملك إلا{400, 700}. - تعود من المائل إلى العادي حين لا يكون للعائلة وجه مائل بالوزن المطلوب، بدل إفشال الإنتاج كله.
- تعيد استخدام الذاكرة المؤقتة بين عمليات الإنتاج (أبقِ
bytesCacheعلى مستوى الوحدة، لا داخل كل استدعاء)، فتصبح إعادة توليد ملف PDF بعد تغيير في الإعدادات شبه مجانية.
#خطوط الصينية واليابانية والكورية
لا يأتي وجه CJK في ملف صغير واحد. تقدّم Fontsource الخط Noto Serif SC في نحو مئة ملف لكل وزن، يحمل كل منها جزءًا من الحروف، ويُعلَن عنه في ورقة أنماط العائلة مع نطاقه unicode-range (@fontsource/noto-serif-sc/400.css)؛ ويُنزّل المتصفح الملفات التي يمسّها نص الصفحة. وملف latin الذي يجلبه المزوّد أعلاه لا يحوي أي حرف من حروف الهان (Han) على الإطلاق، والمجموعات الفرعية المسمّاة ناقصة: فالمجموعة chinese-simplified من Noto Serif SC تفتقر إلى 釵، والمجموعة chinese-traditional من Noto Serif TC لا تحوي أيًّا من العلامات كاملة العرض (),!?:;.
لذا يجوز للمزوّد أن يجيب عن الوجه بعدة ملفات. تمرّر إليه renderToPdf الحروف التي تنضّدها الصفحات بذلك الوجه (request.codePoints)، مجموعةً من كل الفصول قبل رسم أي شيء، ويعيد المزوّد الملفات التي تحملها، بالترتيب الذي يبحث به المتصفح فيها. يُضمَّن كل ملف مجموعةً فرعية مستقلة، ويُرسم كل حرف من أول ملف فيه رسم له: فالفصل الذي يمسّ 60 شريحة يضمّن 60 مجموعة فرعية صغيرة. والوجه الذي يُطلب مرة أخرى، لنص شكل SVG مثلًا، لا يُسأل عنه إلا للحروف التي تفتقر إليها ملفاته. والمزوّد الذي يعيد Uint8Array واحدة يعمل كما كان. يقرأ مزوّد Sandbox ورقة أنماط Fontsource الخاصة بالوزن والنمط، ويجلب الملفات التي تحوي نطاقاتها النص؛ وهذا جوهره:
import type { PdfFontProvider } from 'postext-pdf';
import { decompressWoff2 } from 'postext-pdf';
type Slice = { url: string; ranges: Array<[number, number]> };
async function fontsourceSlices(family: string, weight: number, style: 'normal' | 'italic'): Promise<Slice[]> {
const id = family.toLowerCase().replace(/\s+/g, '-');
const cssUrl = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/${weight}${style === 'italic' ? '-italic' : ''}.css`;
const css = await (await fetch(cssUrl)).text();
return [...css.matchAll(/@font-face\s*{([^}]*)}/g)].map(([, rule]) => ({
url: new URL(/url\(\.?\/?([^)]+\.woff2)\)/.exec(rule)![1], cssUrl).href,
ranges: /unicode-range:\s*([^;]+);/.exec(rule)![1].split(',').map((part) => {
const [lo, hi = lo] = part.trim().slice(2).split('-');
return [parseInt(lo, 16), parseInt(hi, 16)] as [number, number];
}),
}));
}
export const sliceFontProvider: PdfFontProvider = async (family, weight, style, request) => {
// Where ranges overlap, the browser tries the last rule first.
const slices = (await fontsourceSlices(family, weight, style)).reverse();
const picked = new Set<Slice>();
for (const cp of request?.codePoints ?? []) {
const slice = slices.find((s) => s.ranges.some(([lo, hi]) => cp >= lo && cp <= hi));
if (slice) picked.add(slice);
}
if (picked.size === 0) picked.add(slices[0]!);
return Promise.all(slices.filter((s) => picked.has(s)).map(async (s) =>
decompressWoff2(new Uint8Array(await (await fetch(s.url)).arrayBuffer()))));
};وتمرّ العائلة اللاتينية بالشيفرة نفسها: يأخذ النص الإنجليزي ملفه latin وحده، ويأخذ النص التشيكي latin وlatin-ext.
الحروف التي لا يحوي أي ملف من ملفات الوجه رسمًا لها تُرسم بالرسم .notdef في الخط (مربع فارغ في معظم الخطوط)، وتبلّغ renderToPdf عنها مرة واحدة لكل وجه بعد رسم الصفحات:
// { kind: 'missingGlyph', family: 'Noto Serif TC', weight: 400, style: 'normal',
// characters: [',', '!', '?'], message: '…' }يسرد Sandbox هذه الحروف، والتحذيرين الواردين أدناه، في لوحة الفحوص (Checks) بعد كل ملف PDF يولّده. وحين يتغيّر الكتاب أو إعداداته أو موارده، تُعلَّم على أنها آتية من ملف PDF سابق إلى أن يحل محلها الملف التالي؛ وفتح كتاب آخر يمسحها.
- الوجه العريض يحتاج إلى ملف ثابت لكل وزن. تقدّم Fontsource كل وزن من Noto Serif SC وTC في ملفات ثابتة منفصلة، لذا يعمل الوجه العريض في Sandbox. أما ملفات Google Fonts (
NotoSerifSC[wght].ttf، 25 MB) فهي خطوط متغيّرة: تضمّن pdf-lib نسختها الافتراضية، فيُطبع الوجه 700 بالوزن 400، وتبلّغrenderToPdfعن ذلك بتحذيرvariableFontDefaultInstance. ولحزمة الكتاب، اقتطع نسخة ثابتة لكل وزن بأداة fontTools (fonttools varLib.instancer NotoSerifSC[wght].ttf wght=700) ثم اقتطع منها المجموعة الفرعية لحروف الكتاب بالأداةpyftsubset. - استخدم إصدارات TrueType. ملفات
.otfمن Source Han Serif وNoto Serif CJK ذات مخططات CFF، وتضمّنها postext-pdf كاملة، من 8 إلى 25 MB لكل وزن؛ ويُبلَّغ عن وجه CFF يتجاوز 2 MB بتحذيرcffEmbeddedWhole. أما إصدارات TrueType (Google Fonts، Fontsource) فتُقتطع منها الرسوم المستخدمة فقط.
لا يؤخذ الحرف المفقود من عائلة ما من عائلة أخرى: فلا تستعير Noto Serif TC من Noto Serif SC. احسم التغطية عند بناء ملفات الخطوط؛ فنموذج العرض 红楼梦 ينسخ إلى مجموعاته الفرعية من TC الرسوم التي تفتقر إليها، آخذًا إياها من وجه SC.
#مزوّد الخطوط على الخادم (Node، ملفات محلية)
في Node يمكنك تجاوز خطوة WOFF2 كليًا وقراءة ملفات TTF/OTF من القرص:
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import type { PdfFontProvider } from 'postext-pdf';
const FONT_DIR = '/path/to/fonts';
function filename(family: string, weight: number, style: 'normal' | 'italic'): string {
const slug = family.replace(/\s+/g, '');
const styleSuffix = style === 'italic' ? 'Italic' : '';
const weightName =
weight >= 700 ? 'Bold'
: weight >= 600 ? 'SemiBold'
: weight >= 500 ? 'Medium'
: weight >= 300 ? 'Light'
: 'Regular';
return `${slug}-${weightName}${styleSuffix}.ttf`;
}
export const localFontProvider: PdfFontProvider = async (family, weight, style) => {
const buf = await readFile(join(FONT_DIR, filename(family, weight, style)));
return new Uint8Array(buf);
};#بايتات الموارد والنسخ الأصلية للطباعة
تعيد resourceBytes(fileId) البايتات الخام لصورة، ويتعرّف المُخرِج على صيغتها من البايتات:
- تُضمَّن صور PNG وJPEG وGIF وWebP صورًا؛
- تُرسم شيفرة SVG مسارات متجهية، أو تُحوَّل إلى صورة نقطية بدقة 600 dpi في المتصفح حين تستخدم ميزات خارج المجموعة المتجهية المدعومة؛
- ملف PDF تُضمَّن صفحته الأولى كما هي، في هيئة form XObject.
تُخزَّن كل صورة في الملف مرة واحدة، مهما تكرر رسمها. يصبح ملف SVG المرسوم مسارات متجهية form XObject ترسمه كل صفحة، فالإطار أو الشعار في تصميم صفحات مستند من ثلاثين صفحة يُكتب مرة واحدة لا ثلاثين مرة؛ وتضيف كل صفحة إضافية بضع مئات من البايتات. وحتى الإصدار 1.4 من postext-pdf، كانت كل صفحة تحمل نسختها الخاصة من المسارات.
يمكن لشكل SVG أن يسمّي نسخة أصلية للطباعة (print master) في svg.pdfFileId: ملف PDF من صفحة واحدة للشكل نفسه، وهو عادةً الأصل الذي صُدِّر منه ملف SVG. تسأل renderToPdf الدالة resourceBytes عن معرّف النسخة الأصلية أولًا. وتضمّن تلك الصفحة مكان ملف SVG، بخطوطها وتدرّجاتها وفضاءات ألوانها سليمةً، أينما رُسم ملف SVG: شكلًا، أو صورةً في خلية جدول (TableCell.image)، أو صورةَ تصميم، أو أيقونةَ إطار (وعلامته marker كذلك). تحمل شجرة VDT معرّف النسخة الأصلية في كل استخدام من هذه الاستخدامات (svg.pdfFileId على مورد الشكل، وpdfFileId على صورة الخلية وعلى كتلة صورة التصميم)، فيحصل كل فصل من فصول الكتاب على النسخة الأصلية. ويواصل مُخرِجا canvas وHTML رسم ملف SVG. وتُستخدم بايتات ملف SVG نفسه بدلًا منها في ثلاث حالات: حين تكون النسخة الأصلية مفقودة، أو حين لا تكون ملف PDF، أو حين يكون الحبر الواحد مفعّلًا (diagramStyle.singleInk يعيد تلوين شيفرة SVG وحدها).
const resources: Resource[] = [{
id: 'map', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0,
svg: { fileId: 'map.svg', width: 800, height: 600, pdfFileId: 'map.pdf' },
}];
const files = new Map([['map.svg', svgBytes], ['map.pdf', masterPdfBytes]]);
const pdf = await renderToPdf(buildDocument({ markdown, resources }, config), {
fontProvider,
resourceBytes: (fileId) => files.get(fileId),
});يمكن للمضيف أيضًا أن يعيد بايتات النسخة الأصلية لمعرّف ملف SVG نفسه، كما تفعل bundleResourceBytes؛ والطريقتان تعملان.
#النص العمودي في ملف PDF
تُرسم الصفحة العمودية (layout.writingMode: 'vertical-rl') عبر إطار مُدار ربع دورة، كما ترسمها Canvas، ويُنضَّد نصها نزولًا في العمود:
- الحروف القائمة تُعرض عبر خط Type0 ثانٍ من الملف المضمَّن نفسه: الخط CIDFont نفسه والعروض نفسها وجدول ToUnicode نفسه، مع
Encoding /Identity-V(الوضع العمودي). كل سلسلة من الحروف كائن نصي واحد تتقدّم رسومه بنفسها مسافة em واحدة نزولًا في العمود (DW2 [880 −1000])، فتحدّد برامج القراءة العمود وتستخرجه سطرًا واحدًا. تُشكَّل الرسوم بخاصيتَي OpenType المسمّاتينvertوfwid، ما يعطي الأقواس وعلامات الاقتباس وعلامات الوقف الصينية القارية وعلامات الحذف والشرطات أشكالها العمودية؛ والحرف الذي يقف قائمًا كما هو يحتفظ برسمه الأفقي. ولا يُضمَّن أي شيء من الخط مرتين. - الكلمات اللاتينية والأعداد الطويلة تجري جانبيًا بالخط الأفقي؛ والعدد المنضَّد في خلية واحدة يقف قائمًا، مضغوطًا عرضًا إلى مقدار em حين يكون أعرض منه؛ والعلامة التي ليس لها في الخط شكل عمودي تُدار أو تُزاح، كما في Canvas.
- التتبّع بين الحروف يُكتب أعدادًا في
TJ، وهي في الوضع العمودي تحرّك القلم نزولًا في العمود. - كل سطر عمودي يُعلَّم بـ
/ActualTextيحمل نصه، فيقرؤه النسخ واستخراج النص كما كُتب. يقرأpdftotextوpdf.js الأعمدة من الأعلى إلى الأسفل ومن اليمين إلى اليسار؛ ويبدأ pdf.js سطرًا جديدًا عند عدد منضَّد في خلية واحدة. - الروابط والإشارات المرجعية والوجهات تُسقَط على الورقة: الرابط فوق سطر عمودي مستطيل طويل رفيع، والإشارة المرجعية تفتح الصفحة عند أعلى عمود عنوانها.
- ملف PDF الموسوم (tagged) يصرّح بنمط الكتابة في عنصره
Document(السمةWritingMode /TbRlمن سمات Layout، وترثها كل العناصر)؛ ويجتاز الفصل العمودي التحقق من PDF/UA-1 (veraPDF). - برامج العرض: تعرض Acrobat وPreview وChrome (PDFium) وpdf.js وPoppler الخطوط العمودية. والكتاب المجلّد من اليمين (
page.binding) يطلب أيضًا من برامج العرض أن تعرض صفحاته المتقابلة من اليمين إلى اليسار (/Direction /R2L،/PageLayout /TwoPageRight)؛ وتلتزم بذلك Acrobat وFoxit، ولا يلتزم به Chrome.
يبلغ حجم فصل من 43 صفحة منضَّد بخط Noto Serif TC (حروف الكتاب، TrueType) نحو 820 KB، معظمها للمجموعتين الفرعيتين من الخط.
#الروابط في ملف PDF
تصبح كلمات رابط Markdown (انظر صيغة المستند › الروابط) تعليقات روابط توضيحية (annotations) من نوع URI، واحدًا لكل سلسلة من الكلمات المرتبطة في السطر. يغطي كل منها صندوق السطر، ولا إطار له. وفي الإنتاج الميسَّر الوصول، تكون كل سلسلة عنصر Link قيمة /Contents فيه هي نصه. لا تُربط إلا الوجهات المطلقة http: وhttps: وmailto: وtel: وftp:، لأن عنوان URL النسبي لا أساس له داخل ملف PDF. وتُرمَّز الحروف الواقعة خارج ASCII القابل للطباعة بترميز النسبة المئوية. وتحتفظ استشهادات :ref وصفوف جدول المحتويات بروابطها داخل المستند.
#مثال كامل في المتصفح: البناء والإنتاج والتنزيل
بجمع كل ما سبق: ابنِ شجرة VDT، وأنتج ملف PDF، وأطلق تنزيلًا من المتصفح:
import { buildDocument, createMeasurementCache } from 'postext';
import { renderToPdf } from 'postext-pdf';
import { createPdfFontProvider } from './pdfFontProvider';
const fontProvider = createPdfFontProvider();
export async function downloadPdf(markdown: string, config: PostextConfig) {
const cache = createMeasurementCache();
const vdt = buildDocument({ markdown }, config, cache);
const bytes = await renderToPdf(vdt, { fontProvider });
const blob = new Blob([bytes.slice().buffer], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'document.pdf';
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}مهم: استدعِ ensureConfigFontsLoaded(config) (أو ما يعادلها) قبل buildDocument حين تشير إعداداتك إلى خطوط ويب. يُقاس الإخراج بمقاييس الخط المتوفرة لدى المتصفح لتلك العائلة في تلك اللحظة؛ فإن لم يصل الخط الحقيقي بعد، قيست شجرة VDT بخط بديل ولن يطابق ملف PDF مخرجات Canvas أو HTML. يفعل Sandbox ذلك صراحةً قبل كل إنتاج (انظر packages/postext-sandbox/src/viewport/PdfViewport.tsx).
#مثال حي: ملف PDF في المتصفح
المسار الكامل أعلاه، يعمل في المتصفح: يستورد هذا المثال على CodePen الحزمتين postext وpostext-pdf من شبكة CDN، ويحمّل خطوط الويب، ويبني المستند، ويضمّن وجوه Fontsource عبر مزوّد الخطوط، ويسلّم البايتات إلى رابط يفتح الملف في تبويب جديد، وإلى رابط تنزيل. ولملف PDF الناتج فواصل الأسطر نفسها التي في مخرجات Canvas وHTML، وخطوط حقيقية مضمَّنة، وإشارات مرجعية في المخطط التفصيلي.
import { buildDocument } from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';
const markdown = `# The Lantern
The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.
## Two columns
Postext lays this text out in **two columns**, breaking each paragraph with the *Knuth–Plass* algorithm and hyphenating with TeX patterns. Widows and orphans are avoided, and the columns are balanced on the last page.
The light it gave was small, but it was enough to find the step. The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.`;
const config = {
page: { sizePreset: '17x24' },
layout: { layoutType: 'double' },
bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 10, unit: 'pt' } },
};
// The PDF embeds real font files. Fontsource publishes one static WOFF2 per
// weight and style; decompress it to the TTF bytes pdf-lib can embed.
const fontProvider = async (family, weight, style) => {
const id = family.toLowerCase().replace(/\s+/g, '-');
const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@latest/files/${id}-latin-${weight}-${style}.woff2`;
const res = await fetch(url);
if (!res.ok) throw new Error(`font fetch failed: ${res.status} ${url}`);
return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
};
// Layout is measured with the browser's fonts, so load them before building:
// otherwise the PDF would not match the canvas or HTML output.
await Promise.all([
document.fonts.load('16px "EB Garamond"'),
document.fonts.load('bold 16px "EB Garamond"'),
document.fonts.load('italic 16px "EB Garamond"'),
document.fonts.load('bold 16px "Open Sans"'), // the default heading face
]);
const doc = buildDocument({ markdown }, config);
// Same VDT, now translated to PDF points: identical line breaks and placement.
const bytes = await renderToPdf(doc, { fontProvider });
// A PDF viewer cannot run inside this sandboxed result frame,
// so hand the file to a new tab and to a download link.
const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' }));
document.getElementById('open').href = url;
document.getElementById('download').href = url;
document.getElementById('links').hidden = false;
document.getElementById('status').textContent =
`${doc.pages.length} page(s) · ${(bytes.length / 1024).toFixed(0)} KB PDF`;index.html
<p id="status">Rendering…</p>
<p id="links" hidden>
<a id="open" target="_blank" rel="noopener">Open lantern.pdf in a new tab</a> ·
<a id="download" download="lantern.pdf">Download it</a>
</p>style.css
body {
margin: 16px;
font-family: system-ui, sans-serif;
}يحمّل محررًا تفاعليًا من codepen.io. يستورد المثال أحدث إصدار من postext عبر شبكة توصيل المحتوى (CDN).
#إنتاج ملف PDF في عامل
تنقل postext-pdf/worker الدالة renderToPdf خارج الخيط الرئيسي. يكتب العامل النص والأشكال المتجهية وشجرة البنية والملف نفسه. وتحتاج مهمتان إلى الصفحة، فيطلبهما العامل من الخيط الرئيسي: جلب الخطوط، وتحويل SVG إلى صورة نقطية عبر <img>. ففي كتاب من مئات الصفحات يستغرق الإنتاج ثواني كانت ستجمّد الصفحة لولا ذلك؛ أما لبضع صفحات فاستدعاء renderToPdf مباشرةً أبسط.
import { createPdfWorker } from 'postext-pdf/worker';
const pdfWorker = createPdfWorker();
const bytes = await pdfWorker.render(docs, {
fontProvider, // runs on this thread
resourceBytes: new Map([['map.svg', svgBytes]]), // a Map; its buffers move to the worker
onProgress: ({ phase, pages, totalPages }) => showProgress(phase, pages, totalPages),
onWarning: (w) => console.info(w.message),
});
pdfWorker.dispose();- تأخذ
render(docs, options)مستندًا واحدًا أو مصفوفة مستندات فصول كتاب. وتأخذ خياراتrenderToPdf، مع فرقين. فـresourceBytesهناMap<string, Uint8Array>تُنقل مخازنها المؤقتة (buffers) إلى العامل، فمرّر نسخًا من البايتات التي تحتفظ بها. وrasterizeSvg، إن أُعطيت، تعمل على الخيط الرئيسي؛ وافتراضيًا يتولى المهمةImageولوحة canvas الخاصان بالصفحة. - المقبض الواحد يُنتج مستندًا واحدًا في كل مرة. وتُنهي
dispose()العامل وترفض أي إنتاج لا يزال معلّقًا. - تأخذ
createPdfWorker({ worker })كائنWorkerتنشئه أنت، لأدوات البناء التي تتحكم في عناوين العمّال. ويجب أن يشغّل ذلك العاملpostext-pdf/worker/entry.
من شبكة CDN. افتراضيًا يُحمَّل سكربت العامل من عنوان الحزمة نفسها (new URL('./pdf.worker.js', import.meta.url)). وقد لا تستطيع صفحة على أصل (origin) آخر تشغيله: فعند الاستيراد من esm.sh، ترمي createPdfWorker() الخطأ Failed to construct 'Worker': Script at 'https://esm.sh/postext-pdf@…/pdf.worker.js' cannot be accessed from origin …. شغّل بدلًا من ذلك عامل وحدة (module worker) من الأصل نفسه يستورد نقطة الدخول (وإن ثبّتّ إصدارًا، فثبّت الإصدار نفسه في العنوانين):
import { createPdfWorker } from 'https://esm.sh/postext-pdf/worker';
const entry = URL.createObjectURL(new Blob(
["import 'https://esm.sh/postext-pdf/worker/entry';"],
{ type: 'text/javascript' },
));
const pdfWorker = createPdfWorker({ worker: new Worker(entry, { type: 'module' }) });ويحتاج عامل الإخراج من postext/worker إلى الغلاف نفسه حول postext/worker/entry (انظر تشغيل الإخراج في عامل ويب).
#ملفات PDF جاهزة للطباعة
لسير عمل الطباعة الإنتاجية، اضبط خيارات الإعدادات هذه قبل الإنتاج:
page.cutLines.enabled: true— يضيف منطقة النزف وعلامات القص حول حدّ القص، ويعطي كل صفحة TrimBox وBleedBox. انظر خطوط القص.colorSpace: 'cmyk'(أوpdfGeneration: { forceColorSpace: true, colorSpace: 'cmyk' }) — يكتب الألوان التي يرسمها postext (النص، والخطوط الفاصلة، والتعبئات، والأشكال المتجهية) في DeviceCMYK، ويرسم علامات القص بلون التسجيل (registration)، فتُطبع على كل لوح طباعة. أما الصور النقطية ونسخ PDF الأصلية للطباعة فتُضمَّن كما هي، فتبقى الصورة الفوتوغرافية بنظام RGB على RGB: حوّلها قبل أن تضيفها.page.dpi: 300(أو أعلى) — نقاط PDF ثابتة عند 72 لكل بوصة، لكن حسابات الإخراج في Postext تجري بالبكسل؛ والدقة الأعلى تعطي تقسيمًا أدق للعناصر المقيسة بـmmأوcm.colors.model: 'cmyk'— يحفظ القصد بأن الألوان أُعدّت في فضاء CMYK. ولا تزال قيمةhexالاحتياطية هي المستخدمة فعلًا في الرسم إلى ملف PDF حاليًا؛ ويُوثَّقmodelهنا لأنه ينتقل إلى شجرة VDT لصالح الأدوات اللاحقة.{ pageNegative: true }فيRenderToPdfOptions— يعكس منطقة حدّ القص باستخدام نمط الدمج Difference (وتبقى علامات القص غير معكوسة). مفيد لفحوص ما قبل الطباعة (preflight) على الطباعة الداكنة فوق الفاتح.
#التنفيذ المرجعي
يربط مكوّن PdfViewport في Sandbox (packages/postext-sandbox/src/viewport/PdfViewport.tsx) القطع أعلاه في معاينة حية بأزرار لإعادة التوليد والتنزيل والطباعة، وهو نقطة انطلاق جيدة لأي دمج لـ PDF داخل المتصفح. يبني شجرة VDT عبر عامل الإخراج المشترك (انظر تشغيل الإخراج في عامل ويب)، فلا يجمّد النقر على إعادة الحساب الواجهةَ أثناء عمل المسار؛ ولا يتولى الخيط الرئيسي إلا renderToPdf (وهي سريعة أصلًا متى وُجدت شجرة VDT).
#كتاب ثلاثي الأبعاد (postext-folio)
تعرض postext-folio المستند المُخرَج على الشاشة كتابًا مطبوعًا مفتوحًا على مكتب: صفحات متقابلة وفق قاعدة الصفحة الفردية، وأوراق يقلبها القارئ بالزرّين ‹ ›، أو بمفاتيح الأسهم، أو بالسحب السريع، أو بالنقر على صفحة، أو بإمساك صفحة من حافتها وسحبها فوق الأخرى. تنثني كل ورقة في three.js وفق نوع ورقها، وتُلقي ظلًا حقيقيًا على الصفحات التي تحتها. ترسم لوحة WebGL الكتاب ساكنًا ومتقلّبًا على حد سواء، فلا يتغيّر مظهر الصفحة حين تستقر. وهي أداة العرض لوصفات دليل الوصفات ولتبويب Folio في Sandbox.
npm install postext postext-folio threeimport { buildDocument } from 'postext';
import { createFolioFromDocument } from 'postext-folio';
const doc = buildDocument({ markdown }, config);
const book = createFolioFromDocument(document.getElementById('book')!, doc, {
onChange: ({ pages }) => console.log('showing pages', pages),
});
// After an edit: the same viewer, on the same page.
book.setDocument(buildDocument({ markdown: edited }, config));- تُرسم الصفحات عند الحاجة إليها. ترسم
createFolioFromDocumentكل صفحة بـrenderPageToCanvasبعدد بكسلات الجهاز الدقيق لخانة الصفحة (فيعرضها WebGL بنقطة نسيج (texel) لكل بكسل، بحدة معاينة Canvas نفسها)، ولا ترسم إلا الصفحات المتقابلة المحيطة بالمفتوحة (window، ثلاث من كل جانب افتراضيًا). وتُحرَّر الصفحات التي تخرج من تلك النافذة، فلا يكلّف كتاب من ألف صفحة إلا ذاكرة بضع صفحات. والقفز إلى صفحة بعيدة يرسم تلك الصفحات المتقابلة أولًا. فإلى مسافة عشر صفحات تنقلب الأوراق واحدةً واحدة؛ وأبعد من ذلك ترتفع كتلة الصفحات الواقعة بينهما لوحًا واحدًا، بسماكة تلك الصفحات (مجموع سماكاتها)، وتستقر على الجانب الآخر. وتحتفظsetDocumentبرسم كل صفحة تبقى كما هي في الإخراج الجديد ({ repaint: true }يعيد رسمها كلها، بعد وصول صورة مثلًا). - المستند يحدّد الكتاب. تُفتح صفحته الأولى وحدها على اليمين حين تكون صفحة فردية (
pageIndexOffsetزوجي)، ويُعرض الكتاب المجلّد من اليمين (page.binding: 'right'، أو المستند العمودي) معكوسًا وتنقلب أوراقه نحو اليسار، وتأخذ الصفحات الفارغة لون خلفية الصفحة، ويضبط عرض قص الصفحة (pageWidthMm) مقياس سماكة الورق ولوحَي الغلاف. والفصل المُخرَج معcontinuationيحسب صفحات الكتاب الأخرى (pageIndexOffsetقبله، وbookPageCountبعده) في سماكة كتلتَي الصفحات دون أن يرسمها (extraPages). - المستند يحدّد المظهر. الورق والتجليد والمكتب والإضاءة هي إعدادات
folioفي المستند (doc.config.folio). والصفحة المنضَّدة داخل مقطع:::paperتحمل نوع ورقها الخاص (VDTPage.paper)، وتُرسم ورقتها بلون ذلك الورق وسطحه وسماكته وصلابته. ومعbinding.cover: 'pages'تكون الصفحة الأولى لوح الغلاف الأمامي، والأخيرة، حين تكون صفحة زوجية، لوح الغلاف الخلفي (covers). - الحاوية تحدّد الحجم. يملأ الكتاب الحاوية، مع الأزرار وعدد الصفحات في الهوامش، فأعطِ الحاوية ارتفاعًا؛ وتغيير الحجم يعيد رسم الصفحات بالحجم الجديد. وتحت عرض 560 px يعرض صفحة واحدة في كل مرة (
mode: 'auto'؛ و'single'و'double'يفرضان أحد الوضعين): يمتد الكعب على طول الحافة الداخلية للصفحة وتنقلب الورقة فوقه، والسحب نحو الكعب يقلب إلى الأمام، والسحب السريع بعيدًا عنه يعود إلى الخلف، والنقرة تقلب. - ما يفعله المؤشر. يحدّد
interaction(وsetInteractionلاحقًا) ما يفعله الزر الأيسر أو الإصبع الواحدة أو القلم على الكتاب:'hand'(الافتراضي) يمسك الصفحات ويقلبها، و'orbit'يدير المنظر كما يفعل السحب بالزر الأيمن (للوحات اللمس والأجهزة اللوحية)، و'select'يترك المؤشر للمضيف، لتحديد النص مثلًا. وتعطيpageAt(event)الصفحة الواقعة تحت المؤشر والموضع عليها ({ page, x, y }، كسورًا من الصفحة بدءًا من زاويتها العلوية اليسرى)، على الكتاب كما يُرى، مائلًا أو مُدارًا؛ وتسلكpointOnScreen(point)الاتجاه المعاكس، لرسم مؤشر الكتابة (caret) أو التحديد فوق الصفحة. وتعرضrefreshPage(src)من جديد لوحة صفحة أعاد المضيف رسمها في مكانها. ويستخدمها Sandbox كلها لتحديد النص وتتبّع مؤشر الكتابة في المحرر على الصفحات ثلاثية الأبعاد. - يستطيع القارئ أن يدور حوله. السحب بالزر الأيمن يدير المنظر حول الكتاب (حتى 70° بعيدًا عن المنظر العمودي من الأعلى)، حتى أثناء تقلّب الأوراق؛ وتعيده
resetView()تدريجيًا إلى قيمتَيtiltوyawفي الإعدادات، وتعطيgetView()المنظر كما يُرى الآن ({ tilt, yaw }، بالدرجات) لحفظه في تلك الإعدادات. ويضعها Sandbox على زرّين، إعادة ضبط العرض وحفظ بوصفه العرض الافتراضي. - الخطوط والصور أولًا. كما في
renderPage، يجب أن تكون الوجوه التي يستخدمها المستند محمّلة فيdocument.fonts، وصور موارده مسجّلة بـregisterResourceImageقبل رسم الصفحات. - ميسَّر الوصول. أداة العرض مجموعة قابلة للتركيز تستجيب لـ ←/→ (معكوستين في الكتاب المجلّد من اليمين)، وPage Up/Down، وHome وEnd؛ وأزرارها وعدد صفحاتها موسومة (ويترجمها
labels)، وتحمل كل لوحة صفحة نصًا بديلًاalt(alt: (index) => …). - دون WebGL2، أو حين يطلب القارئ تقليل الحركة، تتبدّل الصفحات المتقابلة ببساطة. كتب WebGL ثقيلة على الهاتف (نسيج لكل وجه من وجهَي الورقة): لذا لا يعرض Sandbox تبويب Folio إلا حيث يتوفر WebGL2 ولا يقل الضلع الأقصر للشاشة عن 600 px. وتخبرك
canFlip()هل ستنقلب الأوراق بثلاثة أبعاد هنا: WebGL2، ولا تقليل للحركة.
#المظهر
يتجاوز الخيار appearance، وsetAppearance لاحقًا، ما يقوله المستند. وكل ما يُترك يحتفظ بقيمة المستند:
const book = createFolioFromDocument(container, doc, {
appearance: {
folio: {
tilt: 22,
paper: { type: 'bookWove', texture: 'laid' },
binding: { type: 'hardcover', coverColor: { hex: '#5a1f1f', model: 'hex' } },
surface: { type: 'walnut' },
lighting: { environment: 'lamp' },
},
// Photographed desks: a folder laid out as postext.dev's /folio/textures/
// (manifest.json and one folder per desk). Without it, procedural maps.
textureBaseUrl: '/folio/textures',
// The picture for `folio.binding.spineImage`: the resource's URL,
// or a drawn canvas or image.
spineImage: spineUrl,
},
});
// A settings panel: the book redrawn in place, nothing painted again.
book.setAppearance({ folio: { ...folio, lighting: { environment: 'daylight' } } });
book.resetView();| الحقل | ما يفعله |
|---|---|
folio | إعدادات folio: الميل، والورق، والتجليد، والسطح، والإضاءة. تحل محل إعدادات المستند حين تُعطى. |
pageWidthMm | عرض قص الصفحة بالمليمتر، الذي تُقاس عليه سماكة الورق ولوحا الغلاف. من المستند: الصفحة المقصوصة بدقتها (dpi). الافتراضي 150 في createFolio. |
extraPages | : صفحات من الكتاب خارج الصفحات المعطاة، تُحسب في سماكة كتلتَي الصفحات ولا تُرسم أبدًا. |
covers | : الصفحة الأولى المعطاة هي الغلاف الأمامي، والأخيرة الغلاف الخلفي (حين تقع على صفحة زوجية). تنقلبان لوحَي غلاف، ولا يُرسم غلاف مقوّى حولهما. من المستند: binding.cover: 'pages' في كتاب يبدأ بصفحته الأولى وينتهي بصفحته الأخيرة. |
spineImage | الصورة المطبوعة على الكعب، عنوانَ URL أو لوحة canvas أو صورة. لا تبحث createFolioFromDocument عن الموارد: مرّر صورة المورد الذي يسمّيه folio.binding.spineImage. وتُتجاهل في التجليد بالدبّوس (saddle stitch). |
textureBaseUrl | المكان الذي تُقدَّم منه خامات المكتب المصوّرة. وإلى أن تُحمَّل، أو في غيابه، يُرسم المكتب بخرائط إجرائية. |
createFolio(container, { pages }) هي أداة العرض نفسها على أي صفحات: عناوين صور، أو عناصر <img> أو <canvas>، و"" لصفحة فارغة؛ ويمكن أن تكون الصفحة { src, alt, paper }، حيث paper نوع ورق على طريقة :::paper لتلك الورقة. وPageFlipper هو محرّك three.js وحده، لمضيف يبني بنفسه DOM الصفحات المتقابلة؛ وFlatPageFlipper هو قلب الصفحات المسطّح الذي سبق الكتاب ثلاثي الأبعاد، وأُبقي عليه لطاولة الضوء في دليل الوصفات. والقائمة الكاملة للخيارات في ملف README للحزمة.
#مثال حي: كتاب ثلاثي الأبعاد
يستورد هذا المثال على CodePen الحزمتين postext وpostext-folio من شبكة CDN، وينضّد مستندًا قصيرًا ويفتحه كتابًا. أمسك الصفحة اليمنى من حافتها واسحبها فوق الأخرى.
import { buildDocument } from 'https://esm.sh/postext';
import { createFolioFromDocument } from 'https://esm.sh/postext-folio';
const paragraph = `The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved. The light it gave was small, but it was enough to find the step.`;
// Thirty-six short sections: about ten pages to turn.
const markdown = ['# The Lantern']
.concat(Array.from({ length: 36 }, (_, i) => `## Evening ${i + 1}\n\n${paragraph} ${paragraph}\n\n${paragraph}`))
.join('\n\n');
const config = {
page: { sizePreset: '17x24', dpi: 150 },
layout: { layoutType: 'double' },
bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 10, unit: 'pt' } },
};
// Postext measures text with the fonts the browser has loaded,
// so wait for every face the document uses before laying it out.
await Promise.all([
document.fonts.load('16px "EB Garamond"'),
document.fonts.load('bold 16px "EB Garamond"'),
document.fonts.load('italic 16px "EB Garamond"'),
document.fonts.load('bold 16px "Open Sans"'), // the default heading face
]);
const doc = buildDocument({ markdown }, config);
const status = document.getElementById('status');
// The book: drag a page by its edge, click it, or use ← → and the buttons.
// Pages are painted at the size they are shown, around the open spread only.
createFolioFromDocument(document.getElementById('book'), doc, {
onChange: ({ pages }) => {
status.textContent = `${doc.pages.length} pages · open at ${pages.map((i) => i + 1).join('–')}`;
},
});
status.textContent = `${doc.pages.length} pages · drag a page by its edge to turn it`;index.html
<p id="status">Laying out…</p>
<div id="book"></div>style.css
body {
margin: 0;
font-family: system-ui, sans-serif;
color: #eee;
background: radial-gradient(ellipse 70% 75% at 50% 42%, #272b34 0%, #1a1d23 58%, #121418 100%);
min-height: 100vh;
}
#status {
margin: 12px 16px 0;
font-size: 14px;
opacity: 0.8;
}
/* The viewer fits the book into its container: give it a height. */
#book {
height: calc(100vh - 48px);
--postext-folio-accent: #f0b35a;
}يحمّل محررًا تفاعليًا من codepen.io. يستورد المثال أحدث إصدار من postext عبر شبكة توصيل المحتوى (CDN).
#مثال حي: صور الصفحات
createFolio مع صفحات مرسومة على لوحات canvas، وصفحة أخيرة فارغة، ولون الورق.
import { createFolio } from 'https://esm.sh/postext-folio';
// Any pages will do: image URLs, <img> or <canvas> elements, and "" for a
// blank page. Here, eight pages drawn on canvases.
function drawPage(n) {
const canvas = document.createElement('canvas');
canvas.width = 600;
canvas.height = 840;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fbf8f1';
ctx.fillRect(0, 0, 600, 840);
ctx.fillStyle = `hsl(${n * 45} 45% 45%)`;
ctx.fillRect(60, 80, 480, 320);
ctx.fillStyle = '#222';
ctx.font = 'bold 56px Georgia, serif';
ctx.fillText(`Plate ${n}`, 60, 480);
ctx.font = '22px Georgia, serif';
for (let line = 0; line < 8; line++) ctx.fillRect(60, 530 + line * 30, line === 7 ? 260 : 480, 3);
ctx.textAlign = 'center';
ctx.fillText(String(n), 300, 800);
return { src: canvas, alt: `Plate ${n}` };
}
const pages = Array.from({ length: 8 }, (_, i) => drawPage(i + 1));
// A blank page at the end, drawn as paper.
pages.push('');
const status = document.getElementById('status');
createFolio(document.getElementById('book'), {
pages,
firstPageRecto: true, // page 1 opens alone, on the right
binding: 'left', // 'right' lays a right-to-left book mirrored
paper: '#fbf8f1',
onChange: (state) => {
status.textContent = `Showing ${state.pages.map((i) => i + 1).join('–')} of ${pages.length}`;
},
});
status.textContent = 'Drag a page by its edge, click it, or use ← →';index.html
<p id="status">Drawing pages…</p>
<div id="book"></div>style.css
body {
margin: 0;
font-family: system-ui, sans-serif;
color: #eee;
background: radial-gradient(ellipse 70% 75% at 50% 42%, #272b34 0%, #1a1d23 58%, #121418 100%);
min-height: 100vh;
}
#status {
margin: 12px 16px 0;
font-size: 14px;
opacity: 0.8;
}
/* The viewer fits the book into its container: give it a height. */
#book {
height: calc(100vh - 48px);
--postext-folio-accent: #f0b35a;
}يحمّل محررًا تفاعليًا من codepen.io. يستورد المثال أحدث إصدار من postext عبر شبكة توصيل المحتوى (CDN).
#كتب EPUB (postext-epub)
تكتب postext-epub الكتاب المُخرَج ملفَّ EPUB 3.3، في المتصفح أو في Node، بلا خادم. وتقرأ مستندات الفصول نفسها التي تأخذها renderToPdf للكتاب، فتصل أرقام الصفحات والحواشي والاستشهادات والإحالات المرجعية وجدول المحتويات والفهرس الأبجدي محلولةً، وتعيد الملف بايتات. وهي الكاتب الذي يقف وراء تبويب EPUB 3 في Sandbox.
npm install postext postext-epubpostext تبعية نظيرة، كما في postext-pdf: حدّث الحزمتين معًا، وثبّتهما على شبكة CDN على الإصدار نفسه.
#التخطيط الثابت والتخطيط المتدفق
يعرّف EPUB 3 نسختَي عرض (renditions)، تُحدَّدان بالخاصية rendition:layout في ملف الحزمة؛ ويختار layout إحداهما:
layout: 'fixed' | layout: 'reflowable' | |
|---|---|---|
| الاسم في EPUB | pre-paginated (تخطيط ثابت، FXL) | reflowable، القيمة الافتراضية في EPUB |
| مستندات المحتوى | مستند XHTML لكل صفحة مطبوعة، بمقاس الصفحة المقصوصة بوحدات px في CSS | مستند XHTML لكل فصل (والجزء يفتتح مستندًا خاصًا به) |
| ما يحتفظ به | الصفحة: الأعمدة، والعناصر العائمة، والترويسات، وصفحات الافتتاح، وفواصل الأسطر ومواضعها، بالخطوط المضمَّنة. ويبقى النص نصًا حقيقيًا: قابلًا للتحديد والبحث والقراءة بصوت عالٍ | النص وبنيته: العناوين، والفقرات مُعادًا بناؤها من الأسطر، والقوائم، والإطارات في هيئة عناصر aside، والأشكال والجداول بعد النص الذي يستشهد بها، والحواشي، والروابط، وعلامات صفحات الطبعة الورقية. وورقة أنماط مشتقة من الإعدادات |
| ما يتخلى عنه | اختيار القارئ للخط وحجمه والهوامش؛ وعلى الشاشة الصغيرة تُصغَّر الصفحة | الأعمدة، والترويسات، وتصميم الصفحات، وفواصل الأسطر الدقيقة |
| الصفحات المتقابلة والاتجاه | page-spread-left / page-spread-right وفق زوجية الصفحة والتجليد؛ والكتاب المجلّد من اليمين يُقرأ من اليمين إلى اليسار | اتجاه القراءة من التجليد؛ يحتفظ النص الصيني العمودي بـ vertical-rl، والنص العربي dir="rtl" |
| يناسب | الصفحات المصمَّمة: الكتب المصوّرة، والكتب المدرسية، والكتالوجات، والمجلات؛ والشاشات الكبيرة | النص المتصل: الروايات، والمقالات، والتقارير؛ والهواتف وأجهزة الحبر الإلكتروني |
تحمل نسختا العرض التنقل نفسه: جدول محتويات من العناوين وصفحات الأجزاء، وقائمة صفحات بأرقام الصفحات المطبوعة، ومعالم (landmarks) هي الغلاف وجدول المحتويات المطبوع وبداية المتن، وملف NCX لأنظمة القراءة الأقدم.
#كتابة كتاب
import { openBundle, buildBundle } from 'postext';
import { renderToEpub } from 'postext-epub';
const bundle = await openBundle(fileBytes);
const docs = buildBundle(bundle); // one VDTDocument per chapter, in book order
const bytes = await renderToEpub(docs, {
layout: 'reflowable',
metadata: { title: 'Lantern', creators: ['Ada Lovelace'], language: 'en' },
fonts: bundle.fonts.map((f) => ({ family: f.family, weight: f.weight, style: f.style, bytes: new Uint8Array(f.bytes), format: f.format })),
resourceBytes: (fileId) => {
const data = bundle.files.get(fileId);
return data ? { bytes: data, mediaType: '' } : undefined;
},
onWarning: (w) => console.warn(w),
});المستند الواحد كتاب من فصل واحد: renderToEpub([doc], options).
renderToEpub(docs, options): Promise<Uint8Array>تكتب الملف. وoptionsهي{ layout, metadata, fonts?, resourceBytes?, cover?, onProgress?, onWarning?, signal? }.metadata: الحقلانtitleوlanguage(وسم BCP 47) مطلوبان؛ وsubtitleوcreatorsوidentifierوdateوpublisherوrightsوdescriptionوmodifiedاختيارية. ويصبح رقم ISBN المجرّدurn:isbn:…. ودونidentifierيحصل الكتاب علىurn:uuid:مشتق من عنوانه ومؤلفيه ولغته، فتحتفظ النسخة الجديدة من الكتاب نفسه بمكانها في مكتبة القارئ. ومرّرmodifiedأيضًا لتحصل على مخرجات متطابقة بايتًا ببايت.fonts: الوجوه المراد تضمينها،{ family, weight, style, bytes, format, unicodeRange? }حيثformatواحد منwoff2وwoffوttfوotf. يصبح كل وجه ملفًا وقاعدة@font-face؛ وعدة ملفات لكل منهاunicodeRangeتشكّل وجهًا واحدًا (شرائح Google Fonts). والعائلة أو الوزن أو النمط الذي تستخدمه الصفحات دون وجه مضمَّن يُبلَّغ عنه بتحذيرmissingFont، وتستبدل به أنظمة القراءة خطوطها الخاصة. ولا تضمّن إلا الخطوط التي تسمح تراخيصها بذلك.resourceBytes(fileId): الصور التي تضعها الصفحات، متزامنةً أو غير متزامنة، في هيئة{ bytes, mediaType }؛ وmediaTypeالفارغ يُستنتج من البايتات. أعطِ الصور النقطية كما خُزّنت، وملفات SVG بمصدرها، لا بنسخة PDF الأصلية للطباعة (svg.pdfFileId). تُخزَّن كل صورة مرة واحدة. والكتاب أحادي الحبر (diagramStyle.singleInk) يُعاد تلوين ملفات SVG فيه داخل الملف. والصورة التي لا بايتات لها يُبلَّغ عنها بتحذيرmissingImageوتُترك إطارًا فارغًا.cover:{ bytes, mediaType, alt? }، صورة JPEG أو PNG أو WebP أو SVG. يُفتح الكتاب حينئذ على مستند غلاف يحملها، وتكون هيcover-imageالخاصة بالحزمة (الصورة المصغّرة في المكتبة). ودونها، يسمّي التخطيط الثابت صفحته الأولى غلافًا، ولا تكون للكتاب المتدفق صورة غلاف.onProgress({ phase, done, total }):resources(الخطوط والصور)، ثمdocuments(الصفحات في التخطيط الثابت، والفصول في المتدفق)، ثمpackage. و**signal** يُلغي العملية بين الخطوات.readEpub(bytes)تقرأ الملف من جديد لأداة عرض، دونDOMParser: التخطيط، والبيانات الوصفية، واتجاه القراءة، وكل ملف بمساره، والبيان (manifest)، والعمود الفقري (spine)، وجدول المحتويات، وقائمة الصفحات، ونافذة العرض (viewport) للتخطيط الثابت، والغلاف. وقارئ Sandbox مبني عليها.
تحمل نسختا العرض كلتاهما بيانات EPUB Accessibility 1.1 الوصفية (أنماط الوصول، والميزات مثل جدول المحتويات وأرقام الصفحات المطبوعة، والمخاطر، وملخصًا)، ولا تدّعيان افتراضيًا أي مطابقة لمعايير WCAG. والقائمة الكاملة للخيارات والقيود في ملف README للحزمة.
#فحص ملف باستخدام EPUBCheck
W3C EPUBCheck هو أداة التحقق المرجعية لملفات EPUB؛ وبه تفحص متاجر الكتب الإلكترونية الملفات التي تتلقاها. بعد تثبيته (brew install epubcheck، أو إصدار Java)، يسرد epubcheck book.epub الأخطاء والتحذيرات وملاحظات الاستخدام؛ وملاحظات الاستخدام التي تتركها مخرجات Postext (CSS-028، OBS-001، HTM_062) معلوماتية فقط. وفي مستودع Postext، يفحص pnpm --filter postext-epub epubcheck كتب العينات في مجموعة الاختبارات، وينضّد node packages/postext-epub/scripts/epubcheck.mjs book.postext --layout both ملف .postext واحدًا أو مجلد إعداد مسبق ويفحص نسختَي العرض كلتيهما، ويشغّل pnpm --filter postext-epub validate المصفوفة الكاملة من الكتب (دليل Postext، والإعدادات المسبقة للعرض، والكتب الصينية والعربية، وكتب دليل الوصفات)، وكل منها يجتاز الفحص بلا أخطاء ولا تحذيرات.
#الحزم (ملفات .postext)
ملف .postext كتابٌ كامل في ملف واحد: أرشيف zip يضم ملف بيان (manifest) اسمه preset.json، وملف Markdown لكل فصل، وملفات الموارد (صور نقطية، وملفات SVG، ونسخ PDF الأصلية للطباعة (print masters))، وملفات الخطوط التي تسمّيها الإعدادات. يصدّره Sandbox ويستورده، وتسلّمه مهارة الوكيل (agent skill). وتستطيع مكتبة postext أيضًا إنشاءه وفتحه، فينتقل الكتاب بين هذه الأدوات وبرنامجك دون أن يفقد شيئًا.
my-book.postext
├── preset.json manifest: name, locale, chapters, config, resources, fonts
├── chapters/01-dusk.md
├── chapters/02-night.md
├── resources/lantern.svg
└── fonts/ebgaramond-400-normal.woff2
يصف ملحق صيغة حزمة الإعداد المسبق في توثيق Sandbox البيانَ حقلًا حقلًا. وقد يحمل الملف أيضًا layouts.json، أي أعداد الصفحات التي حسبها Sandbox، فيُفتح الكتاب هناك مقسَّمًا إلى صفحات من البداية، أو ملفًا لكل طبعة من كتاب متعدد اللغات (layouts.zh-Hant.json، ويُقرأ أولًا). ويتجاهلها openBundle.
تُصدَّر الواجهة البرمجية من postext نفسها ومن المسار الفرعي postext/bundle، الذي يضيف الأدوات المساعدة منخفضة المستوى. استورد من postext إذا كنت سترسم أيضًا. بهذا تتشارك محوّلات الحزمة (adapters) والمُخرِجات نسخة واحدة من الوحدة، وهذا مهم على شبكة توزيع محتوى (CDN) مثل esm.sh، حيث تُبنى كل نقطة دخول على حدة.
#فتح حزمة
تأخذ openBundle بايتات الملف (Uint8Array أو ArrayBuffer أو Blob / File من <input type="file">) وتعيد كل ما يحتاجه المحرّك ومُخرِجاته (backends):
import { openBundle } from 'postext';
const bundle = await openBundle(await file.arrayBuffer(), { locale: 'es' });
bundle.chapters; // [{ title, file, markdown }, …] in book order
bundle.config; // PostextConfig, ready for buildDocument
bundle.resources; // Resource[]
bundle.files; // Map<path, Uint8Array>: every file of the bundle| الحقل | ما يحتويه |
|---|---|
manifest | ملف preset.json بعد التحقق من صحته. |
id، name، description | من البيان. |
locale، locales | اللغة التي قُرئ بها المحتوى، وكل لغة تحملها الحزمة الثنائية اللغة. يختار options.locale إحداها: الوسم المطابق تمامًا أولًا، ثم اللغة دون متغيرها الإقليمي، ثم لغة الحزمة نفسها. |
chapters | { title, file, markdown } لكل فصل. الفصل الذي لا عنوان له في البيان يأخذ نص أول عنوان # فيه. |
config | لوحة الألوان الافتراضية وأنواع الموارد بلغة الحزمة، ثم config الموجود في البيان، ثم تجاوزات اللغة. لغة الحزمة هي locale أعلاه، فالحزمة المكتوبة بلغة واحدة تنال تسمياتها الخاصة أيًّا كان ما يطلبه options.locale. والبيان الذي لا يسمّي لغة يأخذ اللغة التي يضبطها config الخاص به (locale، ثم لغة تقسيم الكلمات بالواصلة)، وإلا فـoptions.locale. ويسرد customFonts عائلات الخطوط التي في الحزمة. هذه هي الإعدادات نفسها التي يفتح بها Sandbox الحزمة. |
resources | الموارد، مع تعليقات اللغة المختارة. المقاس الناقص من البيان يُقرأ من الملف. |
fonts | مُدخل واحد لكل وجه خط: { family, weight, style, format, file, bytes }. |
files | كل ملف في الأرشيف، ومفتاحه مساره. |
thumbnail، canvasScope | مسار صورة الغلاف، وطريقة العرض التي تطلبها الحزمة: الحقل view في البيان، وفوقه localized[…].view الخاص باللغة المقدَّمة. |
warnings | مشكلات لا توقف القراءة: ملف خط غير مدعوم، أو نسخة طباعة أصلية مفقودة. |
معرّف fileId لأي ملف هو مساره داخل الحزمة. يمكن البحث عن resource.svg.fileId وresource.bitmap.fileId وfileId الخاص بكل متغير في customFonts مباشرةً في bundle.files. ترمي openBundle خطأً إذا لم تكن البايتات أرشيف zip، أو لم يوجد preset.json صالح (في الجذر أو تحت مجلد واحد في المستوى الأعلى)، أو كان ملف يسمّيه البيان مفقودًا.
الحزم التي كتبها postext 1.4 أو ما قبله
كل بيان يكتبه createBundle أو Sandbox يحمل configVersion: 8: قواعد الإعدادات التي كُتب لها config الخاص به. والبيان الذي يخلو منه كتبه postext 1.4 أو ما قبله، وكان يضبط ثلاثة عشر أمرًا على نحو مختلف:
- فواصل العناوين (القواعد 3): حتى 1.4، كان كائن
headingsالذي لا فاصل فيه لـH1 بلا أي فاصل (انظر التجاوزات حسب المستوى). - حجم الصيغ الرياضية (القواعد 4): حتى 1.4، كانت الصيغ تخرج أكبر بـ1.131 مرة مما يقوله
fontSizeScale(انظر حجم الصيغ). - الفراغ تحت المورد المضمَّن (القواعد 5): حتى 1.4، كان النص الذي يلي شكلًا أو جدولًا موضعه
placement.position: 'here'يُستأنف عند خط الشبكة التالي، دون فراغ العناصر العائمة تحته (انظرlayout.inlineResourceGapفي تخطيط الصفحة). - الفراغ حول المورد المضمَّن داخل إطار (القواعد 6): حتى 1.4، كان مثل هذا المورد يلتصق بنص الإطار المحيط به (انظر
layout.inlineResourceGapInBoxesفي تخطيط الصفحة). - العلامات المضمَّنة في العناوين (القواعد 6): حتى 1.4، كان العنوان يطبع كلمات
*italic*و**bold**وسائر علاماته بنمطه العادي الخاص (انظرheadings.inlineMarksفي العناوين). - حجم الحرف الاستهلالي (القواعد 6): حتى 1.4، كان
dropCapفي نص تصميمي بلاfontSizeبطول جميع صناديق الأسطر التي يمتد عليها، وأعلاه فوق السطر الأول (انظرdropCapفي عناصر النص). - المساحة تحت سطر النقطتين (القواعد 6): حتى 1.4، كان
keepColonWithListيعدّ سطرًا واحدًا من المساحة تحت السطر المنتهي بنقطتين كافيًا للقائمة، فينتقل البند الأول ذو السطرين، الذي تُبقيه قواعد اليتيمة والأرملة كاملًا، إلى العمود التالي من دونه (انظرbodyText.colonListRoom). - الأسطر التي يتركها قطع الإطار (القواعد 6): حتى 1.4، كان الإطار الذي ينقسم داخل فقرة أو بند قائمة قد يترك سطرًا واحدًا منها على أحد الجانبين، ما دام كل جانب من الإطار يضم أسطره
splitMinLinesإجمالًا (انظرlayout.boxChildSplitMinLinesفي تخطيط الصفحة). - فواصل الأسطر عند الشرطة (القواعد 7): حتى 1.4، لم يكن Knuth-Plass ينهي سطرًا أبدًا بعد شرطة طويلة أو متوسطة ملتصقة بين كلمتين (
say—that’s)، ولم يكن مقسِّم الأسطر سطرًا بسطر للنص المنسَّق ينهيه إلا بين حرفين (انظرbodyText.breakAfterDashesفي النص الأساسي). - النص غير المضبوط (القواعد 7): حتى 1.4، كان النص الجاري غير المضبوط يُنضَّد سطرًا بسطر، فيُملأ كل سطر قبل الذي يليه، أيًّا كانت قيمة
optimalLineBreaking(انظرbodyText.optimalRaggedفي النص الأساسي). - التقسيم تحت العنوان (القواعد 8): حتى 1.4، كانت الفقرة التي تلي عنوانًا في أسفل عمود تُبقي هناك كل ما يتسع من أسطرها، مهما قلّ ما ينتقل منها إلى العمود التالي (انظر
headings.keepWithNextSplitفي العناوين). - الفراغ تحت حاوية
:::paragraphs(القواعد 8): حتى 1.4، كان فراغ النمط يوضع تحت الفقرة الأخيرة قبل المحاذاة إلى الشبكة، ويُضاف تحته فراغ الكتلة التالية من أعلاها (marginTopالخاص بالعنوان)، ويُهمَل تباعد الفقرات في النص (انظرbodyText.paragraphContainerSpacingفي النص الأساسي). - فواصل الأسطر عند واصلة الكلمة المركبة (القواعد 8): حتى 1.4، لم يكن Knuth-Plass ينهي سطرًا مضبوطًا أبدًا بعد واصلة بين حرفين (
well-known) في فقرة بلا تنسيق مضمَّن، بينما كان يفعل ذلك في فقرة فيها تنسيق (انظرbodyText.breakAfterHyphensفي النص الأساسي).
يقرأ openBundle وreadBundle الحقل config في بيان كهذا، وإعدادات كل لغة في localized، عبر migrateConfig، التي تكتب صراحةً فواصل العناوين كما أخرجها الإصدار 1.4، وتضرب مقياس الصيغ في 1.131 (وتقسم عليه هوامش الصيغ المعروضة المعبَّر عنها بوحدة em). أما البيان الموسوم بقيمة من 3 إلى 7، الذي كتبه إصدار تجريبي من 1.5، فلا يتلقى إلا تثبيتات (pins) القواعد اللاحقة لوسمه. مع 3 يعني ذلك حجم الصيغ، والفراغ تحت المورد المضمَّن، وتثبيتات القواعد 6 الخمسة، وتثبيتَي القواعد 7، وتثبيتات القواعد 8 الثلاثة؛ ومع 4، الفراغ تحت المورد المضمَّن وتثبيتات القواعد 6 و7 و8؛ ومع 5، تثبيتات القواعد 6 و7 و8؛ ومع 6، تثبيتات القواعد 7 و8؛ ومع 7، تثبيتات القواعد 8 وحدها. يُكتب التقسيم تحت العنوان (pinLegacyHeadingSplit) على شكل headings.keepWithNextSplit: 'fill' في كائن headings الذي تُبقيه الطبقات ساريًا، إذا كان في أحد الفصول المقروءة عنوان، ولم تحدد الإعدادات قيمة خاصة بها، وأبقت headings.keepWithNext مفعَّلًا، ولم تعطّل bodyText.avoidOrphans. وتُكتب فواصل الكلمات المركبة (pinLegacyHyphenBreaks) على شكل bodyText.breakAfterHyphens: false في bodyText الساري، إذا وضع أحد الفصول المقروءة واصلة بين حرفين، ولم تكن الإعدادات تضبطه أصلًا ولا تعطّل optimalLineBreaking. ويُكتب الفراغ تحت الحاويات (pinLegacyParagraphContainerSpacing) على شكل bodyText.paragraphContainerSpacing: 'add' في bodyText الساري، إذا عرّفت الإعدادات نمط فقرة (في paragraphStyles أو في تجاوزات عارض HTML)، وفتح أحد الفصول المقروءة حاوية :::paragraphs في سطر مستقل، ولم تكن الإعدادات تضبطه أصلًا. وتُكتب فواصل الشرطات (pinLegacyDashBreaks) على شكل bodyText.breakAfterDashes: false في bodyText الساري، إذا وضع أحد الفصول المقروءة شرطة طويلة أو متوسطة ملتصقة بين كلمتين (قبلها حرف أو رقم أو علامة ترقيم إغلاق، وبعدها حرف أو رقم أو قوس فتح أو علامة اقتباس فتح؛ وتُحتسب علامة الاقتباس التي قبلها إذا سبقها حرف أو رقم أو علامة ترقيم إغلاق أو مسافة غير قابلة للكسر، كما في "no"—and، لا في said "—Hola؛ وتُحتسب علامة التنسيق المضمَّنة الملاصقة للشرطة أو لعلامة الاقتباس، مثل ** في **riddles.**—I، من أي الجانبين)، ولم تكن الإعدادات تضبطه أصلًا. ويُكتب تقسيم الأسطر غير المضبوطة (pinLegacyRaggedBreaking) على شكل bodyText.optimalRagged: false في bodyText الساري، إذا جعلت الإعدادات بعض النص الجاري غير مضبوط (قيمة textAlign غير 'justify' على النص الأساسي، أو على نمط فقرة، أو متن إطار، أو متن جزء، أو متن نمط قسم (headingStyles[].bodyStyle)، أو في تجاوزات عارض HTML)، ولم تكن تضبطه أصلًا، ولا تعطّل optimalLineBreaking. ويُكتب الفراغ في الأطر (pinLegacyBoxResourceGap) على شكل layout.inlineResourceGapInBoxes: false في layout الساري، إذا ضُمِّن مورد في سطر مستقل داخل :::callout في الفصول المقروءة، ولم تكن الإعدادات تضبطه أصلًا. ويُكتب قطع الإطار (pinLegacyBoxChildCut) على شكل layout.boxChildSplitMinLines: 1 في layout الساري، إذا فتح أحد الفصول المقروءة :::callout في سطر مستقل، ولم تكن الإعدادات تضبطه أصلًا. وتُكتب علامات العناوين (pinLegacyHeadingMarks) على شكل headings.inlineMarks: false في headings الذي تُبقيه الطبقات ساريًا، إذا حمل عنوانٌ في الفصول المقروءة علامةً (* أو _ أو ^ أو ~ أو :smallcaps[ أو رابطًا في نصه) ولم تحدد الإعدادات قيمة خاصة بها. أما الحروف الاستهلالية (pinLegacyDropCapSize) فيُكتب حجمها في 1.4 صراحةً على شكل dropCap.fontSize، أينما كانت: بوحدة تباعد أسطر العنصر إن كان طولًا، وإلا فبوحدة حجم خطه. وتُكتب المساحة تحت سطر النقطتين (pinLegacyColonListRoom) على شكل bodyText.colonListRoom: 'line' في bodyText الساري، إذا جاءت قائمة في الفصول المقروءة بعد سطر ينتهي بنقطتين (ويُسمح بأسطر فارغة بينهما)، ولم تحدد الإعدادات مساحة ولا عطّلت keepColonWithList. ويُكتب الفراغ تحت المورد المضمَّن (pinLegacyInlineGap) على شكل layout.inlineResourceGap: 'above' في layout الذي تُبقيه الطبقات ساريًا، إذا ضمَّن سطرٌ من الفصول المقروءة موردًا (::resource{id="…"} وحده في سطره، كما يقرؤه المحلِّل: ذكره في النص الجاري أو في مقطع شيفرة لا يُحتسب) ولم تحدد الإعدادات فراغًا خاصًا بها. ويُثبَّت الحجم على math الذي تُبقيه الطبقات ساريًا (إذ يحلّ math الخاص بلغةٍ ما محلّ المشترك)، ولا يُثبَّت إلا إذا كان في الفصول المقروءة الرمز $: فالحزمة التي لا صيغ فيها تحتفظ بـconfig كما كُتب. وإذا لم يحدد البيان ولا اللغة math، فالساري هو baseConfig الخاص بـreadBundle (إعدادات القارئ نفسه)، ويُثبَّت هو أيضًا، لأن 1.4 كان ينضّد صيغ الحزمة بذلك الحجم: فالقيمة الأساسية fontSizeScale: 1.5 تُقرأ 1.5 × 1.1312. أما فواصل العناوين في الإعدادات الأساسية فتؤخذ كما هي. وهكذا تحتفظ الحزمة القديمة بما أخرجته هذه القواعد، ويُظهر bundle.config ما تُخرَج به من فواصل، وحجم صيغ، وفراغات، وعلامات عناوين، وأحجام حروف استهلالية، ومساحة تحت سطر النقطتين، وقطع إطار، وفواصل شرطات، وفواصل كلمات مركبة، وتقسيم للأسطر غير المضبوطة، وتقسيم تحت العنوان، وفراغ تحت الحاويات. أما إصلاحات الإخراج في 1.5 فلا تثبيت لها، وتسري عليها كما تسري على أي كتاب، لذا قد تتحرك صفحة تمسّها (انظر حجم الصيغ للاطلاع على القائمة). ملف preset.json المكتوب يدويًا لقواعد اليوم يضبط "configVersion": 8؛ ووسم بيان حزمة قديمة بهذه القيمة هو أيضًا الطريقة ذات السطر الواحد لقراءتها بقواعد اليوم (وعندها تفقد الحزمة التي لا إصدار لها تثبيت فواصل العناوين أيضًا).
import { CONFIG_VERSION, migrateConfig } from 'postext/bundle';
migrateConfig({ headings: { fontFamily: 'Georgia' } }, undefined, { content: 'A book with no maths.' });
// => { headings: { fontFamily: 'Georgia', levels: [{ level: 1, breakBefore: { enabled: false } }] } }
migrateConfig({ math: { fontSizeScale: 1.2 } }, 3);
// => { math: { fontSizeScale: 1.35746…, marginTop: { value: 0.7072, unit: 'em' }, marginBottom: { value: 0.7072, unit: 'em' } },
// layout: { inlineResourceGap: 'above', inlineResourceGapInBoxes: false, boxChildSplitMinLines: 1 },
// headings: { inlineMarks: false, keepWithNextSplit: 'fill' }, bodyText: { colonListRoom: 'line', breakAfterDashes: false, breakAfterHyphens: false } }
migrateConfig({ layout: { layoutType: 'single' } }, 4, { content: 'Text.\n\n::resource{id="fig"}' });
// => { layout: { layoutType: 'single', inlineResourceGap: 'above' } }
migrateConfig({ layout: { layoutType: 'single' } }, 5, { content: ':::callout\nText.\n\n::resource{id="fig"}\n:::' });
// => { layout: { layoutType: 'single', inlineResourceGapInBoxes: false, boxChildSplitMinLines: 1 } }
migrateConfig({ bodyText: { textAlign: 'left' } }, 6, { content: 'I say—that is all.' });
// => { bodyText: { textAlign: 'left', breakAfterDashes: false, optimalRagged: false } }
migrateConfig({ paragraphStyles: [{ id: 'verse' }] }, 7, { content: ':::paragraphs{style="verse"}\nA line.\n:::' });
// => { paragraphStyles: [{ id: 'verse' }], bodyText: { paragraphContainerSpacing: 'add' } }
migrateConfig({ bodyText: { fontFamily: 'Georgia' } }, 7, { content: 'A well-known tale.' });
// => { bodyText: { fontFamily: 'Georgia', breakAfterHyphens: false } }
migrateConfig(config, CONFIG_VERSION); // today's rules: `config` itselfcontent هو نص Markdown الذي تُخرجه الإعدادات (سلسلة نصية أو قائمة فصول). من دونه يُثبَّت حجم الصيغ كلما كانت الرياضيات مفعَّلة، والفراغ تحت الحاويات كلما عرّفت الإعدادات نمط فقرة، أما الفراغان وعلامات العناوين والمساحة تحت سطر النقطتين وقطع الإطار وفواصل الشرطات والتقسيم تحت العنوان وفواصل الكلمات المركبة فتُثبَّت دائمًا، إذ لا يستطيع المحرّك أن يعرف هل في الكتاب صيغة، أو حاوية :::paragraphs، أو شكل مضمَّن، أو عنوان فيه علامة، أو قائمة تمهّد لها نقطتان، أو إطار، أو شرطة ملتصقة، أو عنوان، أو كلمة مركبة. أما تقسيم الأسطر غير المضبوطة فيُثبَّت بناءً على الإعدادات وحدها، سواء وُجد المحتوى أم لا. رحِّل الإعدادات المخزنة مرة واحدة وخزّنها من جديد تحت CONFIG_VERSION: فتثبيت الصيغ يضرب المقياس، والإعدادات التي تُرحَّل مرتين تكبر مرتين.
#إخراج حزمة ورسمها
أربع أدوات مساعدة تصل الحزمة المفتوحة بالمحرّك وبالمُخرِجات:
loadBundleFonts(bundle)يسجّل أوجه خطوط الحزمة فيdocument.fonts. انتظر اكتماله (await) قبل الإخراج، لأن الإخراج يقيس النص بالخطوط المتاحة في المتصفح. أما العائلات التي تسمّيها الحزمة ولا تحملها (Google Fonts) فعليك تحميلها بنفسك، كما في أي مستند آخر.registerBundleImages(bundle)يفكّ ترميز الصور لمُخرِج Canvas (renderPageوrenderToCanvas). و**bundleImageUrl(bundle)** هو المحلِّلresourceImageUrlالذي يحتاجهrenderToHtml. وكلاهما يعيد تلوين أشكال SVG حين يكونdiagramStyle.singleInkمفعَّلًا، مرة واحدة: يعيدان تلوين الشيفرة ويعلّمان الصور كي لا يلوّنها أي مُخرِج مرة أخرى (انظر الحبر الواحد على Canvas وفي HTML).buildBundle(bundle)يُخرج الفصول بالترتيب ويعيدVDTDocumentلكل فصل. كل فصل يتابع ما قبله: عدّادات العناوين والموارد، والجزء المفتوح، وزوجية الصفحة، وترقيم الصفحات. والفصل الذي يطبع المحتويات (:::toc) أو الفهرس الأبجدي (:::index) يتلقى مخطط الكتاب كله. ويقبل الخيارات نفسها التي يقبلهاbuildDocument، إضافةً إلىconfigلتجاوز إعدادات الحزمة، وcacheلمشاركة ذاكرة تخزين مؤقت للقياسات، وmetadata(انظر أدناه).bundleResourceBytes(bundle)و**bundleFontProvider(bundle, { decodeWoff2, fallback })** هما الخيارانresourceBytesوfontProviderللدالةrenderToPdfفيpostext-pdf. يختار مزوّد الخطوط من الحزمة أقرب وزن للنمط المطلوب. ويحتاج لوجه.woff2إلىdecompressWoff2، وللعائلة التي لا تحملها الحزمة يستدعيfallbackبمعاملات المُخرِج، بما فيهاrequest، ويمرّر ما تعيده كما هو. والبديل الذي لا يجلب إلا ملفlatinللعائلة يطبع العائلة الصينية التي لا تضمّنها الحزمة مربعاتٍ فارغة؛ أما البديل الذي يجيب بشرائح، مثلsliceFontProviderفي خطوط الصينية واليابانية والكورية، فيطبعها كاملة.
import { openBundle, loadBundleFonts, registerBundleImages, buildBundle, renderPage,
bundleResourceBytes, bundleFontProvider } from 'postext';
import { renderToPdf, decompressWoff2 } from 'postext-pdf';
const bundle = await openBundle(bytes);
await loadBundleFonts(bundle);
await registerBundleImages(bundle);
const docs = buildBundle(bundle); // one VDTDocument per chapter
const firstPage = renderPage(docs[0].pages[0], docs[0]); // a <canvas>
const pdf = await renderToPdf(docs, { // the whole book
fontProvider: bundleFontProvider(bundle, { decodeWoff2: decompressWoff2, fallback: fontsource }),
resourceBytes: bundleResourceBytes(bundle),
});لإخراج فصل واحد بنفسك، مرّر bundle.chapters[i].markdown وbundle.resources وbundle.config إلى buildDocument، كما تفعل مع أي مستند.
البيانات الوصفية للكتاب. كما في Sandbox، البيانات التمهيدية (front matter) للفصل الأول هي بيانات الكتاب: يسلّم buildBundle قيم title وauthor وسائرها إلى كل فصل، فتصحّ الترويسات {title} و{author} على كل صفحة، ويحملها doc.metadata في كل فصل. وتُتجاهَل كتلة البيانات التمهيدية في أعلى فصل لاحق: يُتعرَّف عليها بسطرَي --- وتُمحى دون تحليلها، فلا يضرّ YAML قد يرفضه المحلِّل. ويوفّر options.metadata القيم التي لا تضبطها البيانات التمهيدية (والغلبة للبيانات التمهيدية). ويصل عدد صفحات الكتاب إلى كل فصل أيضًا: يطبعه {bookTotalPages}، بينما يعدّ {totalPages} صفحات الفصل (انظر عدد صفحات الكتاب).
const docs = buildBundle(bundle, { metadata: { author: 'A. Author' } });
docs[3].metadata.title; // the first chapter's `title:`#مثال حي: فتح حزمة
يحمّل هذا المثال على CodePen كتابًا نموذجيًا من فصلين (lantern.postext، بخطه الخاص وشكل SVG وجدول) من المستودع. ويسجّل خطوط الحزمة وصورها، ويُخرج الكتاب بـbuildBundle ويرسم كل صفحة. ويُنتج زر Make the PDF المستندات نفسها بـpostext-pdf، مضمِّنًا خطوط الحزمة. اختر ملف .postext خاصًا بك، مصدَّرًا من Sandbox مثلًا، لتراه بالطريقة نفسها.
import {
openBundle,
loadBundleFonts,
registerBundleImages,
buildBundle,
bundleResourceBytes,
bundleFontProvider,
renderPage,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';
// A two-chapter book with its own typeface, an SVG figure and a table.
const SAMPLE = 'https://cdn.jsdelivr.net/gh/drnachio/postext@main/docs/examples/open-bundle/lantern.postext';
const status = document.getElementById('status');
const pdfButton = document.getElementById('pdf');
let current = null;
async function show(data) {
// Chapters, config (fonts wired to the bundle's own files), resources and
// every file, keyed by its path inside the bundle.
const bundle = await openBundle(data);
// Layout measures text with the fonts the browser has: register the
// bundle's faces, and load the Google Fonts it names but does not carry
// (the default running heads use Open Sans; see the pen's CSS).
await loadBundleFonts(bundle);
await document.fonts.load('600 16px "Open Sans"');
await registerBundleImages(bundle);
// One VDTDocument per chapter, each continuing the one before it.
const docs = buildBundle(bundle);
const pages = docs.flatMap((doc) => doc.pages.map((page) => renderPage(page, doc)));
document.getElementById('pages').replaceChildren(...pages);
status.textContent = `${bundle.name} · ${bundle.chapters.length} chapter(s) · ${pages.length} page(s)`
+ (bundle.warnings.length ? ` · ${bundle.warnings.length} warning(s)` : '');
current = { bundle, docs };
pdfButton.disabled = false;
document.getElementById('links').hidden = true;
}
// Fonts the bundle does not carry come from Fontsource.
async function fontsource(family, weight, style) {
const id = family.toLowerCase().replace(/\s+/g, '-');
const res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@latest/files/${id}-latin-${weight}-${style}.woff2`);
if (!res.ok) throw new Error(`font fetch failed: ${res.status} ${family}`);
return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
}
pdfButton.addEventListener('click', async () => {
pdfButton.disabled = true;
status.textContent = 'Rendering the PDF…';
const { bundle, docs } = current;
const bytes = await renderToPdf(docs, {
fontProvider: bundleFontProvider(bundle, { decodeWoff2: decompressWoff2, fallback: fontsource }),
resourceBytes: bundleResourceBytes(bundle),
});
const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' }));
document.getElementById('open').href = url;
document.getElementById('download').href = url;
document.getElementById('links').hidden = false;
status.textContent = `${bundle.name} · ${(bytes.length / 1024).toFixed(0)} KB PDF`;
pdfButton.disabled = false;
});
document.getElementById('file').addEventListener('change', async (event) => {
const file = event.target.files[0];
if (!file) return;
status.textContent = `Opening ${file.name}…`;
await show(file).catch((err) => { status.textContent = `Could not open ${file.name}: ${err.message}`; });
});
const res = await fetch(SAMPLE);
await show(await res.arrayBuffer());index.html
<p>
<label>Open a .postext file: <input id="file" type="file" accept=".postext,application/zip"></label>
<button id="pdf" disabled>Make the PDF</button>
<span id="links" hidden>
<a id="open" target="_blank" rel="noopener">open it</a> ·
<a id="download" download="book.pdf">download it</a>
</span>
</p>
<p id="status">Loading the sample book…</p>
<div id="pages"></div>style.css
body {
margin: 16px;
font-family: system-ui, sans-serif;
background: #e8e8e8;
}
#pages {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: flex-start;
}
#pages canvas {
display: block;
width: 240px;
height: auto;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}يحمّل محررًا تفاعليًا من codepen.io. يستورد المثال أحدث إصدار من postext عبر شبكة توصيل المحتوى (CDN).
#إنشاء حزمة
يكتب createBundle ملف .postext من مستند: فصوله وإعداداته وموارده والملفات التي تشير إليها.
import { createBundle } from 'postext';
const { bytes, manifest, warnings } = await createBundle({
name: 'The Lantern',
locale: 'en',
chapters: [
{ markdown: '# Dusk\n\nIt is drawn in :ref{id="lantern"}.' },
{ title: 'Night', markdown: '# Night\n\n…' },
],
config,
resources: [{
id: 'lantern', typeId: 'figure', kind: 'svg', caption: 'The lantern.',
svg: { fileId: 'lantern.svg', width: 240, height: 150 },
createdAt: 0, updatedAt: 0,
}],
files: { 'lantern.svg': svgMarkup, 'garamond-regular': fontBytes },
});| المُدخل | المعنى |
|---|---|
name، id، description، locale | البيانات الوصفية للبيان. القيمة الافتراضية لـid صيغة مختصرة (slug) من name. |
chapters أو markdown | الكتاب، { title?, markdown } لكل فصل، أو مستند واحد. |
config | إعدادات PostextConfig. القيم المساوية للقيم الافتراضية لا تُكتب في البيان. |
resources | الموارد. تسمّي الصورة ملفها بـbitmap.fileId / svg.fileId (وsvg.pdfFileId لنسخة الطباعة الأصلية). |
files | الملفات حسب fileId (كائن أو Map): الصور التي تشير إليها الموارد، وملفات الخطوط التي تشير إليها متغيرات config.customFonts. يمكن أن تكون القيم Uint8Array أو ArrayBuffer أو Blob أو سلسلة نصية (شيفرة SVG). |
thumbnail | { data, mime }: صورة غلاف (PNG أو JPEG أو WebP أو GIF أو SVG). |
canvasScope | القيمة 'book' تطلب من العارضين إخراج الكتاب كله لوحةً واحدة. |
mtime | تاريخ التعديل المكتوب على كل ملف في الأرشيف (Date أو طابع زمني أو سلسلة تاريخ). إذا أُغفل فهو وقت الاستدعاء، فيعطي استدعاءان بالمُدخل نفسه بايتات مختلفة. مرّر تاريخًا ثابتًا يعطِ المُدخل نفسه البايتات نفسها، فتستطيع حساب بصمتها (hash) أو مقارنتها. يحفظ ملف zip تاريخًا ووقتًا بلا منطقة زمنية، بخطوات من ثانيتين، من 1980 إلى 2099، ويُكتب التاريخ بالتوقيت المحلي للجهاز. وللحصول على بايتات متطابقة على كل جهاز، ابنِ التاريخ من حقول محلية، مثل new Date(1980, 0, 1): فالطابع الزمني أو السلسلة المنتهية بـZ يسمّيان لحظة بعينها، تقع في وقت محلي مختلف في كل منطقة زمنية ('1980-01-01T00:00:00Z' ما زال في 1979 غربَ UTC). والتاريخ الواقع خارج تلك السنوات، بالتوقيت المحلي، يرمي خطأً. |
localized | لغات أخرى للكتاب نفسه، حسب وسم اللغة: { es: { chapters?, config?, resources? } }. تصبح المُدخلات أعلاه حينئذٍ محتوى locale، وهو مطلوب. انظر الحزم الثنائية اللغة. |
تعيد الدالة بايتات الأرشيف bytes، وmanifest المكتوب على شكل preset.json، وكل ملف في files (المسار ← البايتات)، وقائمة warnings. تُسمّى الملفات باسم معرّف المورد (resources/lantern.svg) أو اسم ملف الخط (fonts/…)، والفصول بترتيبها وعنوانها (chapters/01-dusk.md). تُعرَّف الخطوط في fonts في البيان، ولا تُعرَّف أبدًا داخل config.customFonts. وتُستبعد بعض الأشياء، مع تحذير لكل منها:
- مورد أو وجه خط ليس ملفه في
files - وجه
.woff(لا يستطيع مُخرِج PDF تضمينه) - عائلة معلَّمة بـ
redistributable: false
في المتصفح، سلّم bytes إلى رابط تنزيل: URL.createObjectURL(new Blob([bytes], { type: 'application/zip' })). وفي Node، اكتبها بـfs.writeFile. لا يحتاج createBundle ولا openBundle إلى DOM. تستخدم ملفات التوزيع (dists) مسارات وحدات بلا امتداد، لذا تحتاج تحت Node المجرّد، دون أداة تجميع (bundler)، إلى خطاف حلّ (resolve hook). ويعرض الملف docs/examples/open-bundle/build-sample.mjs في المستودع خطافًا كهذا في بضعة أسطر.
#الحزم الثنائية اللغة
يمكن لملف .postext أن يحمل كتابًا بعدة لغات، ويقرؤه openBundle(bytes, { locale }) بأيٍّ منها. ويكتب createBundle ملفًا كهذا من localized: مُدخل واحد لكل لغة إضافية، فيه ما يختلف عن المحتوى الأساسي (مُدخل locale):
const { bytes, manifest } = await createBundle({
name: 'The Lantern',
locale: 'en',
chapters: [{ markdown: '# Dusk\n\n…' }, { markdown: '# Night\n\n…' }],
config,
resources: [lanternFigure, hoursTable],
files: { 'lantern.svg': svgEn, 'lantern-es.svg': svgEs },
localized: {
es: {
chapters: [{ markdown: '# Anochecer\n\n…' }, { markdown: '# Noche\n\n…' }],
config: { headings: { levels: [{ level: 1, numberingTemplate: 'Capítulo {1}' }] } },
resources: [
{ id: 'lantern', caption: 'El farol.', svg: { fileId: 'lantern-es.svg', width: 240, height: 150 } },
{ id: 'hours', caption: 'Horas de luz.' },
],
},
},
});
const es = await openBundle(bytes, { locale: 'es' }); // Spanish chapters, config and captionschapters: الكتاب بتلك اللغة. تذهب ملفات الفصول إلى مجلد لكل لغة (chapters/en/01-dusk.md،chapters/es/01-anochecer.md) ويصبحchaptersفي البيان خريطة من اللغة إلى الفصول. اللغة التي لاchaptersلها تقرأ الفصول الأساسية؛ وإذا لم يكن لأي لغة فصول خاصة بها، تبقى قائمة واحدة.config: الإعدادات لتلك اللغة. يحلّ كل مفتاح من المستوى الأعلى محل المفتاح المشترك برمّته حين تُقرأ الحزمة بتلك اللغة، فـheadingsأعلاه يحلّ محل كائنheadingsكله. والمفاتيح المُغفلة، أو المساوية للمشتركة، مشتركة ولا تُكتب، فتمرير إعدادات اللغة كاملة يصلح كما يصلح تمرير المفاتيح القليلة التي تتغير. والمفتاح المضبوط على قيمه الافتراضية بينما المشترك ليس كذلك (layout: {}) يُكتب كما هو، فيعيد ضبط القيمة المشتركة. الخطوط مشتركة: تنضم عائلاتcustomFontsالخاصة بلغة ما إلىfontsفي الحزمة.resources: صياغة الموارد المشتركة، مطابَقةً بـid:captionوnoteوaltTextوtableفي الجدول. والصورة التي فيها كلمات يمكن أن يكون لها عملها الفني الخاص: يسمّيbitmap.fileIdأوsvg.fileId(وsvg.pdfFileId) ملفًا آخر فيfiles، يُكتب على شكلresources/es/lantern.svg. أما الحقول الأخرى، كالنوع أو الموضع، فمشتركة. والمعرّف غير الموجود بينresourcesيُستبعد مع تحذير، والصورة المفقودة للغةٍ ما تُبقي تلك اللغة على الصورة المشتركة، مع تحذير أيضًا.
يسرد البيان كل لغة في locales (['en', 'es'])، ويحتفظ باللغة الأساسية في locale ويخزّن البقية تحت localized. ويقرأ openBundle دون لغةٍ اللغةَ الأساسية.
أي لغة يحصل عليها القارئ. يقدّم openBundle(bytes, { locale }) اللغة المطابقة تمامًا، وإلا فاللغة دون متغيرها الإقليمي (es-MX يقرأ es)، وإلا فاللغة الأساسية، ويبيّن bundle.locale أيّها قدّم. تأتي الفصول والصياغة دائمًا من اللغة نفسها. وتحتفظ اللغة الأساسية بالصياغة المشتركة حتى إن حمل localized متغيرًا إقليميًا منها: فالحزمة بـpt-PT التي فيها مُدخل pt-BR تقرأ التعليقات البرازيلية لـpt-BR وحدها، والتعليقات المشتركة لـpt-PT وpt.
#مثال حي: إنشاء حزمة
يبني هذا المثال على CodePen كتابًا من فصلين فيه شكل SVG، ويسرد الملفات التي كتبها createBundle مع البيان. ويعرض الأرشيف للتنزيل، ثم يفتحه من جديد بـopenBundle ويرسم صفحته الأولى: الدورة الكاملة ذهابًا وإيابًا في بضعة أسطر. استورد الملف المنزَّل في Sandbox لتواصل العمل عليه هناك.
import { createBundle, openBundle, registerBundleImages, buildBundle, renderPage } from 'https://esm.sh/postext';
// A picture resource names its payload by fileId; the bytes (here, SVG
// markup) go in `files` under that same id.
const lanternSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 150">
<rect width="240" height="150" fill="#f3efe6"/>
<path d="M100 36 h40 l8 14 h-56 z" fill="#2f3e46"/>
<rect x="98" y="50" width="44" height="58" rx="4" fill="#f6c453" stroke="#2f3e46" stroke-width="4"/>
<circle cx="120" cy="79" r="11" fill="#fff4c2"/>
<path d="M94 108 h52 l-6 12 h-40 z" fill="#2f3e46"/>
</svg>`;
const resources = [{
id: 'lantern',
typeId: 'figure',
kind: 'svg',
caption: 'The lantern by the door.',
svg: { fileId: 'lantern.svg', width: 240, height: 150 },
createdAt: 0,
updatedAt: 0,
}];
const text = 'The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.';
// One entry per chapter; a chapter without a title takes its first # heading.
const chapters = [
{ markdown: `# Dusk\n\n${text} It is drawn in :ref{id="lantern"}.\n\n${text}\n\n${text}` },
{ markdown: `# Night\n\n${text}\n\n${text}` },
];
const config = {
layout: { layoutType: 'double' },
// Two short chapters that run on, with no blank verso between them (an
// H1 otherwise opens on a fresh recto), as in the open-bundle sample.
headings: { levels: [{ level: 1, numberingTemplate: 'Chapter {1}', breakBefore: { enabled: false } }] },
};
// Everything a .postext file holds: manifest, chapters, resources, fonts.
const { bytes, manifest, files, warnings } = await createBundle({
name: 'The Lantern',
locale: 'en',
chapters,
config,
resources,
files: { 'lantern.svg': lanternSvg },
});
if (warnings.length) console.warn(warnings);
const url = URL.createObjectURL(new Blob([bytes], { type: 'application/zip' }));
document.getElementById('download').href = url;
document.getElementById('actions').hidden = false;
document.getElementById('files').replaceChildren(...Object.entries(files).map(([path, data]) => {
const li = document.createElement('li');
li.textContent = `${path} (${data.length} B)`;
return li;
}));
document.getElementById('manifest').textContent = JSON.stringify(manifest, null, 2);
// Round trip: open the file just written, the way any program would.
await Promise.all([
document.fonts.load('16px "EB Garamond"'),
document.fonts.load('bold 16px "EB Garamond"'),
document.fonts.load('bold 16px "Open Sans"'), // the default heading face
]);
const bundle = await openBundle(bytes);
await registerBundleImages(bundle);
const [firstChapter] = buildBundle(bundle);
document.getElementById('page').replaceChildren(renderPage(firstChapter.pages[0], firstChapter));
document.getElementById('status').textContent =
`${bundle.name}: ${bundle.chapters.length} chapters, ${(bytes.length / 1024).toFixed(1)} KB`;index.html
<p id="status">Building the bundle…</p>
<p id="actions" hidden>
<a id="download" download="lantern.postext">Download lantern.postext</a> ·
<a href="https://postext.dev/en/sandbox" target="_blank" rel="noopener">open the Sandbox</a> and import it (Projects → New → Import .postext…)
</p>
<div id="output">
<section>
<h3>Files in the bundle</h3>
<ul id="files"></ul>
<h3>preset.json</h3>
<pre id="manifest"></pre>
</section>
<section>
<h3>Opened again: page 1</h3>
<div id="page"></div>
</section>
</div>style.css
body {
margin: 16px;
font-family: system-ui, sans-serif;
background: #e8e8e8;
}
#output {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: flex-start;
}
#output section {
flex: 1 1 280px;
min-width: 0;
}
h3 {
margin: 8px 0;
font-size: 14px;
}
ul {
margin: 0;
padding-left: 20px;
font-family: ui-monospace, monospace;
font-size: 13px;
}
pre {
max-height: 320px;
overflow: auto;
padding: 8px;
background: #fff;
font-size: 12px;
}
#page canvas {
display: block;
max-width: 100%;
height: auto;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}يحمّل محررًا تفاعليًا من codepen.io. يستورد المثال أحدث إصدار من postext عبر شبكة توصيل المحتوى (CDN).
#العمل بالحزم
لأن Sandbox ومهارة الوكيل ومكتبة postext تقرأ كلها الملف نفسه وتكتبه، فملف .postext وسيلة عملية لتسليم كتاب من أداة إلى أخرى:
- ابدأ من حزمة. انقل منشورًا قائمًا بـمهارة الوكيل، أو صمّم كتابًا في Sandbox وصدّره (تنزيل (.postext) في قائمة ⋯ في صفّه ضمن لوحة الكتب). حمّل الملف من برنامجك بـ
openBundleلترسمه على Canvas أو في HTML أو PDF. واحتفظ بالملف مصدرًا للكتاب: عدّل الفصول أو الإعدادات أو الموارد في الشيفرة واكتبه من جديد بـcreateBundle، أو أعد تحميله ببساطة كلما تغيّر. - صحّح الأخطاء واضبط التفاصيل في Sandbox. حين يحتاج شيء في مخرجات برنامجك إلى عمل (شكل يقع في الصفحة الخطأ، أو نمط عنوان، أو توازن الأعمدة)، صدّر ما يُخرجه برنامجك بـ
createBundle. استورد ذلك الملف في Sandbox (الكتب › جديد › افتح ملف .postext…)، وأصلح النص أو التصميم أو الأشكال بالمعاينة الحية ولوحة الفحوص وعرض PDF، ثم صدّره من جديد. بعدها يحمّل برنامجك الملف المصحَّح بـopenBundle. أو انسخ ما تغيّر إلى شيفرتك: لا يحتويconfigفي البيان إلا على القيم التي تختلف عن الافتراضية، فيُقرأ كفرقٍ (diff) قصير.
#الواجهة البرمجية منخفضة المستوى
يصدّر postext/bundle أيضًا اللبنات التي يقوم عليها openBundle وcreateBundle، للمضيفين الذين يخزّنون الحزم أو يقدّمونها بطريقتهم (مجلد غير مضغوط عبر HTTP، أو سجلات في قاعدة بيانات):
openBundleZip(bytes)/zipBundle(files, { mtime }): طبقة الأرشيف. يتسامح الفتح مع مجلد في المستوى الأعلى، ويتجاهل مُدخلات__MACOSXوالملفات التي يبدأ اسمها بنقطة (dotfiles). وتُرفض المسارات التي تخرج من الحزمة. ويؤرّخmtimeالملفات كما يفعل مُدخلcreateBundle.readBundle(manifest, readFile, options)يقرأ بيانًا مع دالة الاستدعاءreadFile(path)ويحوّلهما إلى فصول وإعدادات وموارد وصور وخطوط. يضبطoptionsاللغة، وطريقة تسمية معرّفات الملفات (ids)، والإعدادات الأساسية (baseConfig، تحت إعدادات البيان؛ وهي افتراضيًا لوحة الألوان وأنواع الموارد فيbundleBaseConfigبلغة الحزمة، التي يعيدهاresolveBundleConfigLocale(manifest, locale)، وعلى المضيف الذي يمرّرbaseConfigخاصًا به أن يكيّفه مع تلك اللغة؛ وتحت بيان أقدم منconfigVersion: 4يُثبَّتmathالخاص بها مع ما في الحزمة، وتحت بيان أقدم من 5 الفراغ تحت المورد المضمَّن فيlayoutالخاص بها، وتحت بيان أقدم من 6 الفراغ في الأطر فيlayout، والمساحة تحت سطر النقطتين فيbodyText، والعلامات المضمَّنة فيheadings، وأحجام الحروف الاستهلالية، وتحت بيان أقدم من 7 فواصل الشرطات وتقسيم الأسطر غير المضبوطة فيbodyText، وتحت بيان أقدم من 8 التقسيم تحت العنوان فيheadings، وفواصل الكلمات المركبة والفراغ تحت حاويات:::paragraphsفيbodyText، انظر الحزم التي كتبها postext 1.4 أو ما قبله)، وطريقة قياس المقاسات الأصلية.planBundle(meta, content)/resolveBundleFiles(plan, sources): جانب الكتابة، مقسومًا إلى خطة خالصة (أسماء الملفات والبيان) وجلب البايتات عبر دالتَي الاستدعاءreadBlob/readFont.isBundleManifest(value)، ومنتقيات اللغة (pickChapterSpecs،pickLocaleOverrides،pickBundleView،resolveBundleLocale،resolveBundleConfigLocale)، وsvgSize/bitmapSize، وأنواع الصيغة (BundleManifest،BundleResourceSpec،BundleFontFamilySpec، …).CONFIG_VERSION،migrateConfig(config, configVersion, { content })،pinLegacyHeadingBreaks(config)،pinLegacyMathSize(config)،pinLegacyInlineGap(config)،pinLegacyBoxResourceGap(config)،pinLegacyHeadingMarks(config)،pinLegacyDropCapSize(config)،pinLegacyColonListRoom(config)،pinLegacyBoxChildCut(config)،pinLegacyDashBreaks(config)،pinLegacyRaggedBreaking(config)،pinLegacyHeadingSplit(config)،pinLegacyParagraphContainerSpacing(config)،pinLegacyHyphenBreaks(config)وLEGACY_MATH_SIZE(0.5 ÷ 0.442): تحوّل إعدادات مخزنة إلى مصطلحات اليوم (انظر الحزم التي كتبها postext 1.4 أو ما قبله). يطبّقهاreadBundle؛ ويستطيع ذلك أيضًا المضيف الذي يخزّن الإعدادات بطريقته، مرة واحدة لكل نسخة مخزنة.
يقوم Sandbox على هذه الأدوات، ويضيف إليها معرّفات التخزين الخاصة به وسجلات الصفحات في layouts.json.