# بنية Postext

> البنية التقنية لمحرّك الإخراج في Postext

- نسخة HTML: https://postext.dev/ar/docs/architecture
- آخر تحديث: 2026-10-04
- مدة القراءة: 38 دقيقة
- لغات أخرى: [en](https://postext.dev/en/docs/architecture.md), [es](https://postext.dev/es/docs/architecture.md), [ca](https://postext.dev/ca/docs/architecture.md), [zh](https://postext.dev/zh/docs/architecture.md)

## باختصار

تشرح هذه الصفحة كيف يعمل Postext من الداخل، وهي مكتوبة للمبرمجين. أولًا يقيس Postext كل كلمة دون أن يرسم شيئًا على الشاشة، وهذا يجعله سريعًا جدًا. ثم يحسب موضع كل سطر وكل صورة وكل عمود، ويحفظ هذه الخطة في الذاكرة. ويكرّر العمل حتى لا يتحرك شيء بعد ذلك. وأخيرًا يرسم الصفحات الجاهزة صفحةَ ويب أو صورةً مرسومة في المتصفح أو ملف PDF. وحين تغيّر كلمة، لا يُعاد حساب إلا الأجزاء التي تغيّرت.

إن كنت قد عملت مع React من قبل، فأنت تفهم الحيلة الأساسية. يبني React نموذج DOM افتراضيًا في الذاكرة، ويقارنه بالنسخة السابقة، ولا يلمس DOM الحقيقي في المتصفح إلا بعد ذلك. ويفعل Postext الشيء نفسه، لكنه بدلًا من مكوّنات واجهة المستخدم يبني شجرة من **الصفحات والأعمدة والكتل النصية والمربعات المحيطة**. الهندسة الكاملة لمستند متعدد الصفحات ومتعدد الأعمدة، محسوبةً قبل رسم بكسل واحد. كل فقرة وعنوان وصورة وحاشية سفلية واقتباس بارز موضوع عند إحداثيات دقيقة، مع احترام قواعد تنضيد عمرها قرون لا تستطيع CSS التعبير عنها.

كل هذا ممكن بفضل [`@chenglou/pretext`](https://github.com/chenglou/pretext)، وهي مكتبة لقياس النص لا تعتمد على DOM، وأسرع بما بين 300 و600 مرة من إعادة حساب التخطيط (reflow) في المتصفح. ولقصة المشروع (عقد من المحاولات الفاشلة، وعنق الزجاجة الذي أوقفها كلها، والمكتبة التي أزالته أخيرًا)، راجع [مدخل](/ar/docs/introduction).

## الفكرة الأساسية

تخيّل مستندًا من 74 صفحة بعمودين: تقريرًا سنويًا لشركة مثلًا، أو كتابًا مدرسيًا كثيف الرسوم. تسلّمه إلى Postext، فيبني المحرّك الإخراج كاملًا في الذاكرة: كل صفحة، وكل عمود، والموضع الدقيق لكل فقرة وأبعادها بالبكسل. تريد أن تعرف ما في الصفحة 72، العمود 2؟ الجواب موجود مسبقًا، دون أي رسم. فقد قرّر المحرّك أين يكسر كل فقرة، وأين يضع كل صورة، وكيف يتجنّب الأرامل والأسطر اليتيمة (widows and orphans)، وكيف يحاذي خطوط الأساس بين الأعمدة المتجاورة.

وإليك سبب أهمية ذلك.

قواعد التنضيد مترابطة ترابطًا عميقًا ومرهقًا. تعالج أرملة في الصفحة 5 (سطرًا أخيرًا وحيدًا عالقًا أسفل عمود) بسحبها إلى العمود السابق. حسنًا. لكن هذا التغيير يقصّر عمود الصفحة 5، فيُزيح المحتوى إلى الأمام، وقد ينشأ عن ذلك سطر يتيم في الصفحة 6: سطر أول وحيد دُفع إلى عمود جديد منفصلًا عن فقرته. وحتى *تكتشف* أنك أحدثت مشكلة جديدة، تحتاج إلى أن يكون إخراج المستند كله متاحًا للفحص. ولكي *تعالجها* دون أن تُحدث مشكلة أخرى في مكان آخر، تحتاج إلى أن تستطيع تعديل الكل وإعادة قياسه وإعادة فحصه.

هذه هي فلسفة «احسب كل شيء أولًا، وارسم لاحقًا». ليست حيلة لتحسين الأداء، بل الطريقة الوحيدة لتطبيق عشرات القواعد التنضيدية المترابطة التي استعملها المنضّدون المحترفون منذ قرون.

## المفاهيم الأساسية

مسرد سريع. يفترض باقي المستند هذه المصطلحات، فارجع إلى هنا كلما غمض عليك أحدها.

| المصطلح | التعريف |
| --- | --- |
| **VDT** | شجرة المستند الافتراضية (Virtual Document Tree). بنية البيانات القابلة للتعديل في مكانها التي تمثّل المستند كله: الصفحات والأعمدة والكتل والمقاطع السطرية والمربعات المحيطة. تشبه DOM الافتراضي، لكنها لهندسة إخراج المستند. |
| **الصفحة** | مساحة مستطيلة ثابتة المقاس. يعرف المحرّك الصفحات منذ البداية. والمستند سلسلة مرتّبة من الصفحات. |
| **العمود** | تقسيم عمودي للصفحة. للأعمدة عرض ثابت وارتفاع أقصى. يتدفق النص من عمود إلى الذي يليه، ثم إلى الصفحة التالية. |
| **الكتلة** | وحدة محتوى تشغل حيّزًا عموديًا في عمود: فقرة أو عنوان أو صورة أو جدول أو اقتباس كتلي أو اقتباس بارز أو منطقة حواشٍ سفلية. |
| **السطر** | سطر نصي مقيس داخل كتلة، تنتجه Pretext. لكل سطر مربع محيط وموضع لخط الأساس. |
| **المربع المحيط (bounding box)** | {  `x, y, width, height`  } بالبكسل، نسبةً إلى أصل الصفحة. كل عقدة في VDT تحمل واحدًا. |
| **المورد** | عنصر غير نصي يرتبط بالنص بمعرّف (id): صورة نقطية (bitmap) أو SVG أو جدول (`Resource`، مع `kind: 'bitmap' \| 'svg' \| 'table'`). تُرقَّم الموارد حسب النوع (الشكل 1، الجدول 2.1…) عند أول إحالة إليها، وتُعوَّم إلى شريط أعلى الصفحة أو أسفلها قرب تلك الإحالة. |
| **الحاشية** | حاشية سفلية أو تعليق في آخر الفصل، يُكتب في markdown بعلامة `[^id]` وتعريف `[^id]:` (راجع [الحواشي السفلية](/ar/docs/document-format#الحواشي-السفلية)). حواشي الهامش، وقائمة `PostextNote` التي يقبلها نموذج المحتوى، غير منفّذة: المحرّك لا يقرأ `notes`. |
| **شبكة خطوط الأساس** | الشبكة العمودية المشتقة من ارتفاع سطر المتن (مثلًا 24px لـ 16px/1.5). ينبغي أن يقع كل خط أساس لنص المتن على مضاعف لها، فتبقى الأسطر متحاذية عبر الأعمدة والصفحات المتقابلة. |
| **الرداءة (badness)** | مقدار انحراف المسافات بين كلمات السطر المضبوط عن عرضها الطبيعي، وهي مربّع نسبة التعديل، وتتشبّع عند 10000. وهي الكلفة الأساسية في كسر الأسطر بخوارزمية Knuth-Plass. |
| **نقاط الجزاء (demerits)** | الكلفة الكلية لكسر سطر مرشّح في Knuth-Plass: الرداءة مضافًا إليها الجزاءات (تقسيم الكلمات بالواصلة، والأرملة/اليتيمة/الكلمة المعزولة، وعدم تطابق فئة الملاءمة). تختار الخوارزمية مجموعة الكسور ذات أدنى مجموع لنقاط الجزاء. |
| **فئة الملاءمة (fitness class)** | تصنيف تقريبي لمدى ضيق السطر أو اتساعه. الأسطر المتجاورة في فئات متباعدة جدًا (سطر ضيق بجوار سطر واسع جدًا) تتحمّل نقاط جزاء إضافية، فيصير نسيج الفقرة أكثر انتظامًا. |
| **الفراغ المتبقي (slack)** | حيّز عمودي غير مستعمل يبقى أسفل العمود. كلفة مربّعة للفراغ (موزونة بـ `slackWeight`) تدفع كاسر الأسطر نحو مجموعات الكسور التي تملأ الأعمدة بإحكام. |
| **المُخرِج (backend)** | هدف رسم يستهلك VDT بعد تقاربها. ثلاث منها متاحة اليوم: **اللوحة (canvas)** (معاينة نقطية)، و**HTML** (قراءة على الشاشة مبنية على DOM)، و**PDF** (مخرجات جاهزة للطباعة عبر `postext-pdf`). تشترك الثلاث في القياس نفسه (وحدة القياس المبنية فوق Pretext). ورابعة، **EPUB** (`postext-epub`)، تكتب الكتاب كتابًا إلكترونيًا من المستندات نفسها. |
| **المرحلة (pass)** | مرحلة واحدة من خط الإخراج. كل مرحلة تقرأ VDT وتعدّلها، ولها مسؤولية واحدة. |
| **حلقة التقارب** | الحلقة الخارجية التي تعيد تشغيل مراحل الإخراج حين تُبطل مراحل لاحقة قرارات سابقة. حدّها الأقصى 5 تكرارات. |

## بنية النظام

> **شكل: بنية نظام Postext**
> يدخل Markdown المُثرى وPostextConfig إلى محلّل يبني شجرة المستند الافتراضية. تقيس Pretext النص. سبع مراحل إخراج تعدّل VDT داخل حلقة تقارب. ويرسم مُخرِجٌ VDT النهائية بصيغة HTML أو PDF.
>
> *المحلّل ← VDT ↔ Pretext ← سبع مراحل إخراج ← المُخرِج ← المخرجات.*

**هذا هو المسار الذي يسلكه محتواك داخل المحرّك:**

1. يقرأ **المحلّل** (parser) ملف markdown المُثرى والإعدادات، ويبني VDT الأولية: شجرة من الكتل المصنّفة بلا مواضع بعد، فيها المحتوى والبنية فقط
2. تتولّى **مراحل الإخراج** العمل، فتعدّل VDT بالتتابع: تقيس النص عبر Pretext، وتوزّع الكتل على الصفحات والأعمدة، وتحسّن التنضيد حتى يبلغ المعايير المهنية
3. تراقب **حلقة التقارب** المشكلات: حين تُبطل مرحلة لاحقة (معالجة أرملة مثلًا) قرارًا سابقًا (ارتفاعات الأعمدة مثلًا)، يعود المحرّك ويعيد التشغيل من النقطة المتأثرة. حتى 5 تكرارات، إلى أن يستقر كل شيء
4. **VDT النهائية** هي هندسة الإخراج الكاملة: كل عنصر يعرف رقم صفحته والعمود المخصّص له وموضعه ومربعه المحيط. يكون المستند «منضّدًا» بالكامل قبل أي رسم
5. يمرّ **مُخرِج** على VDT المكتملة ويرسمها بالصيغة المستهدفة: صورة نقطية على لوحة canvas، أو شجرة DOM من عناصر HTML محدّدة المواضع، أو مستند PDF بخطوط مضمّنة. VDT نفسها تغذّي الثلاث؛ واختيار المُخرِج قرار يخص المخرجات وحدها

## طبقة الإدخال

### نموذج المحتوى

نموذج المحتوى فلسفة بقدر ما هو بنية بيانات. أنت تصف *ماذا* تقول، لا *كيف* يُخرَج. والمحرّك يتخذ قرارات الإخراج.

> **شكل: نموذج المحتوى: markdown والموارد والحواشي**
> يفصل مُدخل Postext بين markdown (ترتيب القراءة والبنية الدلالية) والموارد (الصور النقطية وملفات SVG والجداول) والحواشي. يحلّ المحرّك الإحالات السطرية :ref والتضمينات ::resource بالمعرّف وينتج VDT.
>
> *يملك markdown ترتيب القراءة، وتملك الموارد البيانات المرئية.*

```typescript
// packages/postext/src/types.ts

interface PostextContent {
  markdown: string;            // enriched markdown with :ref / ::resource markers
  metadata?: DocumentMetadata; // title, subtitle, author, publishDate, …
  resources?: Resource[];      // bitmaps, SVGs, tables — referenced by id
  notes?: PostextNote[];       // not read: footnotes are written as [^id] in the markdown
}

interface Resource {
  id: string;                  // stable id, referenced by inline :refs
  typeId: string;              // ResourceType this resource belongs to ('figure', 'table', …)
  kind: 'bitmap' | 'svg' | 'table';
  caption?: string;            // the type prefix + number are computed, not written here
  altText?: string;
  createdAt: number;
  updatedAt: number;
  // Exactly one kind-specific payload:
  bitmap?: { fileId: string; format: string; width: number; height: number };
  svg?: { fileId: string; width?: number; height?: number };
  table?: { model: TableModel };
  placement?: ResourcePlacement; // optional per-resource float override
}
```

**الموارد** تحمل البيانات المرئية: التعليقات والنص البديل وحمولة خاصة بالنوع. الحمولات الثنائية (الصور النقطية وملفات SVG) تبقى *خارج النطاق* (out-of-band): لا يخزّن المورد إلا `fileId`، ويحلّه المُخرِج وقت الرسم (يحفظ Sandbox البايتات في IndexedDB). وموارد الجداول استثناء: نموذجها `TableModel` (شبكة خلايا مع امتداداتها ومحاذاتها) ينتقل داخل المورد، لأنه بيانات مهيكلة لا بايتات.
**الحواشي** يُفترض أن تحمل محتوى ونمطًا للعلامة، ويُحال إليها من مواضع داخل أسطر markdown. لكنها غير منفّذة بعد: يتجاهل المحرّك `notes`، وليس في markdown صيغة للإحالة إلى الحواشي.

هذا الفصل خيار تصميمي مقصود، وأهميته أكبر مما يبدو. يملك markdown *ترتيب القراءة* و*البنية الدلالية*: ما يأتي أولًا، وما هو عنوان، وأين يُحال إلى حاشية سفلية. وتملك مصفوفتا الموارد والحواشي *البيانات المرئية*: أبعاد الصور ونص التعليقات ومحتوى الحواشي. وبإبقائهما منفصلين، يمكن إخراج markdown نفسه بطرق مختلفة تمامًا بمجرد تغيير الإعدادات. فيمكن لتخطيط أكاديمي بعمودين ولتدوينة بعمود واحد أن يشتركا في المحتوى المصدري نفسه. ويستطيع المحرّك أن يتخذ قرارات الوضع، كتأجيل صورة إلى العمود التالي لأنها لا تتسع هنا، دون أن يمس محتواك المصدري أبدًا.

```typescript
// Example: a simple article with a referenced figure
const content: PostextContent = {
  markdown: `
# The Art of Typography

The history of typography begins with Gutenberg's
movable type, shown in :ref{id="printing-press"}.
His invention transformed the production of books.

The technique spread rapidly across Europe, reaching
Italy by 1465 and France by 1470.
  `,
  resources: [
    {
      id: 'printing-press',
      typeId: 'figure',
      kind: 'bitmap',
      caption: 'A reconstruction of the original press.',
      altText: "Reconstruction of Gutenberg's printing press",
      createdAt: 1765379100000,
      updatedAt: 1765379100000,
      bitmap: { fileId: 'press-photo', format: 'jpeg', width: 600, height: 400 },
    },
  ],
};
```

ترتبط الموارد بالنص بالمعرّف، و**تكفي الإحالة إلى المورد لإدراجه**. تؤدي الإحالة السطرية `:ref{id="printing-press"}` عملين معًا: ترسم الرقم المحسوب للمورد في النص الجاري («الشكل 1»)، وعند الإحالة الأولى بترتيب القراءة *تعوّم* المورد على الصفحة، فتحجز له شريطًا أعلى الصفحة أو أسفلها قرب تلك الإحالة، كما يفعل المنضّد في المطبعة تمامًا. لا تضع الشكل مرة ثانية أبدًا. وللمورد الذي يجب أن يستقر أحيانًا عند نقطة محدّدة من التدفق، فإن `::resource{id="…"}` في سطر مستقل تضمين كتلي اختياري: لا يُرسم في موضعه إلا إذا كانت قيمة `placement.position` المحلولة للمورد هي `'here'` (وهذا يُخرجه من التعويم)؛ أما في المورد العائم فيُعامل كأي إحالة أخرى. لا يحتاج المؤلف أبدًا إلى التفكير في الوضع، فالمحرّك يتولاه.

> **شكل: حلّ الإحالات**
> يحيل مصدر markdown إلى شكل داخل السطر بـ :ref{id=printing-press}. وتوفّر مصفوفة الموارد المحتوى الفعلي بالمعرّف. يحلّ المحرّك الإحالة، ويرسم الرقم المحسوب في النص، ويعوّم الشكل إلى شريط أعلى الصفحة.
>
> *الإحالات في markdown مجرد أسماء. يحلّها المحرّك مقابل الموارد، ويرقّمها، ويعوّم الشكل قرب الإحالة.*

### الإعدادات

يتحكم `PostextConfig` في كل جانب من جوانب خط الإخراج. العرض الكامل للأقسام (الصفحة، والتخطيط، ونص المتن، والعناوين، والقوائم، والرياضيات، والترويسات والتذييلات…) موجود في صفحة <a href="/ar/docs/configuration">الإعدادات</a>؛ وأكثرها صلة بهذا المستند:

| الإعداد | يتحكم في | يُستعمل في |
| --- | --- | --- |
| `bodyText` / `headings` | جزاءات الأرملة/اليتيمة/الكلمة المعزولة لكل حقل، وقواعد الإبقاء معًا، وحدود ضبط الأسطر، وتقسيم الكلمات بالواصلة — راجع [الإعدادات › نص المتن](/ar/docs/configuration#نص-المتن) | المرحلة 2، المرحلة 5 |
| `tableStyle` | تنضيد الخلايا، والحدود، ونصف قطر الزوايا، وألوان تعبئة رأس الجدول ومتنه في موارد `kind: 'table'`، إضافة إلى المتغيّرات المسمّاة في `tableStyles` التي يختارها الجدول بـ `table.styleId` — راجع [الإعدادات › نمط الجداول](/ar/docs/configuration#نمط-الجداول) | المرحلة 4 |
| `captionStyle` | تنضيد تعليقات الموارد: التسمية المرقّمة ونص الوصف — راجع [الإعدادات › نمط التعليقات](/ar/docs/configuration#نمط-التعليقات) | المرحلة 4 |
| `diagramStyle` | `singleInk` + `inkColor` — يعيد تلوين مخططات SVG بدرجات من حبر واحد تُحدَّد حسب الإضاءة (luminance)، فتُستنسخ الأشكال بأمانة عند الطباعة بلون خاص واحد — راجع [الإعدادات › نمط المخططات](/ar/docs/configuration#نمط-المخططات) | المُخرِجات |
| `resourceTypes` | ترقيم الموارد حسب النوع: القوالب، وصيغ العدّادات، ونطاقات إعادة الضبط، والوضع العائم الافتراضي — راجع [الإعدادات › أنواع الموارد](/ar/docs/configuration#أنواع-الموارد) | المرحلة 1، المرحلة 4 |
| `TypographyConfig`, `ColumnConfig`, `ResourcePlacementConfig`, `ReferenceConfig`, `PostextSectionOverride` | **قديمة.** مُعلَنة في `types.ts` لكنها لم تُربط قط بخط الإخراج؛ وقد استوعبت الأقسام أعلاه مسؤولياتها. أُبقيت للرجوع إليها فقط. | — |

### استراتيجية التحليل

التحليل عمدًا أبسط خطوة في خط الإخراج. يدخل markdown، ويُحلَّل إلى شجرة بناء مجرّدة (AST)، وتصير كل عقدة `VDTBlock`. تُحلّ إحالات الموارد مقابل المصفوفة `resources[]` بالمعرّف (المصفوفة `notes[]` لا تُقرأ بعد: الحواشي مخطّط لها وغير منفّذة). والمخرجات قائمة مسطّحة من الكتل المصنّفة الممتلئة بالمحتوى، لكن بلا صفحة مخصّصة ولا عمود ولا موضع.

فكّر فيها كقائمة جرد: «هناك عنوان، ثم فقرة من 200 كلمة، ثم إحالة إلى شكل، ثم فقرة أخرى». لا قياسات، ولا مواضع، ولا أي قرار إخراج. يبدأ العمل الثقيل في المرحلة 2.

## شجرة المستند الافتراضية (VDT)

تخيّل أنك طلبت من منضّد محترف أن يُخرج كتابًا كاملًا، لكنه بدلًا من أن يسلّمك صفحات مطبوعة سلّمك جدولًا حسابيًا. كل صف عنصر، وكل خلية قياس دقيق: «العنوان عند (40, 30)، والفقرة الأولى تبدأ عند (40, 78) وارتفاعها 144px، والصورة أعلى العمود 2 في الصفحة 3...». ذلك الجدول هو VDT.

شجرة المستند الافتراضية هي بنية البيانات المركزية في Postext: شجرة قابلة للتعديل في مكانها تمثّل كل صفحة وعمود وكتلة وسطر، ويحمل كل منها مربعًا محيطًا دقيقًا. وحين يتقارب خط الإخراج، *تكون* VDT هي الجواب. يمكنك أن تسأل «ما الذي في الصفحة 72، العمود 2؟» دون رسم بكسل واحد.

### لماذا قابلة للتعديل

هذا هو النهج نفسه المتّبع في خطوط الرسم في محرّكات الألعاب، حيث تحدّث أنظمة متعاقبة حالةَ عالم مشتركة قابلة للتعديل في حلقة سريعة متلاحقة. وللسبب نفسه.

الأشجار غير القابلة للتعديل (مثل DOM الافتراضي في React) تخصّص كائنات جديدة عند كل تغيير. هذا مقبول لواجهة مستخدم فيها بضع مئات من المكوّنات. لكن في حلقة تقارب قد تجري حتى 5 تكرارات عبر 7 مراحل، وقد تمس آلاف الكتل، يصبح ضغط تخصيص الذاكرة وتوقفات جمع المهملات مشكلة حقيقية. لذلك تستعمل VDT التعديل في المكان مع نمط العلامة `dirty`: تعلّم المراحل العقدَ بأنها متّسخة (dirty)، فتعرف المراحل اللاحقة بالضبط أي العقد تعيد فحصها. يتذكر المحرّك ما تغيّر فلا يعيد عملًا صحيحًا.

### البنية

> **شكل: بنية شجرة المستند الافتراضية**
> VDT هرمية: المستند يحتوي على صفحات، وكل صفحة تحتوي على أعمدة، وكل عمود يحتوي على كتل (عنوان، فقرة، مورد)، وكل كتلة نصية تحتوي على أسطر مقيسة. تحمل كل عقدة مربعًا محيطًا وعلامة dirty وفهرسي الصفحة والعمود.
>
> *تحمل كل عقدة bbox وعلامة dirty وفهرسي الصفحة والعمود.*

### تعريفات الأنواع

الأشكال أدناه **مبسّطة للشرح**: التعريفات الحقيقية في `packages/postext/src/vdt.ts` تحمل حقولًا أخرى كثيرة موجّهة للرسم (سلاسل الخطوط، والألوان، ونقاط القوائم، ورسوم الرياضيات، وخانات التصميم). المهم هنا هو البنية:

```typescript
// Simplified — see packages/postext/src/vdt.ts for the full definitions

// The root of the Virtual Document Tree
interface VDTDocument {
  pages: VDTPage[];
  blocks: VDTBlock[];         // flat view of the same block objects
  config: ResolvedConfig;     // every sub-config resolved to non-optional
  baselineGrid: number;       // baseline increment in px (e.g. 24 for 16px/1.5)
  converged: boolean;
  iterationCount: number;
  metadata: DocumentMetadata;
}

// A physical page
interface VDTPage {
  index: number;
  width: number;
  height: number;
  columns: VDTColumn[];
  header?: VDTDesignSlot;     // running header (design slot)
  footer?: VDTDesignSlot;     // running footer / page number
  floats?: VDTBlock[];        // resource bands floated to the top/bottom of this page
  pageNumberValue: number;
  pageLabel: string;          // rendered label ('iv', '7', 'A', …)
}

// A column within a page
interface VDTColumn {
  index: number;
  bbox: BoundingBox;          // position within the page
  blocks: VDTBlock[];
  availableHeight: number;    // remaining vertical space
  baselineOffset: number;     // current baseline y-position
  band?: number;              // column band (0 unless a span block split the page)
  kind?: 'text' | 'span';     // 'span' = full-width column holding a page-span block
}

// A content block (paragraph, heading, resource, etc.)
type VDTBlockType =
  | 'paragraph' | 'heading' | 'resource' | 'blockquote'
  | 'listItem' | 'footnoteRef' | 'mathDisplay';

interface VDTBlock {
  id: string;
  type: VDTBlockType;
  bbox: BoundingBox;
  lines: VDTLine[];           // for text blocks (populated by Pass 2)
  resourceBlock?: ResolvedResourceBlock; // for resource blocks
  pageIndex: number;
  columnIndex: number;
  dirty: boolean;             // needs re-layout
  snappedToGrid: boolean;     // baseline aligned to grid
}

// A measured line of text
interface VDTLine {
  text: string;
  bbox: BoundingBox;            // natural width: a justified line is painted to its block's right edge
  baseline: number;             // y-position of the text baseline
  hyphenated: boolean;          // line ends inside a word, or after a closed dash ("say—" | "that’s")
  hardHyphen?: boolean;         // …after a hyphen the text carries ("well-" | "known"): nothing added
  repeatedHyphen?: boolean;     // opens with that hyphen repeated ("vencer-" | "-se"), not in the source
  segments?: VDTLineSegment[];  // word/space/math runs for justified rendering
  isLastLine?: boolean;         // last line of its paragraph
  justifiedSpaceRatio?: number; // applied space width ÷ normal space width
  sourceStart?: number;         // markdown source map (char offsets; a line opening with `\$` starts at the backslash)
  sourceEnd?: number;
  plainStart?: number;          // plain-text source map
  plainEnd?: number;
}

// A measured, placement-ready resource embed (bitmap / svg / table)
interface ResolvedResourceBlock {
  resource: Resource;
  kind: 'bitmap' | 'svg' | 'table';
  number: string;             // computed number, e.g. "1.7"
  captionPrefix: string;      // e.g. "Figure"
  bodyRect: BoundingBox;      // the image / table area
  fileId?: string;            // out-of-band binary (bitmap / svg)
  captionLines: VDTLine[];    // measured caption, prefix + number included
  table?: VDTResourceTableLayout; // cell geometry for table resources
}

// Bounding box — all values in px, relative to page origin
interface BoundingBox {
  x: number;
  y: number;
  width: number;
  height: number;
}
```

بعض هذه الحقول يستحق ملاحظة:

- **`isLastLine`** يوجّه الرسم المضبوط: تُرسم الأسطر الأخيرة غير مضبوطة حتى حين تكون الفقرة مضبوطة، إلا الأسطر الأخيرة *المكتظة* (overfull)، التي تنضغط المسافات بين كلماتها لتتسع في عرض السطر (وفق دلالات ضبط المسافات المرنة في TeX).
- **`sourceStart`/`sourceEnd` و`plainStart`/`plainEnd`** خرائط مصدر تربط كل سطر بموضعه في markdown الأصلي وفي النص الخام للكتلة، وعليها تقوم مزامنة المؤشر والتحديد في تكاملات المحررات.
- **`VDTResourceTableLayout`** (مع مدخلاته `VDTResourceTableCell`) يحمل الهندسة الكاملة لمورد جدول بعد إخراجه: حواف الأعمدة على المحور x، وحواف الصفوف على المحور y، ومستطيلات الخلايا مع أسطر محتواها المقيسة، فترسم كل المُخرِجات الجدول نفسه.
- **`computePageTextExtent(page)`** دالة مساعدة عامة صغيرة تعيد الامتداد العمودي الذي يغطيه النص فعلًا في صفحة (بما في ذلك تعليقات العناصر العائمة). تستعملها طبقات التصحيح المرئية كي لا تمتد خطوط شبكة خطوط الأساس إلا على النص الحقيقي، لا على أسفل الصفحة الفارغ.

### تتبّع التغييرات

تتبّع التغييرات هو الطريقة التي يتجنب بها المحرّك إعادة عمل أنجزه صحيحًا. حين تنقل مرحلةٌ كتلةً أو تغيّر حجمها، تضبط `dirty = true` على تلك الكتلة وعلى كل كتلة تليها في العمود نفسه، لأن مواضعها كلها تعتمد على الكتلة المتغيّرة. ويمكن لحلقة التقارب حينئذ أن تتخطى الأشجار الفرعية غير المتغيّرة كليًا.

إليك مثالًا ملموسًا. تُدخل المرحلة 5 واصلة في فقرة في الصفحة 12، فتفقد الفقرة سطرًا من ارتفاعها. تُعلَّم تلك الفقرة بأنها متّسخة، وكذلك كل الكتل تحتها في العمود نفسه، فكلها تحتاج إلى الصعود سطرًا واحدًا. أما الكتل في الصفحة 11 وما قبلها؟ لم تُمس. تتخطاها المراحل كليًا في التكرار التالي.

وتؤدي العلامة `dirty` أيضًا دور إشارة التقارب: إن لم تبق كتلة متّسخة بعد المراحل 5–7، فقد تقارب الإخراج ويتوقف المحرّك عن التكرار. انتهى.

## خط الإخراج

سبع مراحل، لكل منها مهمة واحدة. هذا هو خط الإخراج كله.

يستعير التصميم من خطوط الرسم في محرّكات الألعاب (مرحلة الظلال، ومرحلة الإضاءة، ومرحلة المعالجة اللاحقة)، حيث يقرأ كل نظام حالة عالم مشتركة ويعدّلها ويثق بأن الأنظمة السابقة أدّت دورها. وهذا يجعل كل مرحلة سهلة الفهم والاختبار والتحسين بمعزل عن غيرها. يمكنك قياس أداء المرحلة 5 دون التفكير في المرحلة 3.

الفرق الجوهري عن محرّك الألعاب أن اللعبة ترسم كل إطار مرة واحدة وتمضي. أما Postext فلا يستطيع ذلك. القرارات التنضيدية مترابطة ترابطًا عميقًا (معالجة أرملة قد تغيّر ارتفاعات الأعمدة، وهذا يؤثر في الموازنة، وقد يُحدث سطرًا يتيمًا جديدًا)، لذا قد يحتاج خط الإخراج إلى التكرار. تجري المراحل 3–7 داخل حلقة تقارب، وتتكرر حتى 5 مرات إلى أن يستقر الإخراج على نتيجة ثابتة.

### المرحلة 1: هيكلة المحتوى

- **المُدخل:** `PostextContent` خام
- **العمل:** تحليل markdown إلى AST، وحلّ إحالات الموارد مقابل `resources[]` بالمعرّف، وإنشاء عقد `VDTBlock` الأولية (الحواشي، ومعها `notes[]`، مخطّط لها وغير منفّذة بعد)
- **المُخرج:** `VDTBlock[]` مسطّحة (مصنّفة وممتلئة بالمحتوى، لكن بلا صفحة ولا عمود مخصّصين)
- **تجري مرة واحدة** (ليست جزءًا من حلقة التقارب)

### المرحلة 2: قياس النص

- **المُدخل:** `VDTBlock[]` ذات محتوى نصي
- **العمل:** لكل كتلة نصية، قياس الأسطر بعرض العمود المستهدف عبر **وحدة القياس** المخصّصة (`packages/postext/src/measure/`)، التي تبني تقسيم الكلمات بالواصلة وضبط الأسطر والمقاطع السطرية المنسّقة وكسر الأسطر بخوارزمية Knuth-Plass فوق Pretext. ثم تخزين `VDTLine[]` المقيسة والارتفاع الكلي في كل كتلة
- **تفصيل مهم:** القياس مخزّن مؤقتًا. تبني `cachedMeasureBlock` / `cachedMeasureRichBlock` (في `measure/cache.ts`) مفتاحها من النص والخطوط والعرض وكل خيار يؤثر في الإخراج، فإعادة قياس فقرة لم تتغير مجرد بحث في خريطة
- **المُخرج:** لكل كتلة نصية أبعاد دقيقة بالبكسل
- **يُعاد تشغيلها حين:** تتغير عروض الأعمدة أو يتغير المحتوى النصي (مثلًا عند إدراج واصلة)

تنقسم الوحدة بوضوح حسب المسؤولية: `plain.ts` يقيس المقاطع البسيطة، و`rich.ts` يقيس المقاطع المختلطة من غامق ومائل ورياضيات، و`font.ts` يبني سلاسل الخطوط ويدير دورة حياة الذاكرة المؤقتة، و`canvas.ts` يغلّف الدوال الأولية لقياس عرض النص على canvas. وتفصيل واحد في دورة الحياة مهم عمليًا: `clearMeasurementCache()` تمسح ذاكرات Pretext المؤقتة الداخلية *وكذلك* ذاكرة عرض النص الخاصة بالمحرّك، فتُطرح عروض الحروف المقيسة بخط احتياطي حالما يكتمل تحميل الخطوط الحقيقية.

تسلك الفقرات الصينية واليابانية والكورية طريقًا آخر داخل الوحدة نفسها. الفقرة التي تفوق فيها حروف CJK عدد المسافات بين الكلمات تذهب إلى **مُنضِّد CJK** (`cjkCompose.ts`). يقطع النص إلى وحدات (حرف، أو مقطع من نص لاتيني، أو شرطة بعرض em مزدوج أو علامة حذف، أو صندوق ذرّي كالشارة أو علامة الحاشية)، ويقيس كل وحدة مرة واحدة، ويملأ الأسطر بطريقة أول ملاءمة (first-fit) وفق قواعد بداية السطر ونهايته في `cjkClasses.ts`: يستوعب السطر العلامةَ التي لا يجوز أن تفتتح السطر التالي بالتخلي عن الفراغ المحيط بعلامات الترقيم (`cjkPunctuation.ts`) قبل أن ينقل حرفًا إلى السطر التالي. ثم يُوزَّع السطر المضبوط بين حروفه. والمخرجات أسطر `VDTLine` عادية تحمل مقاطعها ما تحتاج إليه المُخرِجات لرسمها كما قيست بالضبط: `tracking` (بكسلات بعد كل حرف، داخلة أصلًا في عرض المقطع)، و`inkOffset` للعلامة التي تخلّت عن فراغها، و`hangs`، والمسافات بين حروف الهان والحروف اللاتينية بوصفها مقاطع `autospace`، والعلامات وقراءات الروبي (ruby) وصفوف الواريتشو (warichu) في التعليقات التوضيحية الصينية. والكلفة خطية في طول الفقرة: الفصل الأول من 紅樓夢 (6,949 حرفًا) يُنضَّد بقياس 1,298 حرفًا، كل حرف مميز مرة واحدة، في حين كان قياس بادئات الفقرة يتطلب 636,948. راجع [الإخراج الصيني](/ar/docs/chinese-layout).

وتحت ذلك كله، هنا تثبت Pretext جدواها. استدعاء `prepare()` هو الجزء المكلف: يحلّل النص باستعمال محرّك الخطوط في canvas ويخزّن النتيجة. أما استدعاء `layout()`؟ حساب محض، شبه مجاني. وهذا الفصل بين الاستدعاءين هو جوهر الأمر. فبعد تحضير النص، يستطيع المحرّك إعادة الإخراج بعروض مختلفة (تجربة تشكيلات أعمدة، واختبار ما يحدث لو اكتسبت فقرة واصلة) بكلفة لا تُذكر. حضّر مرة واحدة، وأخرج بقدر ما تحتاج.

```typescript
// Simplified: how the measure module uses pretext internally
const prepared = prepare(paragraphText, '16px/1.5 Inter');
const { height } = layout(prepared, columnWidth, 24); // 24px line-height
// => "This paragraph is 168px tall at 320px column width — that's 7 lines."
```

### المرحلة 3: التوزيع على الصفحات والأعمدة

- **المُدخل:** الكتل المقيسة
- **العمل:** توزيع الكتل على الصفحات والأعمدة بالتتابع. إنشاء عقد `VDTPage` و`VDTColumn`. تتبّع `availableHeight` لكل عمود. وحين لا تتسع كتلة، الانتقال إلى العمود أو الصفحة التالية
- **الاستراتيجية:** وضع جشع بأول ملاءمة (greedy first-fit). تتبع فواصل الأعمدة والصفحات أبسط توزيع صالح
- **المُخرج:** لكل كتلة `pageIndex` و`columnIndex` و`bbox`

هذه هي اللحظة التي تصير فيها VDT مستندًا حقيقيًا. قبل هذه المرحلة، الكتل مجرد قائمة مسطّحة لها أبعاد وليس لها عنوان. تمر المرحلة 3 عليها وتخصّص كل واحدة لصفحة وعمود، كمن يصب الماء في شبكة من الأوعية: املأ العمود 1 حتى يفيض، وانتقل إلى العمود 2، وحين تمتلئ الصفحة ابدأ صفحة جديدة.

قبل أن تستقر أي كتلة محتوى، تحجز المرحلة مساحة للعناصر البنيوية: الترويسات والتذييلات الجارية (تُخرَج كخانات تصميم من `config.header` / `config.footer`) وأي أشرطة عائمة معلّقة للصفحة المفتوحة حديثًا. هذه الحجوزات تُنقص `availableHeight` لكل عمود، فحين تبدأ كتل المحتوى بالتدفق يعرف المحرّك بالضبط كم من المساحة متاح.

**أشرطة الأعمدة والأعمدة الممتدة.** `page.columns` مصفوفة مسطّحة بترتيب القراءة، لكن الصفحة ليست دائمًا صفًا واحدًا من الأعمدة. الكتلة الواقعة في التدفق والممتدة على عرض الصفحة (اليوم: `:::callout` مع `span: 'page'` في تخطيط متعدد الأعمدة) تقطع الصفحة إلى *أشرطة* (bands) متراصّة: تُغلق الأعمدة النصية للشريط الحالي عند خط القطع (يُقيَّد ارتفاعها، وتصير `availableHeight` صفرًا)، وتحصل الكتلة على عمود خاص بها بعرض كامل مع `kind: 'span'`، ويُلحق تحتها شريط جديد من الأعمدة النصية (`band + 1`، بالموضع x والعرض نفسيهما، وبالحافة السفلى نفسها للشريط الذي يحلّ محلّه). لا تُضاف الأعمدة إلا إلحاقًا، فيبقى `columnIndex` يشير إلى `page.columns[i]`، ولا تحتاج المُخرِجات إلى رسم خاص: كل عمود يُقصّ إلى مربعه المحيط (موسّعًا بـ `columnClipRect`: 2pt لحبر الحروف، يُضاف إليها ما تتدلى به عناصر تصميم العمود خارج جانبيه، كلسان العنوان أو شارة الإطار، وما يبلغه تصميم العنوان فوق أعلاه؛ أما أسفله فيبقى الحافة، وهو المستطيل نفسه في واجهتي canvas وPDF)، ويُرسم الخط الفاصل بين الأعمدة لكل شريط بين الأعمدة النصية المتجاورة، بدءًا من أسفل الشريط الذي يشغله عنوان ممتد على عرض الصفحة في رأسها (`columnRuleSegments`)، وبخط الصفحة نفسها حين يحدّده قسم منسّق (`VDTPage.columnRule`، ويُقرأ عبر `pageColumnRule`). تتجاهل موازنة الأعمدة الأعمدةَ الممتدة والأشرطةَ ذات الارتفاع الصفري. تقطع الكتلة الممتدة مباشرةً حيث يكون الشريط مستويًا (أعلى الصفحة، أو مباشرة بعد عنوان افتتاحي، أو كتلة ممتدة أخرى، أو شريط عائم علوي)؛ أما إذا وصلت إلى شريط غير مستوٍ فتقترح بدلًا من ذلك *سقفًا للشريط* (band cap) (`packages/postext/src/pipeline/bandCaps.ts`): تُقصَّر أعمدة الشريط الذي يبدأ بكتلة محتوى معيّنة إلى `ceil(Σ used / N / grid)` سطرًا، ويعيد `buildDocument` تشغيل مرحلة التوزيع مع السقف (ويزيده سطرًا في كل مرة حين يفيض الشريط المسقوف، في بضع جولات إضافية على الأكثر، ثم يرجع إلى الصفحة التالية)، فيملأ النص الأعمدة المقصّرة وفق كل قواعد الوضع، وينتهي مستويًا عند القطع، وتحتفظ الأعمدة المغلقة بما يتبقى من فراغ في `availableHeight` لتستوعبه الموازنة. والآلية نفسها تسوّي الشريط *الختامي* للفصل وللمستند (`headings.balancing.trailing`): حين يُبلغ افتتاح فصل، أو `:::part`، أو إطار `placement: 'fixed'` يختم الفصل، أو نهاية المستند، وأعمدة الشريط الحالي غير مستوية، يُقترح سقف من نوع `kind: 'trailing'` مفتاحه كتلة الحدّ؛ ولأن مفتاح السقف هو الكتلة التي تفتتح شريطه (وهذه الكتلة تتحرك كلما استوعبت صفحة سابقة أسطر موازنة إضافية)، تُحلّ السقوف الختامية *بعد* استقرار موازنة الأعمدة، مع تجميد تلميحات الموازنة، ثم تتيح جولة تحسين قصيرة لأدوات الموازنة (levers) أن تملأ ما تركه القطع ناقصًا. أما الإطارات ذات `placement: 'fixed'` فتغادر التدفق: يُثبَّت الإطار إلى منطقة محتوى الصفحة / مربع القص (trim box) / مربع النزف (bleed box)، وتتخلى الأعمدة النصية التي يغطيها عن تلك المنطقة (تُقطع من الأسفل أو الأعلى كشريط عائم، وتنتقل إلى الصفحة التالية عند التعارض)، ويذهب الإطار مع أبنائه إلى `page.floats`.

**الصفحات العمودية.** مع `layout.writingMode: 'vertical-rl'` تُخرج المرحلة صفحة أفقية مُدارة ربع دورة باتجاه عقارب الساعة. في مثل هذه الصفحة تكون منطقة المحتوى والأعمدة والكتل والأسطر والعناصر العائمة ومناطق الحواشي في **إحداثيات التدفق** (flow coordinates)، ويحمل `VDTPage.flow` الدوران الذي ينقلها إلى الورقة: نقطة التدفق (x, y) تقع عند (عرض الصفحة − y, x). ولا شيء في المراحل اللاحقة يحتاج إلى معرفة ذلك: الكسر والعناصر العائمة وقواعد الإبقاء معًا والموازنة تعمل في إطار التدفق كما في أي صفحة، وعمود التدفق صفٌّ (tier) على الورقة، ويطبّق كل مُخرِج الدوران حين يرسم (`flowToPage` و`pageToFlow` يحوّلان النقاط في الاتجاهين). وتبقى الترويسات وأرقام الصفحات وعلامات القص والخلفية في إحداثيات الورقة. تُخرَج الأشكال والجداول كتلًا قائمة تُدار عائدةً داخل الإطار، والحروف التي تقف قائمة تُدار عائدةً واحدًا واحدًا وقت الرسم.

### المرحلة 4: وضع الموارد

- **المُدخل:** VDT بكتل موضوعة في الأعمدة
- **العمل:** تعويم كل مورد مُحال إليه إلى أول خانة حرّة بعد أول إحالة إليه: أسفل العمود المُحيل، أو أعلى / أسفل العمود الفارغ التالي، أو شريط في الصفحة التالية (`packages/postext/src/pipeline/floatPlacement.ts` يخطّط العناصر العائمة، و`pipeline/floatSlots.ts` يعدّد الخانات ويقيسها؛ ويحجز خط البناء الأشرطة)
- **حلّ الوضع:** لكل مورد، يحلّ المحرّك `resource.placement`، ثم `resourceType.defaultPlacement` الخاص بالنوع، ثم القيمة الافتراضية المدمجة `{ position: 'auto', span: 'column' }`

| حقل الوضع | السلوك |
| --- | --- |
| `position: 'auto'` | يأخذ المورد أول خانة حرّة بعد إحالته، في الأعلى أو الأسفل؛ وهذه هي القيمة الافتراضية |
| `position: 'top'` | الخانات العلوية فقط: شريط أعلى العمود الفارغ التالي أو الصفحة التالية، يدفع محتوى العمود تحته |
| `position: 'bottom'` | الخانات السفلية فقط: شريط أسفل عمود أو صفحة، يقصّر العمود فوقه |
| `position: 'here'` | يُخرج المورد من التعويم: يُضمَّن المورد في التدفق عند توجيهه `::resource`، في الموضع الذي يظهر فيه من التدفق بالضبط |
| `span: 'column'` | يشغل الشريط عمودًا واحدًا (وفي الصفحة المفتوحة حديثًا يختار المحرّك العمود الذي بقيت فيه أكبر مساحة) |
| `span: 'page'` | يمتد الشريط على عرض المحتوى كاملًا عبر كل الأعمدة، فيقطع تدفق الأعمدة؛ وتُحجز الأشرطة الممتدة على العرض الكامل أولًا، فتستقر العناصر العائمة الممتدة على عمود داخل المساحة المتبقية |

- **الوضع المؤجَّل:** العنصر العائم الذي لا يتسع في أي خانة من الصفحة الحالية ينتظر الصفحة التالية التي يفتحها التدفق (لا يُصغَّر ولا يُقسَم أبدًا)، وعند حدّ فصل يُفرَّغ على صفحات تُفتح قبل الحدّ
- **المُخرج:** موارد موضوعة في أشرطة الصفحة (`page.floats`)، مع إنقاص ارتفاعات الأعمدة المتأثرة كي يتدفق النص حول الأشرطة

في وضع الموارد يصير الأمر أدق، لأن العناصر العائمة لا تشغل المساحة فحسب، بل تعيد تشكيل المساحة حولها. حين تستقر الكتلة التي تحمل الإحالة، تُعرض على العنصر العائم المعلّق الخاناتُ الحرّة في الصفحة الحالية بعدها: أسفل ذلك العمود، ثم أعلى العمود الفارغ التالي وأسفله (وللعنصر العائم الممتد على الصفحة: أسفل الصفحة، حين يبقى في كل عمود متسع)؛ وما لا يتسع في أي مكان ينتظر الصفحة التالية، حيث تحجز العناصر العائمة المعلّقة أشرطتها قبل أن يتدفق أي نص. تنكمش الأعمدة لتتسع بين الأشرطة، ويتدفق النص عبر الأعمدة المضيّقة دون انقطاع، ويرى القارئ الشكل قرب النقطة التي ذُكر فيها (لا عندها تمامًا). هذه ممارسة معتادة في التنضيد المهني، والكتب تفعلها باستمرار.

**قواعد الوضع.** إلى جانب توزيع الوضع، تتبع العناصر العائمة قيودًا تحريرية صارمة:

- **قاعدة ما بعد الإحالة.** يستقر العنصر العائم في أول خانة حرّة *بعد* أول إحالة إليه في النص، ولا يسبقها أبدًا. يصادف القارئ الإحالة أولًا، ثم يرى المورد. وإن لم يتسع العنصر العائم، يتأجل إلى الأمام إلى خانة أو صفحة لاحقة، لا إلى الخلف أبدًا.
- **ترتيب الإحالات داخل السلسلة.** تُعرض كل خانة على العناصر العائمة المعلّقة بترتيب أول إحالة، والعنصر الذي لا يتسع في أي مكان يؤخّر ما خلفه في سلسلة ترقيمه: الجدول 3 لا يستقر أبدًا بعد الجدول 4، والشكل 12 لا يسبق الشكل 11 أبدًا. ولا تؤخّر السلاسل بعضها بعضًا: الجدول المنتظر يسمح بمرور شكل لاحق. وحتى لا يضطر جدول طويل إلى انتظار صفحة جديدة، فإن الجدول الذي يُعرض عليه رأس عمود فارغ يُقطع على مقاس ذلك العمود ويُكمَل في الخانة التالية (العمود المجاور، أو أشرطة الصفحة التالية)، مع تكرار صفوف ترويسته.
- **حاجز الفصل.** لا تفلت العناصر العائمة من فصلها أبدًا. عند افتتاح فصل (مستوى عنوان مع `breakBefore` أو `span: 'page'`)، و`:::part`، ونمط إطار مع `floatBarrier: true`، ونهاية المستند، يوضع كل عنصر عائم معلّق أولًا (في الخانات الحرّة للصفحة، ثم على صفحات تُفتح قبل الحدّ، تضع كل منها قسرًا عنصرًا عائمًا واحدًا على الأقل) قبل فاصل الصفحة الخاص بالحدّ. وقبل فتح مثل هذه الصفحة، تُعرض على أي شكل أو جدول لا يزال معلّقًا الخاناتُ الحرّة في الصفحة الحالية مرة أخرى أيًّا كانت قيمة `position` فيه: العنصر العائم المخصّص لرأس الصفحة والمُحال إليه في الصفحة الختامية للفصل يأخذ أسفل تلك الصفحة، تحت الأعمدة الموزونة، بدلًا من صفحة خاصة به (وتحتفظ الإطارات العائمة بوضعها). ويرسل `:::pagebreak` العناصر العائمة المعلّقة إلى الصفحة التي تليه، بعد أي حشو لضبط زوجية الصفحة.
- **الحد الأدنى لمساحة النص.** في الصفحة المفتوحة حديثًا لا يُحجز شريط إلا إذا بقي متسع لـ 3 أسطر متن على الأقل في الأعمدة المتأثرة، مع استثناء واحد: يجوز وضع عنصر عائم مفرط الحجم قسرًا في شريط لا يزال كله نصًا، كي لا يعطّل شكل مهيمن الطابور إلى الأبد. وأما الخانة في الصفحة الحالية فيجب أن تتسع في الارتفاع المتبقي للعمود؛ ولا تنطبق قاعدة الأسطر الثلاثة هناك إلا بجوار شريط عائم آخر.
- **مسافة للتنفس.** تفصل فجوةٌ بارتفاع سطر متن واحد الشريطَ عن النص المجاور له.
- **المحاذاة مع شبكة خطوط الأساس.** تُقرَّب الأشرطة العلوية *صعودًا* إلى مضاعف لشبكة خطوط الأساس (فتكبر الفجوة تحت العنصر العائم)، فيقع كل سطر مُزاح على الشبكة. وتُثبَّت العناصر العائمة السفلية بحيث يقع آخر خط أساس في التعليق على الشبكة: يشترك التعليق في خط أساسه مع آخر سطر نصي في الأعمدة المجاورة، وتنتهي الصفحات عند الارتفاع نفسه عبر الأعمدة والصفحات المتقابلة.

**الترقيم حسب النوع عند أول إحالة.** لا تُرقَّم الموارد في markdown. يخصّص `pipeline/resourceNumbering.ts` لكل مورد رقمه عند أول إحالة إليه بترتيب القراءة، مستعملًا `ResourceType` الخاص بالمورد: يجمع `numberingTemplate` بين عدّاد النوع `{n}` وعدّادات العناوين `{h1}`..`{h6}` السارية عند الإحالة (مثلًا `'{h1}.{n}'` ← «1.7»)، ويتحكم `resetOn` في موعد إعادة العدّاد إلى البداية (`'never'` أو عند أي مستوى عنوان)، ويختار `counterFormat` الأرقام العشرية أو الرومانية أو الأبجدية. ويأتي النوعان المدمجان *الشكل* و*الجدول* من `defaultResourceTypes(locale)`، مترجمَين إلى لغة المستند. ولأن الترقيم يتبع ترتيب أول إحالة، فإن إدراج شكل جديد في وسط المستند يعيد ترقيم كل ما بعده تلقائيًا، دون أي تعديل في المصدر.

### المرحلة 5: التحسين التنضيدي

هذه المرحلة هي ما يميّز محرّك الإخراج من أداة تفرغ النص على الصفحة. تفرض قواعد الجودة التحريرية التي طبّقها المنضّدون المحترفون يدويًا منذ قرون، والتي يتجاهلها رسم النص الساذج تمامًا.

تعمل المرحلة 5 على مستويين: **كسر الأسطر القائم على الجزاءات** داخل كل فقرة، و**فرض قواعد الإبقاء معًا البنيوية** بين الكتل. يعملان معًا، لكنهما آليتان مختلفتان.

#### تجنّب الأسطر الأرامل والأسطر اليتيمة والكلمات المعزولة بالجزاءات

**الأرامل والأسطر اليتيمة** أوضح علامات التنضيد غير المحترف:

- **الأرملة** (widow) سطر واحد من فقرة تُرك وحده أسفل عمود. تكمل الفقرة في العمود التالي، لكن ذلك السطر الوحيد يبدو عالقًا (كأن العمود انتهى قبل أوانه).
- **السطر اليتيم** (orphan) سطر واحد من فقرة عالق أعلى عمود. معظم الفقرة في العمود السابق، لكن سطرًا واحدًا انسكب إلى ما بعده (فيبدو منفصلًا عن سياقه).
- **الكلمة المعزولة** (runt) فقرة سطرها الأخير كلمة قصيرة واحدة (أو اثنتان)، أقصر بصريًا من أن يبدو سطرًا نصيًا حقيقيًا. أقل خطورة بنيويًا من الأرملة، لكنها مزعجة بالقدر نفسه للقارئ المتأني.

تُعالَج الثلاث كلها بحقن **نقاط جزاء** في خوارزمية Knuth-Plass لكسر الأسطر. فبدلًا من إخراج الفقرة ثم محاولة إصلاح كسر سيئ بعد وقوعه، يُعلِّم المحرّك كاسرَ الأسطر أن بعض مجموعات الكسور أغلى من غيرها. ثم تختار الخوارزمية مجموعة الكسور المثلى على مستوى الفقرة كلها، وهي تتجنب من تلقاء نفسها الأرامل والأسطر اليتيمة والكلمات المعزولة كلما أمكن.

وبالتحديد، لكل عقدة كسر مرشّحة في الفقرة:

- إذا كان اختيار هذا الكسر سيترك أقل من `orphanMinLines` سطرًا أعلى العمود التالي، أضف `orphanPenalty` (الافتراضي 1000) إلى نقاط الجزاء للعقدة.
- إذا كان اختيار هذا الكسر سيترك أقل من `widowMinLines` سطرًا أسفل العمود الحالي، أضف `widowPenalty` (الافتراضي 1000).
- إذا كان السطر الأخير الناتج عن هذا الكسر سيحمل محتوى عرضه أقل من `runtMinCharacters × normalSpaceWidth` (القيمة الافتراضية لـ `runtMinCharacters` هي 20، أي ما يعادل نحو عشرين حرفًا مقيسة بعرض المسافة)، فاحقن `runtPenalty` (الافتراضي 1000) بوصفها رداءة مكافئة في صيغة نقاط الجزاء المربّعة، فتتنافس على المقياس نفسه الذي تتنافس عليه رداءة السطر (التي تتشبع عند 10000) بدلًا من أن تتضاءل أمامها.

تجتمع هذه الجزاءات مع نقاط الجزاء المعتادة (الرداءة، أي مربع نسبة التعديل، وكلفة تقسيم الكلمات، وعدم تطابق فئة الملاءمة) في تحسين شامل واحد. وتفصيل واحد في الرسم يكمل الصورة: في الفقرات المضبوطة تُرسم الأسطر الأخيرة غير مضبوطة، إلا الأسطر الأخيرة *المكتظة*، التي تنضغط المسافات بين كلماتها لتتسع في عرض السطر وفق دلالات ضبط المسافات المرنة في TeX، ويُطبَّق ذلك بالطريقة نفسها في مُخرِجات canvas وHTML وPDF. الخوارزمية حرّة في قبول إحدى هذه الحالات إذا كان البديل أسوأ (فقرة ليس فيها كسر مشروع يرضي كل القواعد)، لكنها تجد في أغلب الأحوال مجموعة كسور تتجنبها. وتنضم عناصر القوائم إلى الحماية نفسها عبر `avoidOrphansInLists` و`avoidWidowsInLists` و`avoidRuntsInLists` (كلها `true` افتراضيًا).

وضغط رابع لطيف، `slackWeight`، يزن كلفة مربّعة لـ«المساحة غير المستعملة في العمود» فتفضّل الخوارزمية مجموعات الكسور التي تملأ الأعمدة بإحكام. وبهذه نقاط الجزاء مجتمعة تصبح المرحلة 5 تحسينًا *لكسر الأسطر*: تُحل معظم حالات الأرامل والأسطر اليتيمة والكلمات المعزولة داخل حلّال Knuth-Plass، لا بتعديلات على تباعد الحروف بعد وقوعها.

كل هذا قابل للضبط في `BodyTextConfig`؛ راجع [الإعدادات › الأسطر اليتيمة والأرامل والكلمات المعزولة وقواعد الإبقاء معًا](/ar/docs/configuration#الأسطر-اليتيمة-والأرامل-والكلمات-المعزولة-وقواعد-الإبقاء-معًا). وضبط أي `*Penalty` على `0` يعطّل تلك القاعدة فعليًا.

#### قواعد الإبقاء معًا البنيوية

بعض التجميعات أكبر من فقرة واحدة: تمتد على كتل متجاورة ولا يمكن معالجتها بكسر الأسطر وحده. تفرض المرحلة 5 هذه القواعد على مستوى وضع الكتل، فتنقل مجموعات كاملة إلى الأمام حين كانت ستنقسم عبر فاصل عمود أو صفحة:

- **العنوان مع فقرته الأولى.** يجب ألا يظهر العنوان أبدًا أسفل عمود إذا كانت الفقرة التي يقدّمها ستبدأ في العمود التالي. يفرض ذلك `headings.keepWithNext` (الافتراضي `true`): إذا لم يتسع المكان للعنوان *مع* الحد الأدنى لأسطر الأرملة في المتن (`bodyText.widowMinLines`، الافتراضي `2`) من الكتلة التالية (أو سطر واحد فقط حين يكون `avoidWidows` معطّلًا)، يُدفع العنوان إلى الأمام ليرافق نصه.
- **العناوين المتتالية.** حين تظهر عدة عناوين متتابعة (مثلًا h2 يليه h3 تليه فقرة)، يجب أن تبقى المجموعة كلها معًا. ولا يجوز أن يُترك أي من العناوين عالقًا أسفل عمود دون المحتوى الذي يقدّمه.
- **القوائم التي تقدّمها نقطتان رأسيتان.** حين تنتهي فقرة بنقطتين رأسيتين تقدّمان قائمة مباشرة، يجب أن يبقى السطر الحامل للنقطتين مع بداية القائمة. يفرض ذلك `bodyText.keepColonWithList` (الافتراضي `true`): إذا كان وضع الفقرة لن يترك متسعًا لبدء أول عنصر في القائمة (كاملًا حين تُبقيه قواعد الأسطر اليتيمة والأرامل للقوائم كاملًا؛ وسطرًا واحدًا مع `bodyText.colonListRoom: 'line'`، كما كان حتى postext 1.4)، ينتقل السطر الأخير الحامل للنقطتين (أو الفقرة كلها، إن كانت سطرًا واحدًا) إلى الأمام مع القائمة. وكلما اضطرت هذه القاعدة إلى دفع الفقرة كلها وسبقتها مباشرة في العمود سلسلة من العناوين، تُسحب تلك العناوين إلى الأمام أيضًا كي لا يُنتهك `keepWithNext` بصمت؛ والاستثناء الوحيد حين لا يحتوي العمود إلا العنوان (أو العناوين) الذي نقله تكرار سابق إلى الأمام، وعندها يُبقي المحرّك الفقرة مع العنوان ويقبل الفصل الأخف بين النقطتين والقائمة تجنبًا للدوران بلا نهاية.
- **الشكل مع تعليقه.** الشكل وتعليقه وحدة لا تنفصل، ويتحركان معًا دائمًا.

حين يُكتشف انتهاك لقاعدة إبقاء معًا، يدفع المحرّك المجموعة كلها إلى العمود أو الصفحة التالية. وتعالج آلية ملء الأعمدة المعتادة المساحة المُخلاة (فكاسر الأسطر قد اختار أصلًا مجموعة كسور تتسع؛ وإن جاء العمود الناتج أقصر قليلًا، تعيد المرحلة 7 توزيع المسافة العمودية حول العناصر الكاسرة للشبكة كي تبقى شبكة خطوط الأساس سليمة).

#### المُخرج

تُعلَّم الكتل التي تغيّرت قياساتها أو مواضعها بـ `dirty` للتكرار التالي من حلقة التقارب. وعمليًا، لأن العمل الثقيل يجري داخل Knuth-Plass لا بتعديلات لاحقة، تستقر معظم المستندات بسرعة: يختار كاسر الأسطر مجموعة كسور جيدة من المرة الأولى، ولا تتعامل التكرارات اللاحقة إلا مع الآثار المترتبة على تحرك الكتل وموازنة الأعمدة.

هذه التصحيحات غير مرئية حين تُتقن (لا ينبغي أن يلاحظها القارئ أبدًا). لكن *غيابها* واضح فورًا لكل من يقرأ بانتباه: ذلك السطر الوحيد المحرج أعلى عمود، وتلك الفجوات غير المتساوية حيث كفّ المحرّك عن محاولة جعل النص يتسع. لدى الناشرين المحترفين أدلة أسلوب كاملة لمنع هذه المشكلات بالذات. وPostext يؤتمتها.

### المرحلة 6: موازنة الأعمدة

- **المُدخل:** VDT بتنضيد محسَّن
- **العمل:** معادلة ارتفاعات الأعمدة في كل صفحة بنقل الكتل بين الأعمدة لتقليل فرق الارتفاع (العلامة `ColumnConfig.balancing` التي كان يُفترض أن تتحكم في ذلك من الخيارات القديمة المُعلَنة غير المربوطة)
- **القيد:** يجب ألا تنتهك قواعد الأرامل والأسطر اليتيمة التي أرستها المرحلة 5
- **المُخرج:** قد تكون الكتل انتقلت بين الأعمدة، وتُعلَّم بـ `dirty`

تلاحظ الأعمدة غير الموزونة فورًا، خصوصًا في الصفحة الأخيرة من الفصل. عمود أيسر ممتلئ وعمود أيمن شبه فارغ يبدوان غير مكتملين، كأن الإخراج توقف في منتصف الطريق. تعيد الموازنة توزيع المحتوى حتى ينتهي العمودان عند ارتفاع متقارب، فتبدو الصفحتان المتقابلتان مصقولتين ومقصودتين.

تحسب الخوارزمية الارتفاع الكلي لمحتوى كل الكتل في الصفحة، وتقسمه على عدد الأعمدة لتجد الارتفاع المستهدف، وتبحث عن أفضل نقطة لكسر العمود تقرّب كل عمود من ذلك الهدف. لكنها ليست قسمة بسيطة. إنها مسألة إرضاء قيود: يجب أن تحترم الخوارزمية قواعد `keepTogether` (يبقى العنوان مع فقرته الأولى)، وأن تراعي الحد الأدنى لعدد الأسطر، والأهم ألا تنقض معالجات الأرامل والأسطر اليتيمة التي اجتهدت المرحلة 5 في إرسائها.

### المرحلة 7: محاذاة الإيقاع العمودي

- **المُدخل:** VDT بأعمدة موزونة
- **العمل:** مطابقة خطوط الأساس مع شبكة خطوط الأساس بتوزيع تعديلات المسافات حول العناوين والصور والعناصر الأخرى الكاسرة للشبكة
- **المُخرج:** قيم مسافات معدَّلة؛ وخطوط أساس متحاذية عبر الأعمدة
- **انظر:** [نظام الإيقاع العمودي](#نظام-الإيقاع-العمودي) للخوارزمية كاملة

### حلقة التقارب

> **شكل: حلقة التقارب**
> المرحلة 1 تحلّل، والمرحلة 2 تقيس، ثم تجري المراحل من 3 إلى 7 داخل حلقة تقارب. إذا بقيت أي كتلة متّسخة (dirty) وكان عدد التكرارات أقل من خمسة، تعيد الحلقة التشغيل من المرحلة 3.
>
> *يعود المحرّك إلى المرحلة 3 حتى لا تبقى كتل متّسخة (5 تكرارات على الأكثر).*

فكّر في حلقة التقارب كأن المحرّك يجادل نفسه. تختار المرحلة 5 مجموعة كسور تتجنب أرملة داخل الفقرة A، لكن ذلك يقصّر الفقرة A سطرًا، فتبقى فجوة أسفل العمود 2. تعيد المرحلة 6 موازنة الأعمدة للتعويض، فتدفع عنوانًا إلى عمود جديد، فيُستثار `keepWithNext` ويُجبر العنوان على الانتقال كليًا إلى العمود التالي. وتعدّل المرحلة 7 الإيقاع العمودي، وقد يُحدث ذلك كلمة معزولة جديدة حيث كان العنوان. فيعود المحرّك إلى المرحلة 3، ويعيد وضع الكتل بالقياسات المحدّثة، ويمر على التسلسل كله من جديد. كل تكرار يحل مشكلات أكثر مما يُحدث، إلى أن لا يبقى في النهاية شيء متّسخ.

ولأن معظم حالات الأرامل والأسطر اليتيمة والكلمات المعزولة تُحل *داخل* حلّال Knuth-Plass في جولة واحدة من كسر الأسطر، تتقارب المستندات المعتادة الآن في تكرار أو تكرارين. ولا تزال الحلقة ضرورية حين تُزيح أحداث على مستوى الكتل (عنوان دفعه `keepWithNext` إلى الأمام، أو شكل أجّله الوضع، أو موازنة الأعمدة وهي تعادل الارتفاعات) حدودَ الأعمدة التي قاست المرحلة 5 على أساسها. وحين يحدث ذلك، تعيد المرحلة 3 الوضع، وتعيد المرحلة 5 الكسر بالقيود الجديدة، فتستقر الحلقة.

بعد اكتمال المراحل 5–7، يتحقق المحرّك هل بقيت كتل معلَّمة بـ `dirty`. فإن وُجدت كتل متّسخة وكان عدد التكرارات أقل من 5، يُعاد تشغيل خط الإخراج من **المرحلة 3**.

**معايير التقارب:**
- لا كتل متّسخة بعد المراحل 5–7، **أو**
- بلوغ الحد الأقصى وهو 5 تكرارات (قبول أفضل نتيجة حتى الآن)

يتتبع المحرّك **درجة المخالفات التنضيدية** في كل تكرار: مجموع موزون للمشكلات المتبقية من أرامل وأسطر يتيمة وأعمدة غير موزونة وانحراف عن شبكة خطوط الأساس. لكل نوع مخالفة وزن يعكس حدّته البصرية (فالأرملة أوضح بكثير من انحراف 2px عن الشبكة). وإذا بُلغ حد التكرارات الخمسة دون تقارب كامل، يختار المحرّك التكرار الذي أنتج أدنى درجة مخالفات. وليس بالضرورة آخرها، فالتكرارات اللاحقة قد تبالغ في التصحيح أحيانًا، فتعالج مشكلة وتُحدث أخرى.

**تتقارب الموازنة لكل مقطع على حدة.** الصفحات الواقعة بين الفواصل الصريحة (افتتاح فصل، أو `:::pagebreak`) تُخرَج مستقلة بعضها عن بعض: لا شيء يتدفق عبر مثل هذا الفاصل، فلا يمكن لأداة موازنة داخل مجموعة من الصفحات أن تحرك سطرًا من مجموعة أخرى. ولذلك تحكم حلقة موازنة الأعمدة على كل *مقطع* (segment) من هذا النوع بمفرده. لا تزال الجولة الواحدة تضع المستند كله، لكن كل مقطع يحتفظ بنصيبه من أدوات الموازنة أو يرفضه وفق درجة الفجوات الخاصة به، ويضع سلاسل التتابع (cascades) الخاصة به في القائمة السوداء، ويبلغ حالة الثبات وحده، وينفق ميزانيته الخاصة من المحاولات؛ والمقطع الذي تراجعت نتيجته بعد إعادة المحاولة يستعيد صفحاته من أفضل جولة حظي بها بينما تمضي المقاطع الأخرى. وهكذا يتوازن كتاب من ثلاثين فصلًا تمامًا كما تتوازن فصوله واحدًا واحدًا (صفحات الفصل في ملف PDF للكتاب كله مطابقة لملف PDF لذلك الفصل وحده)، بدلًا من أن تكلّف سلسلة تتابع واحدة في أي مكان كل صفحات الكتاب جولة إضافية.

حدّ التكرارات الخمسة صمام أمان عملي: الكمال عدوّ الإنجاز. بعض الحالات المرضية (صفحة كل فقرة فيها بالطول الخاطئ تمامًا فتنشأ الأرامل مهما وازنت الأعمدة) لن تتقارب تقاربًا كاملًا أبدًا. يقبل المحرّك «أفضل جهد» ويمضي.

## نظام الإيقاع العمودي

ارفع كتابًا حسن التنضيد إلى الضوء. ستجد أسطر الصفحة اليسرى متحاذية مع أسطر الصفحة اليمنى. خط أساس السطر 5 في العمود 1 يقع في الموضع العمودي نفسه تمامًا الذي يقع فيه خط أساس السطر 5 في العمود 2. هذا هو الإيقاع العمودي، وهو من أول ما تفحصه العين المدرّبة عند تقييم جودة التنضيد. وهو أيضًا من أبرز ما يميّز Postext.

حين لا يحتوي العمودان إلا نص متن بالحجم نفسه، تكون المحاذاة بديهية: كل سطر بالارتفاع نفسه، فتتطابق خطوط الأساس طبيعيًا. يظهر التحدي حين يحتوي أحد العمودين عنوانًا بحجم خط أكبر، أو صورة بارتفاع اعتباطي بالبكسل، أو مسافة إضافية حول اقتباس كتلي. هذه العناصر «تكسر» الشبكة: يُزاح المحتوى تحتها بمقدار ليس مضاعفًا لخطوة خط الأساس، فتخرج خطوط الأساس في ذلك العمود فجأة عن التزامن مع العمود المجاور. ويضيع الانسجام البصري.

والهدف استعادته: يجب أن تتحاذى خطوط أساس نص المتن في الأعمدة المتجاورة أفقيًا، حتى حين تظهر العناوين أو الصور أو غيرها من العناصر ذات الارتفاع غير القياسي في عمود دون الآخر.

### شبكة خطوط الأساس

كل شيء يرتكز على رقم واحد. يعرّف المستند قيمة `baselineGrid` مشتقة من ارتفاع سطر نص المتن: فنص متن بحجم `16px` و`line-height` قدره `1.5` ينتج شبكة خطوط أساس قدرها `24px`. وينبغي أن يقع كل خط أساس لنص المتن على مضاعف لهذه القيمة. هذا هو العقد.

### العناصر الكاسرة للشبكة

بعض العناصر تكسر الشبكة حتمًا لأن ارتفاعها ليس مضاعفًا لـ `baselineGrid`:

- **العناوين** (حجم خط أكبر، وارتفاع سطر مختلف)
- **الصور** (ارتفاع اعتباطي بالبكسل)
- **الجداول** (ارتفاع متغيّر)
- **الاقتباسات الكتلية** (قد تستعمل حجم خط أو حشوة مختلفين)
- **مناطق الحواشي السفلية** (الحواشي أسفل العمود وخطها الفاصل: تنتهي منطقة نص العمود فوقها)

### خوارزمية تعديل المسافات

> **شكل: محاذاة الإيقاع العمودي**
> يحتوي العمود 1 عنوانًا يكسر شبكة خطوط الأساس بمقدار 12 بكسل. يضيف المحرّك 12 بكسل من المسافة بعد العنوان كي يعود سطر المتن التالي إلى الشبكة. ويبقى العمود 2 متحاذيًا طوال الوقت.
>
> *تُعدَّل المسافات بعد العناصر الكاسرة للشبكة كي تبقى خطوط الأساس متزامنة عبر الأعمدة.*

بعد تعديل المسافات في كل عمود على حدة، يتحقق المحرّك من **المحاذاة عبر الأعمدة**: ينبغي أن تتطابق خطوط الأساس الواقعة في الموضع العمودي نفسه عبر الأعمدة. فإن تباعدت (لاختلاف العناصر الكاسرة للشبكة بين الأعمدة)، تعدّل جولة محاذاة ثانية الفجوات في العمودين لإيجاد إيقاع مشترك.

إليك مثالًا ملموسًا. في العمود 1 عنوان ارتفاعه 36px (1.5 ضعف شبكة 24px). وليس في العمود 2 عنوان. بعد العنوان، انحرف العمود 1 عن الشبكة بمقدار 12px. تضيف الخوارزمية 12px من المسافة الإضافية بعد العنوان، فترفع «المسافة بعد العنوان» من 16px إلى 28px. والآن يقع سطر نص المتن التالي في العمود 1 على خط من خطوط الشبكة من جديد، ويطابق خط أساسه السطر المقابل في العمود 2. وعاد الانسجام.

**حالات حدّية:**
- العمود الذي فيه عناصر كاسرة للشبكة أكثر من الفجوات القابلة للتعديل يقبل محاذاة جزئية (تبذل الخوارزمية أقصى جهدها لكنها لا تضمن محاذاة تامة مع الشبكة إذا كثرت الاضطرابات وقلّت المواضع التي تمتص الخطأ)
- الصورة الأطول من العمود تمتد عبر الأعمدة أو الصفحات (تُعالج على حدة في المرحلة 4)
- حين يؤدي التعديل المطلوب إلى مسافات محرجة بصريًا (مثلًا 40px بعد عنوان والمعتاد 16px)، توزّع الخوارزمية الخطأ على عدة فجوات بدلًا من تركيزه في موضع واحد

## واجهة المُخرِجات

هناك مصدر واحد فقط للحقيقة في قياس النص، هو وحدة القياس المبنية على مقاييس خطوط Canvas في Pretext، وكل مُخرِج (backend) يرسم من شجرة VDT المتقاربة نفسها التي أنتجتها.

هذا اختيار مقصود، وله سبب حاسم: **يجب أن تطابق طريقة قياس النص طريقة رسمه تمامًا.** تخيّل أن القياس يستخدم مقاييس خطوط Canvas، بينما يستخدم أحد المُخرِجات مكتبة PDF بجداول تقنين أزواج (kerning) مختلفة قليلًا. عندها لن يطابق الإخراج الناتج النهائي. فالأسطر التي قاسها المحرّك على أنها تتسع في 320px قد تفيض أو تقصر عند رسمها. وكل بكسل من الانحراف كذبة. وحين يُقاس النص مرة واحدة ثم يُرسم في كل مكان من الهندسة الناتجة، لا يمكن للمُخرِجات أن تختلف: فمواضع فواصل الأسطر وارتفاعات الأعمدة ومواضع الموارد تُثبَّت في VDT قبل أن يعمل أي مُخرِج.

لهذا السبب لا يعيد مُخرِج PDF، مثلًا، قياس النص: بل يستهلك VDT متقاربة سلفًا ويحوّل إحداثياتها بالبكسل إلى نقاط PDF. مقاييس Canvas هي مصدر الحقيقة، وPDF مجرد وسيلة نقل. ومن يستخدم `renderToPdf` (من الحزمة `postext-pdf`) يمرّر شجرة VDT نفسها التي كان سيمرّرها إلى `renderToCanvas` أو `renderToHtml`، والمخرجات الثلاثة مضمون تطابقها.

### واجهة البرمجة المتاحة (API)

المُخرِجات دوال بسيطة تعمل على `VDTDocument`، وليست تسلسلًا هرميًا من الأصناف. لا توجد واجهة `PostextBackend`، بل ثلاث نقاط دخول للرسم والدوال المساعدة التي يحتاجها كل هدف إخراج:

```typescript
// Canvas (from 'postext')
renderToCanvas(doc): HTMLCanvasElement[];               // one canvas per page
renderPage(page, doc): HTMLCanvasElement;               // a single page
renderPageToCanvas(page, doc, canvas, options?): void;  // draw into an existing canvas

// Canvas resource-image registry — decoded images keyed by Resource fileId
registerResourceImage(fileId, image): void;
unregisterResourceImage(fileId): void;
clearResourceImages(): void;

// HTML (from 'postext')
renderToHtml(doc, options?): string;
renderToHtmlIndexed(doc, options?): HtmlRenderIndex;    // per-page / per-block breakdown

// PDF (from 'postext-pdf')
renderToPdf(doc, options): Promise<Uint8Array>;

// EPUB (from 'postext-epub'), a book's chapters in order
renderToEpub(docs, options): Promise<Uint8Array>;
```

ولأن البيانات الثنائية للموارد تُحفظ خارج الشجرة، يحلّ كل مُخرِج معرّفات `fileId` بطريقته. يحتفظ مُخرِج Canvas بسجلّ لكائنات `CanvasImageSource` المفكوكة: يسجّل التطبيق المضيف كل صورة نقطية أو SVG مرة واحدة باستدعاء `registerResourceImage(fileId, image)`، ويبحث عنها المُخرِج وقت الرسم. أما مُخرِج HTML فيتلقى في خياراته دالة الحلّ `resourceImageUrl(fileId)`، ويُصدر وسوم `<img>` تشير إلى أي عناوين URL يعيدها المضيف (عناوين كائنات، أو data URI، أو مسارات CDN). ويتلقى مُخرِج PDF مزوّدًا `resourceBytes` ويضمّن البايتات الفعلية؛ ويتلقى كاتب EPUB مزوّدًا مماثلًا ويخزّن كل صورة مرة واحدة ملفًّا من ملفات الكتاب. ولا تحتاج موارد الجداول إلى شيء من هذا، فنموذجها مضمّن، وكل مُخرِج يرسم الخلايا من هندسة الجدول في VDT.

وتستحق `renderToHtmlIndexed` ملاحظة: فإلى جانب سلسلة HTML الكاملة، تعيد تفصيلًا لكل صفحة ولكل كتلة (`HtmlRenderIndex`) كي يقارن المستدعون بالرسم السابق ولا يرقّعوا إلا الأشجار الفرعية في DOM التي تغيّر HTML الخاص بها فعلًا، وهذا هو مسار المعاينة الحيّة في Sandbox.

### المُخرِجات

| المُخرِج | القياس | الرسم | الحالة |
| --- | --- | --- | --- |
| **Canvas** | Pretext (مقاييس خطوط Canvas) | رسم نقطي على `HTMLCanvasElement` (`renderToCanvas`، `renderPage`، `renderPageToCanvas`) | **متاح** |
| **HTML** | Pretext (المقاييس نفسها التي يستخدمها Canvas) | عُقد DOM ذات تموضع مطلق مع CSS تحريري (`renderToHtml`، `renderToHtmlIndexed`) | **متاح** |
| **PDF** | يستهلك VDT المقيسة سلفًا بواسطة Pretext | بناء صفحات PDF عبر pdf-lib مع تضمين الخط لكل وزن على حدة (`renderToPdf` في `postext-pdf`) | **متاح** |
| **EPUB** | يستهلك VDT المقيسة سلفًا بواسطة Pretext | ملف EPUB 3: مستند XHTML واحد لكل صفحة مطبوعة (تخطيط ثابت) أو لكل فصل (نص متدفق)، مع تضمين الخطوط (`renderToEpub` في `postext-epub`) | **متاح** |
| **من جهة الخادم** | Pretext + node-canvas | رسم بلا واجهة (headless) للعرض من جهة الخادم (SSR) أو للتوليد الدفعي | مستقبلي |

تستهلك المُخرِجات الأربعة المتاحة `VDTDocument` نفسها. والفصل بين `postext` (التي تصدّر مُخرِجَي Canvas وHTML) و`postext-pdf` (التي تصدّر مُخرِج PDF) سببه الاعتماديات وحدها: فمسار PDF يجلب `pdf-lib` و`@pdf-lib/fontkit`، ومعظم عمليات التكامل على الويب لا تحتاج إليهما. ثبّت `postext-pdf` فقط حين تريد فعلًا إصدار بايتات PDF. و`postext-epub` حزمة مستقلة للسبب نفسه، ولا تُحتاج إلا للكتب الإلكترونية.

**قيد العمل في المتصفح وحده:** في المرحلة 1، تجري كل حسابات الإخراج على جهة العميل داخل المتصفح. يمكن تشغيل خط المعالجة (pipeline) إما على الخيط الرئيسي (`buildDocument`) أو داخل Web Worker مخصّص (`createLayoutWorker` من `postext/worker`)، ومسار العامل هو طريقة التكامل الموصى بها للتطبيقات التي تقودها واجهة المستخدم، لأنه يُبعد القياس وحلقة التقارب عن الخيط الرئيسي، ويدعم الإلغاء بقاعدة «الأحدث يفوز» (last-wins) عبر `AbortSignal`، ويملك ذاكرته المؤقتة الخاصة للقياس وذاكرة مؤقتة لصور المعادلات النقطية، فتبقى عمليات إعادة البناء المتتالية رخيصة. انظر [الإعدادات › تشغيل الإخراج في Web Worker](/ar/docs/configuration#تشغيل-الإخراج-في-web-worker) لنمط التكامل الكامل. ويبقى العرض من جهة الخادم قرارًا مقصودًا مؤجلًا: أتقن تجربة المتصفح أولًا، ثم توسّع إلى أهداف أخرى لاحقًا.

## استراتيجية الأداء

الفرق بين أداة بطيئة وأخرى تبدو سحرية يقارب 10 أضعاف. فإخراج يستغرق 500ms يعني أن المستخدم يرى تقطّعًا ظاهرًا في كل مرة يغيّر فيها حجم النافذة. أما إخراج يستغرق 50ms فيبدو فوريًا، كأن المستند كان هناك دائمًا. ولا يمكن ترقيع هذا الفارق لاحقًا، بل يجب تصميمه منذ اليوم الأول.

انظر إلى ما يواجهه المحرّك: آلاف الكتل النصية عبر مئات الصفحات، مع احتمال إعادة حساب الإخراج كله عند كل تغيير في حجم نافذة العرض. وهذه هي فئة المشكلات نفسها التي تواجهها محرّكات الألعاب: معالجة آلاف الكائنات (الهندسة والفيزياء والإضاءة والذكاء الاصطناعي) 60 مرة في الثانية. وهي تحلّها ببنية خط معالجة (تمريرات متعددة على حالة مشتركة قابلة للتعديل، كل تمريرة تنجز شيئًا واحدًا بسرعة) وبتجنّب صارم للعمل غير الضروري (الاستبعاد، وأعلام الاتساخ، والتقسيم المكاني). ويستعير Postext كل واحدة من هذه الأفكار.

### المبادئ

1. **الحساب في الذاكرة.** تتسع شجرة VDT كاملة في الذاكرة. لا قراءات من DOM أثناء الإخراج. ولا يُلمس DOM
   إلا في النهاية تمامًا، أثناء الرسم.

2. **تتبّع الاتساخ.** تحمل الكتل علم `dirty`. وتتخطى التمريرات الأشجار الفرعية النظيفة. ولا تعيد حلقة
   التقارب التشغيل إلا من أبكر نقطة متّسخة.

3. **تقارب محدود.** الحد الأقصى البالغ 5 تكرارات ضمانٌ صارم. فأسوأ أداء ممكن
   متوقَّع وقابل للقياس.

4. **سرعة Pretext.** قياس النص بسرعة تفوق DOM بـ300 إلى 600 ضعف يعني أن المحرّك يستطيع
   إعادة قياس النص على سبيل التجربة (بتجربة عروض أعمدة مختلفة، ومواضع تقسيم الكلمات بالواصلة، وتعديلات
   التتبّع) دون أن يحجب الخيط الرئيسي.

5. **ذاكرة مؤقتة متعددة الطبقات للقياس.** تحتفظ وحدة القياس (`packages/postext/src/measure/`) بذاكرة
   مؤقتة صريحة للقياس مفتاحها النص والخطوط والعرض وكل خيار يؤثر في الإخراج،
   فوق ذاكرة `prepare()` المؤقتة الخاصة بـPretext وذاكرة مؤقتة خام لعرض النص. وإعادة قياس
   فقرة لم تتغير تكلّف بحثًا واحدًا في جدول. تُفرغ `clearMeasurementCache()` ذاكرة Pretext المؤقتة
   وذاكرة عرض النص، فتصحّ عروض الحروف بعد اكتمال تحميل الخطوط؛ وهي لا تأخذ أي
   وسيط ولا تمسّ ذاكرة القياس المؤقتة، التي تُستبدل بدلًا من ذلك بذاكرة جديدة.

6. **تقسيم الأسطر في المسار الساخن.** أُعيدت كتابة معالجة العُقد النشطة في Knuth-Plass طلبًا للسرعة:
   تُضغط مجموعة العُقد النشطة في مكانها كلما تقاعدت عُقد، وتُزال المرشّحات المكرّرة
   لكل زوج (سطر، فئة ملاءمة) فلا تبقى إلا العقدة ذات أدنى نقاط جزاء لكل مفتاح. والنتائج
   الخوارزمية متطابقة: مجموعات الفواصل نفسها، لكنها تُحسب أسرع.

7. **البناء خارج الخيط الرئيسي.** تشغّل نقطة الدخول `postext/worker` خط المعالجة كاملًا
   داخل Web Worker مخصّص. يرسل الخيط الرئيسي `{ content, config }`
   و`AbortSignal`؛ فيسجّل العامل الخطوط (المنقولة بصيغة `ArrayBuffer`)، ويشغّل
   حلقة التقارب، ثم يعيد `VDTDocument` المكتملة. وأي استدعاء أحدث لـ`build()`
   يلغي الاستدعاء السابق تعاونيًا: إذ يفحص العامل خطّاف إلغاء لكل كتلة
   داخل `buildDocument` ويرمي `BuildCancelledError`، فلا ينتظر مستخدم يكتب في محرّر
   إخراجًا تجاوزه الزمن. ويحتفظ العامل أيضًا بذاكرة مؤقتة دائمة خاصة به
   للقياس، وبذاكرة مؤقتة لصور المعادلات النقطية مفتاحها المحتوى، كي تبقى كائنات `MathRender`
   المنسوخة بالاستنساخ البنيوي (structured clone) عبر عمليات إعادة البناء دون إعادة تحويلها إلى صور نقطية.

8. **حقول رقمية مسطّحة.** تُخزَّن الصناديق المحيطة حقولًا مسطّحة `x, y, width, height` على
   كل عقدة، لا كائنات متداخلة. وهذا يتجنّب تتبّع المؤشرات ويلائم الذاكرة المخبئية أكثر.

9. **VDT مزدوجة الوصول.** تتيح الشجرة (`pages > columns > blocks`) وصولًا هرميًا للتمريرات
   التي تعمل صفحة بصفحة أو عمودًا بعمود (مثل التمريرة 6، موازنة الأعمدة).
   وتتيح مصفوفة مسطّحة موازية `blocks[]` وصولًا مفهرسًا بزمن O(1) للتمريرات التي تحتاج إلى المرور على
   كل الكتل بصرف النظر عن موقعها (مثل التمريرة 5، كشف الأسطر الأرامل واليتيمة). وكلا العرضين
   يشير إلى كائنات الكتل نفسها (لا تكرار، بل طريقتان لاجتياز
   البيانات نفسها).

### التعامل مع تغيير الحجم

حين يغيّر المستخدم حجم نافذة العرض، لا يعيد المحرّك البناء من الصفر. بل يحدّث عروض الأعمدة في VDT، ويعلّم كل الكتل النصية بأنها متّسخة، ويعيد تشغيل خط المعالجة من التمريرة 2. وتُعاد استعمال بنى الصفحات والأعمدة.

هنا تؤتي VDT القابلة للتعديل ثمارها. فبدلًا من التخلص من الإخراج كله والبدء من الصفر، يعيد المحرّك استعمال أكبر قدر ممكن من العمل. تظل نتائج `prepare()` في Pretext صالحة، لأنها تعتمد على الخط ومحتوى النص لا على العرض، فلا تحتاج إلى إعادة التشغيل إلا استدعاءات `layout()` الرخيصة. ويمكن إعادة إخراج مستند من 50 صفحة كاملًا بإعادة قياس كل الكتل النصية (وهذا سريع لأن `prepare()` محفوظة في الذاكرة المؤقتة) وإعادة تشغيل التمريرات 3 إلى 7، دون إعادة تحليل Markdown أو إعادة حلّ الإحالات. يسحب المستخدم حافة النافذة فيتبعها الإخراج في الزمن الحقيقي.

### قياس الأداء منذ اليوم الأول

يمكن قياس أداء كل تمريرة على حدة بمعزل عن غيرها، باستخدام واجهة `bench` في vitest:

```typescript
// Example benchmark
bench('layout 50-page document', () => {
  const vdt = createVDT(fiftyPageContent, config);
  runPipeline(vdt);
}, { time: 100 }); // sample for 100ms and report ops/sec
```

تحفّظ من باب الأمانة: `{ time: 100 }` هو المدة التي *يأخذ فيها* vitest عيّنات من القياس، وليس عتبة نجاح أو فشل؛ فاختبارات قياس الأداء تُبلغ عن أرقام ولا تُفشل البناء. وتُكتشف حالات التراجع بمقارنة هذه الأرقام بين التشغيلات حين يتغير مسار ساخن (كما جرى عند إعادة كتابة معالجة العُقد النشطة في Knuth-Plass)، لا عبر بوابة آلية في CI. الأداء ميزة لا أمنية، لكن ضمانه اليوم هو القياس والمراجعة، لا خط معالجة يفشل.

## تدفق البيانات

> **شكل: تدفق البيانات عبر المحرّك**
> يدخل المحتوى والإعدادات إلى التمريرة 1 (التحليل) والتمريرة 2 (القياس). وتعمل التمريرات من 3 إلى 7 داخل حلقة التقارب. وبعد التقارب تُسلَّم VDT إلى المُخرِج، الذي يرسم HTML أو PDF.
>
> *تدفق البيانات من البداية إلى النهاية: تحليل، قياس، تقارب، رسم.*

## خارج النطاق

كل حدّ من هذه الحدود اختيار مقصود: فالمحرّك معقّد بما يكفي وحده،
وتحمّل مسؤوليات مكانها في موضع آخر هو أسرع طريق إلى عدم إنجازه أبدًا.

- **العرض من جهة الخادم.** يجري الإخراج كله في المتصفح. يعتمد المحرّك على مقاييس خطوط Canvas (عبر Pretext)، وهي تتطلب بيئة متصفح. قد يأتي لاحقًا مُخرِج من جهة الخادم يستخدم `node-canvas`، لكنه ليس جزءًا من التصميم الأولي. المتصفح أولًا.
- **التحرير المرئي المباشر (WYSIWYG).** Postext محرّك إخراج، لا محرّر. محتوى يدخل، وهندسة تخرج. وبناء سطح تحرير تفاعلي (إدارة المؤشر، والتحديد، والتراجع والإعادة، ومعالجة الإدخال) مشكلة منفصلة تمامًا. يمكن أن يكون Postext مُخرِج الرسم لمحرّر، لكنه لا يوفّر بنفسه إمكانات التحرير.
- **غلاف لـ column-count في CSS.** يحلّ Postext محلّ تخطيط الأعمدة المتعددة في CSS، ولا يغلّفه. فهو يحسب هندسة دقيقة التموضع من الصفر، لأن خوارزمية تخطيط الأعمدة في المتصفح تفتقر إلى التحكم في مواضع الموارد، ومنع الأسطر الأرامل واليتيمة، والقواعد الطباعية العابرة للأعمدة. وهذه هي الغاية كلها.
- **إدارة نقاط التوقف المتجاوبة (breakpoints).** يحسب Postext الإخراج عند مقاس صفحة معيّن. والمستهلك هو من يقرر متى يعيد الإخراج (عند تغيير حجم نافذة العرض، أو عند تغيّر الاتجاه). لا يدير Postext نقاط التوقف ولا استعلامات الوسائط ولا قرارات التصميم المتجاوب. فهذه مهمتك أنت.
- **التحرير التعاوني في الزمن الحقيقي.** Postext عملية حساب إخراج عديمة الحالة (محتوى يدخل، وهندسة تخرج)، لا نظام مستندات تعاوني فيه حلّ للتعارضات أو تحويلات تشغيلية (operational transforms) أو إدراك لتعدد المستخدمين.
- **تحميل الخطوط أو إدارتها.** يفترض Postext أن الخطوط محمّلة سلفًا ومتاحة للقياس. وتحميل الخطوط وسلاسل الخطوط البديلة وتجزئة الخطوط (subsetting) من مسؤولية المستهلك. وإن لم يكن الخط محمّلًا حين يقيس Postext النص، فستستخدم القياسات الخط البديل للمتصفح، وسيكون الإخراج خاطئًا بعد تحميل الخط الحقيقي. حمّل خطوطك أولًا.

## ملحق: العلاقة بالأنواع الموجودة

هكذا تقابل الأنواع الرئيسية المعرّفة في `packages/postext/src/types.ts` البنية الموصوفة أعلاه:

| النوع | الدور في البنية |
| --- | --- |
| `PostextContent` | نقطة الدخول: مُدخل المحرّك (التمريرة 1) |
| `PostextConfig` | يتحكم في كل سلوك خط المعالجة عبر كل التمريرات |
| `Resource` | مورد محدّد النوع: صورة نقطية أو SVG أو جدول. يصبح `ResolvedResourceBlock` أثناء القياس، ثم إما `VDTBlock` مضمّنًا من النوع `'resource'` (الموضع `'here'`) أو عنصرًا عائمًا في شريط من الصفحة (التمريرة 4) |
| `ResourceType` | يقود الترقيم حسب النوع، وبادئات التعليقات، وتسميات الإحالات، والموضع الافتراضي للعنصر العائم (التمريرة 1، التمريرة 4) |
| `ResourcePlacement` | تجاوز لموضع العنصر العائم خاص بكل مورد: `position` (`'top'` / `'bottom'` / `'here'`) و`span` (`'column'` / `'page'`)، ويُحلّ في التمريرة 4 |
| `PostextNote` | لا يقرؤه المحرّك. تُكتب الحواشي السفلية في Markdown (`[^id]`) وتُنضَّد أسفل العمود الذي يستشهد بها أو بعد الفصل؛ أما الحواشي الجانبية فغير منفّذة |
| `PostextResource` | **مُهمَل.** مورد نموذج المحتوى القديم، يُبقى عليه فقط إلى أن ينتقل آخر مرجع إليه في المُخرِجات (`VDTBlock.resource`) إلى نموذج `Resource` |
| `PlacementStrategy`، `ColumnConfig`، `TypographyConfig`، `ResourcePlacementConfig`، `ReferenceConfig`، `PostextSectionOverride` | **قديم.** مُعلَن لكنه لم يُربط قط بخط المعالجة؛ حلّت محلّه `bodyText`/`headings` (الطباعة)، و`layout` (الأعمدة)، ونموذج العناصر العائمة في `Resource` (المواضع) |

### أين تقع أنواع VDT

تقع أنواع VDT (`VDTDocument`، `VDTPage`، `VDTColumn`، `VDTBlock`، `VDTLine`، `VDTLineSegment`، `ResolvedResourceBlock`، `VDTResourceTableLayout`، `BoundingBox`، وأخواتها) في `packages/postext/src/vdt.ts`، إلى جانب دوال المصنع المساعدة (`createVDTDocument`، `createVDTPage`، `createVDTBlock`، …) و`computePageTextExtent`. لا توجد وحدة منفصلة لواجهة المُخرِجات، فنقاط دخول الرسم الموصوفة في [واجهة المُخرِجات](#واجهة-المُخرِجات) تُصدَّر مباشرة من `postext` (Canvas، HTML) ومن `postext-pdf` (PDF).
