# الاستخدام البرمجي: HTML وFolio وEPUB

> الكتاب المُخرَج داخل صفحة ويب، وكتابًا ثلاثي الأبعاد مع postext-folio، وبصيغة EPUB 3 ثابتة التخطيط أو متدفقة مع postext-epub

- نسخة HTML: https://postext.dev/ar/docs/configuration-html-folio-epub
- آخر تحديث: 2026-10-16
- مدة القراءة: 17 دقيقة
- لغات أخرى: [en](https://postext.dev/en/docs/configuration-html-folio-epub.md), [es](https://postext.dev/es/docs/configuration-html-folio-epub.md), [ca](https://postext.dev/ca/docs/configuration-html-folio-epub.md), [de](https://postext.dev/de/docs/configuration-html-folio-epub.md), [fr](https://postext.dev/fr/docs/configuration-html-folio-epub.md), [it](https://postext.dev/it/docs/configuration-html-folio-epub.md), [pt](https://postext.dev/pt/docs/configuration-html-folio-epub.md), [zh](https://postext.dev/zh/docs/configuration-html-folio-epub.md), [ja](https://postext.dev/ja/docs/configuration-html-folio-epub.md)

## باختصار

هذه الصفحة لمن يكتبون الشيفرة ويريدون عرض كتاب على الشاشة. يضع القسم الأول الصفحات داخل صفحة ويب، ويشرح كيف يختلف هذا العرض عن Canvas وPDF. ويعرض القسم الثاني الكتاب بالأبعاد الثلاثة، مفتوحًا على مكتب، بصفحات يقلّبها القارئ. ويصنع القسم الأخير كتابًا إلكترونيًا بصيغة EPUB، بصفحات ثابتة أو بنص يتدفق ليناسب الشاشة، ثم يفحص الملف.

## دمج عارض 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`**: ذاكرات مؤقتة قابلة للتوصيل تتيح لك إعادة استخدام القياسات عبر عمليات إعادة الإخراج.
- **`prepareFonts` / `buildDocumentWithFonts` / `watchFonts` / `onFontsChanged`**: تحمّل أوجه المستند قبل الإخراج وتعيد الإخراج حين تصل أوجه جديدة (انظر [تحميل الخطوط قبل الإخراج](https://postext.dev/ar/docs/configuration-programmatic-usage.md#تحميل-الخطوط-قبل-الإخراج)).

### مثال حيّ: سلسلة HTML

الدورة الكاملة بلغة JavaScript المجرّدة، قبل التكامل مع React أدناه: ابنِ المستند، وسلّم `VDTDocument` إلى `renderToHtml`، وضع السلسلة في حاوية. يرصّ `mode: 'single'` الصفحات عموديًا؛ ويمنحها `background` لونًا، لأن الصفحات شفافة افتراضيًا. ويطبع المثال على CodePen أيضًا الترميز المولَّد، فترى الأسطر ذات المواضع المطلقة التي يُنتجها المُخرِج، والتي يرسمها المتصفح دون أن يعيد تدفّقها أبدًا.

> **مثال قابل للتشغيل: Postext · تحويل مستند إلى HTML** — أخرِج مستند markdown باستخدام postext وحوّله إلى سلسلة HTML. ([الشيفرة](https://github.com/drnachio/postext/tree/main/docs/examples/render-html))

### التكامل الأدنى

المقتطف أدناه أقصر تكامل مفيد: ابنِ المستند بمقاس مساحة العرض الحالي، وارسمه في حاوية، وأعد التشغيل عند تغيّر المقاس.

```tsx
import { useEffect, useRef } from 'react';
import {
  buildDocument,
  renderToHtmlIndexed,
  resolveHtmlViewerConfig,
  buildFontString,
  measureGlyphWidth,
  dimensionToPx,
  createMeasurementCache,
  watchFonts,
  onFontsChanged,
} 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 stopWatching = watchFonts();
    const off = onFontsChanged(() => relayout());

    return () => {
      ro.disconnect();
      off();
      stopWatching();
    };
  }, [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`.
- **مراعاة تحميل الخطوط.** تستمع `watchFonts` إلى `document.fonts`، وتطرح مرة في كل إطار ما قيس بالعائلات التي وصلت أوجهها؛ ثم تعيد `onFontsChanged` إخراج العمود. ومن دون إعادة الإخراج، يستخدم العرض الأول مقاييس خط بديل ثم يقفز حين يصل الخط الحقيقي.
- **إعادة استخدام ذاكرة القياس المؤقتة.** إنشاء الذاكرة مرة واحدة لكل مكوّن يعني أن تغيّر المقاس وتغيّر حجم الخط يعيدان استخدام قياسات العرض السابق بدلًا من إعادة قياس كل فقرة.

### أبعد من ذلك

المثال أعلاه مبسّط عن قصد. وعادة ما تضيف عمليات التكامل في بيئة الإنتاج ما يلي:

- **العزل عبر Shadow DOM**: ارسم داخل `host.attachShadow({ mode: 'open' })` كي لا يتسرّب أي CSS من الصفحة الخارجية إلى العارض.
- **الترقيع التدريجي**: تُعيد `renderToHtmlIndexed` القيمة `pages[i].blocks`، لكل منها `id` ثابت وHTML الخارجي للكتلة. وحين لا تختلف بين عمليتَي عرض إلا كتل قليلة، يمكنك استبدال أغلفة تلك الكتل في مكانها بدلًا من إعادة بناء `innerHTML`.
- **الطبقات العلوية**: وضع SVG ذي موضع مطلق فوق كل `.pt-page` للمؤشرات أو التحديدات أو شبكات خطوط الأساس.
- **الروابط**: تُغلَّف كلمات رابط Markdown في `<a href="…" rel="noopener noreferrer">`، الذي يأخذ لون النص بلا خط تحته؛ انظر [صيغة المستند › الروابط](https://postext.dev/ar/docs/document-format-inline.md#الروابط). وفي عارض شبيه بالمحرّر، اعترض النقرات على `a[href]` التي لا تبدأ بـ`#` وافتحها في تبويب جديد (فروابط `:ref` تشير إلى مواضع داخل المستند).
- **الصور أحادية الحبر**: حين يكون `diagramStyle.singleInk` مفعّلًا، تحصل عناصر `<img>` من نوع SVG على مرشّح CSS ما لم تمرّر `singleInk: false` لعناوين URL المُعاد تلوينها أصلًا؛ انظر [الحبر الواحد في Canvas وفي HTML](https://postext.dev/ar/docs/configuration-resources.md#الحبر-الواحد-على-اللوحة-canvas-وفي-html).

ويطبّق المكوّن `HtmlPreview` في Sandbox (`packages/postext-sandbox/src/viewport/HtmlPreview/index.tsx`) كل ذلك فوق الواجهة البرمجية نفسها المعروضة هنا، ويمكن استخدامه مرجعًا. وهو يمرّر أيضًا كل عملية بناء عبر عامل إخراج مشترك (انظر [تشغيل الإخراج في Web Worker](https://postext.dev/ar/docs/configuration-programmatic-usage.md#تشغيل-الإخراج-في-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](https://postext.dev/ar/docs/configuration-pdf-output.md#النص-العمودي-في-ملف-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`.

## كتاب ثلاثي الأبعاد (`postext-folio`)

تعرض `postext-folio` المستند المُخرَج على الشاشة كتابًا مطبوعًا مفتوحًا على مكتب: صفحات متقابلة وفق قاعدة الصفحة الفردية، وأوراق يقلبها القارئ بالزرّين ‹ ›، أو بمفاتيح الأسهم، أو بالسحب السريع، أو بالنقر على صفحة، أو بإمساك صفحة من حافتها وسحبها فوق الأخرى. تنثني كل ورقة في three.js وفق نوع ورقها، وتُلقي ظلًا حقيقيًا على الصفحات التي تحتها. ترسم لوحة WebGL الكتاب ساكنًا ومتقلّبًا على حد سواء، فلا يتغيّر مظهر الصفحة حين تستقر. وهي أداة العرض لوصفات دليل الوصفات ولتبويب **Folio** في Sandbox.

```bash
npm install postext postext-folio three
```

```ts
import { 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`](https://postext.dev/ar/docs/configuration-fonts-colors-viewers.md#عارض-folio-الإعدادات) في المستند (`doc.config.folio`). والصفحة المنضَّدة داخل مقطع `:::paper` تحمل نوع ورقها الخاص (`VDTPage.paper`)، وتُرسم ورقتها بلون ذلك الورق وسطحه وسماكته وصلابته. ومع `binding.cover: 'pages'` تكون الصفحة الأولى لوح الغلاف الأمامي، والأخيرة، حين تكون صفحة زوجية، لوح الغلاف الخلفي (`covers`). ومقاس الصحيفة (`'broadsheet'` و`'berliner'` و`'tabloid'` و`'compact'`) الذي لا تحدّد إعداداته نوع الورق ولا التجليد يظهر ورقَ صحف مطويًّا، وكذلك حين يمرّر المضيف `folio` خاصًّا به.
- **الحاوية تحدّد الحجم.** يملأ الكتاب الحاوية، مع الأزرار وعدد الصفحات في الهوامش، فأعطِ الحاوية ارتفاعًا؛ وتغيير الحجم يعيد رسم الصفحات بالحجم الجديد. وتحت عرض 560 px يعرض صفحة واحدة في كل مرة (`mode: 'auto'`؛ و`'single'` و`'double'` يفرضان أحد الوضعين): يمتد الكعب على طول الحافة الداخلية للصفحة وتنقلب الورقة فوقه، والسحب نحو الكعب يقلب إلى الأمام، والسحب السريع بعيدًا عنه يعود إلى الخلف، والنقرة تقلب.
- **ما يفعله المؤشر.** يحدّد `interaction` (و`setInteraction` لاحقًا) ما يفعله الزر الأيسر أو الإصبع الواحدة أو القلم على الكتاب: `'hand'` (الافتراضي) يمسك الصفحات ويقلبها، و`'orbit'` يدير المنظر كما يفعل السحب بالزر الأيمن (للوحات اللمس والأجهزة اللوحية)، و`'select'` يترك المؤشر للمضيف، لتحديد النص مثلًا. وتعطي `pageAt(event)` الصفحة الواقعة تحت المؤشر والموضع عليها (`{ page, x, y }`، كسورًا من الصفحة بدءًا من زاويتها العلوية اليسرى)، على الكتاب كما يُرى، مائلًا أو مُدارًا؛ وتسلك `pointOnScreen(point)` الاتجاه المعاكس، لرسم مؤشر الكتابة (caret) أو التحديد فوق الصفحة. وتعرض `refreshPage(src)` من جديد لوحة صفحة أعاد المضيف رسمها في مكانها. ويستخدمها Sandbox كلها لتحديد النص وتتبّع مؤشر الكتابة في المحرر على الصفحات ثلاثية الأبعاد.
- **عدسة مكبّرة للحروف الصغيرة.** يضع `interaction: 'magnify'` عدسة مستديرة بإطار أسود فوق الكتاب حيث المؤشر (ويرفعها الإصبع فوقه ما دام يلمس الشاشة). تُظهر الكتاب كما تراه عين القارئ، بإضاءته وانحنائه، وتكبّر الوسط أكثر ثم تنحني نحو الإطار. تغيّر العجلة و`+` و`−` درجة التكبير (`setMagnification(zoom)`، من 1.5 إلى 10؛ والافتراضي يُظهر الصفحة بنحو 5.5 بكسل CSS للمليمتر)، ويُبعدها مفتاح Esc؛ ويضبط `magnifier: { zoom, diameter }` الاثنين من البداية. يعيد `createFolioFromDocument` رسم الصفحات تحت العدسة بدقة تكفي وسطها، فيُقرأ متن الجريدة؛ ويأخذ `createFolio` هذه الرسوم من `detail: { paint(index, deviceWidth), release() }`. يضعها الـSandbox على زر **العدسة المكبّرة** (M). وتحدّد العدسة النصّ أيضًا: فوق الصفحات يصير المؤشّر مؤشّر نصّ، وتُعيد `pageAt` النقطة التي تحت وسط العدسة (فوق الإصبع على الشاشة اللمسية)، وفي الـSandbox تضع النقرة هناك مؤشّر الكتابة ويحدّد السحب النصّ.
- **يستطيع القارئ أن يدور حوله.** السحب بالزر الأيمن يدير المنظر حول الكتاب (حتى 70° بعيدًا عن المنظر العمودي من الأعلى)، حتى أثناء تقلّب الأوراق؛ وتعيده `resetView()` تدريجيًا إلى قيمتَي `tilt` و`yaw` في الإعدادات، وتعطي `getView()` المنظر كما يُرى الآن (`{ tilt, yaw }`، بالدرجات) لحفظه في تلك الإعدادات. ويضعها Sandbox على زرّين، **إعادة ضبط العرض** و**حفظ بوصفه العرض الافتراضي**.
- **مقاطع الفيديو تُشغَّل على الصفحات.** يشغّل النقر على غلاف فيديو المقطعَ على الصفحة نفسها، في كل أوضاع `interaction`، ويستمر تشغيله والورقة تنقلب؛ ونقرة أخرى توقفه مؤقتًا، ويتوقف حين يستقر الكتاب على صفحتين متقابلتين لا تعرضانه. والفيديو الذي يحمل `player.autoplay` يبدأ وحده أول مرة تُعرض فيها صفحتاه؛ وإن كان يعمل أيضًا إلى جانب غيره (`player.exclusive: false`) بدأ صامتًا في كل مرة وتوقف حين تُقلب الصفحتان، وتعمل عدة مقاطع معًا. والخيارات هي `videos` و`videoUrl` و`onVideo`، ومعها `stopVideo()` في أداة العرض؛ انظر [صيغة المستند › مقاطع الفيديو على صفحات Folio](https://postext.dev/ar/docs/document-format-resources.md#مقاطع-الفيديو-على-صفحات-folio).
- **الخطوط والصور أولًا.** كما في `renderPage`، يجب أن تكون الوجوه التي يستخدمها المستند محمّلة في `document.fonts`، وصور موارده مسجّلة بـ `registerResourceImage` قبل رسم الصفحات.
- **ميسَّر الوصول.** أداة العرض مجموعة قابلة للتركيز تستجيب لـ ←/→ (معكوستين في الكتاب المجلّد من اليمين)، وPage Up/Down، وHome وEnd؛ وأزرارها وعدد صفحاتها موسومة (ويترجمها `labels`)، وتحمل كل لوحة صفحة نصًا بديلًا `alt` (`alt: (index) => …`).
- **دون WebGL2**، أو حين يطلب القارئ تقليل الحركة، تتبدّل الصفحات المتقابلة ببساطة. كتب WebGL ثقيلة على الهاتف (نسيج لكل وجه من وجهَي الورقة): لذا لا يعرض Sandbox تبويب Folio إلا حيث يتوفر WebGL2 ولا يقل الضلع الأقصر للشاشة عن 600 px. وتخبرك `canFlip()` هل ستنقلب الأوراق بثلاثة أبعاد هنا: WebGL2، ولا تقليل للحركة.

### المظهر

يتجاوز الخيار `appearance`، و`setAppearance` لاحقًا، ما يقوله المستند. وكل ما يُترك يحتفظ بقيمة المستند:

```ts
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`](https://postext.dev/ar/docs/configuration-fonts-colors-viewers.md#عارض-folio-الإعدادات): الميل، والورق، والتجليد، والسطح، والإضاءة. تحل محل إعدادات المستند حين تُعطى. |
| `pageWidthMm` | عرض قص الصفحة بالمليمتر، الذي تُقاس عليه سماكة الورق ولوحا الغلاف. من المستند: الصفحة المقصوصة بدقتها (dpi). الافتراضي 150 في `createFolio`. |
| `extraPages` | `{ before, after }`: صفحات من الكتاب خارج الصفحات المعطاة، تُحسب في سماكة كتلتَي الصفحات ولا تُرسم أبدًا. |
| `covers` | `{ front, back }`: الصفحة الأولى المعطاة هي الغلاف الأمامي، والأخيرة الغلاف الخلفي (حين تقع على صفحة زوجية). تنقلبان لوحَي غلاف، ولا يُرسم غلاف مقوّى حولهما. من المستند: `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 للحزمة](https://www.npmjs.com/package/postext-folio).

### مثال حي: كتاب ثلاثي الأبعاد

يستورد هذا المثال على CodePen الحزمتين `postext` و`postext-folio` من شبكة CDN، وينضّد مستندًا قصيرًا ويفتحه كتابًا. أمسك الصفحة اليمنى من حافتها واسحبها فوق الأخرى.

> **مثال قابل للتشغيل: Postext · مستند في هيئة كتاب ثلاثي الأبعاد** — نضّد مستند Markdown باستخدام postext واقلب صفحاته بثلاثة أبعاد باستخدام postext-folio. ([الشيفرة](https://github.com/drnachio/postext/tree/main/docs/examples/render-folio))

### مثال حي: صور الصفحات

`createFolio` مع صفحات مرسومة على لوحات canvas، وصفحة أخيرة فارغة، ولون الورق.

> **مثال قابل للتشغيل: Postext · كتاب صور ثلاثي الأبعاد** — اقلب أي صفحات بثلاثة أبعاد باستخدام postext-folio: عناوين صور، أو عناصر img أو canvas. ([الشيفرة](https://github.com/drnachio/postext/tree/main/docs/examples/folio-images))

## كتب EPUB (`postext-epub`)

تكتب `postext-epub` الكتاب المُخرَج ملفَّ EPUB 3.3، في المتصفح أو في Node، بلا خادم. وتقرأ مستندات الفصول نفسها التي تأخذها `renderToPdf` للكتاب، فتصل أرقام الصفحات والحواشي والاستشهادات والإحالات المرجعية وجدول المحتويات والفهرس الأبجدي محلولةً، وتعيد الملف بايتات. وهي الكاتب الذي يقف وراء تبويب **EPUB 3** في Sandbox.

```bash
npm install postext postext-epub
```

`postext` تبعية نظيرة، كما في `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 لأنظمة القراءة الأقدم.

### كتابة كتاب

```ts
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?, svgFonts?, 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`، وتستبدل به أنظمة القراءة خطوطها الخاصة. ولا تضمّن إلا الخطوط التي تسمح تراخيصها بذلك: فالوجه الذي يحمل `redistributable: false` لا يُكتب في الملف أبدًا (قد يُنضَّد به الكتاب، لكن ملفه يبقى خارجه، وخارج صور SVG أيضًا).
- **`resourceBytes(fileId)`**: الصور التي تضعها الصفحات، متزامنةً أو غير متزامنة، في هيئة `{ bytes, mediaType }`؛ و`mediaType` الفارغ يُستنتج من البايتات. أعطِ الصور النقطية كما خُزّنت، وملفات SVG بمصدرها، لا بنسخة PDF الأصلية للطباعة (`svg.pdfFileId`). تُخزَّن كل صورة مرة واحدة. والكتاب أحادي الحبر (`diagramStyle.singleInk`) يُعاد تلوين ملفات SVG فيه داخل الملف. ويُضمَّن في كل SVG أوجه الخطوط التي يسمّيها نصه، لأن نظام القراءة يعرضه صورةً لا ترى خطوط الكتاب (انظر [الخطوط في نصوص SVG](https://postext.dev/ar/docs/configuration-resources.md#الخطوط-في-نصوص-svg)): من `fonts` (الشرائح التي تحوي حروفه)، ثم من **`svgFonts.provider`** للعائلة التي لا يستخدمها نص الكتاب. وتبقى خارج الملف عائلات الأوجه المعلَّمة بـ `redistributable: false` والعائلات التي يسمّيها `svgFonts.withhold(family)`، ويُبلَّغ عن كل منها مرة واحدة بتحذير `fontWithheld`؛ ويُبلَّغ عن العائلة التي لا وجه لها بتحذير `svgFontUnavailable`، وعن الأوجه التي تتجاوز `svgFonts.maxBytes` (2 MiB) بتحذير `svgFontsTooLarge`. ويُبقي `svgFonts.inline: false` و`diagramStyle.inlineFonts: false` و`svg.inlineFonts: false` الخاص بالمورد البايتاتِ كما أُعطيت. والصورة التي لا بايتات لها يُبلَّغ عنها بتحذير `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 للحزمة](https://www.npmjs.com/package/postext-epub).

### فحص ملف باستخدام EPUBCheck

[W3C EPUBCheck](https://www.w3.org/publishing/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، والإعدادات المسبقة للعرض، والكتب الصينية والعربية، وكتب دليل الوصفات)، وكل منها يجتاز الفحص بلا أخطاء ولا تحذيرات.
