انتقل إلى المحتوى الرئيسي
الوصفة رقم 122

دليل الوصفات · الفصل 2 · الحروف والنص

نص 漢文 بعلامات قراءته، وقراءته اليابانية

ثلاثة أقوال من «المختارات» ورباعية من عصر تانغ كما ينضّدها كتاب قراءة ياباني: علامات العودة والـokurigana بلون الزنجفر، والقراءة بالكانا تحت كل مقطع.

في هذه الصفحة
المُخرَج
Canvas · PDF
المستوى
متوسط
Postext
اختُبرت مع Postext 1.16.1
تتطلب ≥ 1.16.1 · postext-pdf ≥ 1.16.1
الترخيص
حُدّثت في 5 أكتوبر 2026
الكود MIT · النص CC BY 4.0

ص 一 · 1 من 3

  • نموذج باللغة الإنجليزية: لا توجد طبعة عربية بعد
  • مقاس القص 148 × 210 مم
  • عمود واحد
  • Klee One 10.5/21
  • Noto Sans JP
  • Zen Old Mincho
  • 3 صفحات
  • المستوى
  • Postext 1.16.1
  • أُخرجت في 42 ms
  • أسطر الكود: 132

باختصار

الصينية الكلاسيكية كما يتعلمها القرّاء اليابانيون: علامات حمراء صغيرة بجانب الحروف تخبر القارئ بأي ترتيب يقرؤها وأي لواحق يابانية يضيف، وتلي كلَّ مقطع قراءتُه اليابانية كاملة.

ما الذي ستنضده

ثلاث صفحات من كتاب قراءة مدرسي ياباني في 漢文 (kanbun)، أي الصينية الكلاسيكية مقروءةً باليابانية، على صفحة A5 مجلَّدة من اليمين: ثلاثة أقوال من «المختارات» ورباعية منغ هاوران «فجر الربيع». يُنضَّد كل مقطع بخط Zen Old Mincho بحجم 15 pt بالترتيب الصيني، وعلاماته 訓点 (kunten) بجانب الحروف بلون الزنجفر وبنصف حجمها: علامات العودة (返り点) أسفل يسار الحرف تطلب من القارئ أن يتابع ثم يعود إليه، والـokurigana (送り仮名) يمينه تضيف اللواحق اليابانية. وتحت كل مقطع يجري 書き下し文 (yomikudashi)، أي النص نفسه مكتوبًا باليابانية، بخط Klee One، وهو خط الكتب المدرسية، مع الفوريغانا حيث تصعب القراءة. والكلاسيكيات الصينية بأدوات قراءتها الخاصة هي رقم 080.

تجيب هذه الوصفة عن

  • كيف أطبع الكانبون بعلامات الرجوع والـ okurigana؟
  • كيف أضيف الفوريغانا فوق الكانجي، قراءة لكل حرف أو قراءة واحدة للكلمة كلها؟

الجواب المختصر

script.js · الأسطر 32–52في الكود الكامل
// :kunten[習]{kaeri="レ" okuri="フ"} sets the return mark small at the character's lower
// left and the okurigana at its lower right, down the line; the text keeps its Chinese
// order, which the reader follows by the marks. cjk.kunten sizes and colours them: half
// the character, in vermilion. The Japanese reading under each passage is plain kana and
// kanji with furigana: {君子|くん|し} gives each character its reading (jukugo ruby).
const cjk = {
  grid: { enabled: true, charsPerLine: CHARS, linesPerPage: LINES },
  kunten: { fontSize: em(0.5), color: col('vermilion'), placement: 'inline' },
};
const paragraphStyles = [
  // The text: 15 pt on a pitch of two grid lines, room for the okurigana beside it. A
  // quatrain takes two lines, a couplet to the line with a space between its verses.
  { id: 'kanbun', fontFamily: MINCHO, fontSize: pt(KANBUN), lineHeight: pt(2 * LEAD),
    firstLineIndent: em(0), textAlign: 'justify' },
  { id: 'shi', fontFamily: MINCHO, fontSize: pt(KANBUN), lineHeight: pt(2 * LEAD),
    indent: em(2), firstLineIndent: em(0), marginTop: pt(0), marginBottom: pt(0) },
  // The reading (書き下し文), two characters down in a school-book hand.
  { id: 'kudashi', indent: em(2), firstLineIndent: em(0), marginTop: pt(LEAD) },
  { id: 'kudashi-shi', indent: em(4), firstLineIndent: em(0), marginTop: pt(0),
    marginBottom: pt(0) },
];

المكونات

الخطوط
Zen Old Mincho, Klee One, Noto Sans JP (SIL OFL 1.1)
الأصول
لا شيء: كل صورة مرسومة بالكود

طريقة التحضير

#1 · علّم كل حرف بـ:kunten

الشيفرة هي الجواب المختصر أعلاه. يعطي :kunten[習]{kaeri="レ" okuri="フ"} الحرف 習 علامة العودة レ والـokurigana フ؛ ويحتفظ النص بالحرف، فيقرأ البحث والنسخ وملف PDF المقطع كما كُتب، ولا تأخذ العلامات أي حرف منه (العلامات والروبي والواريتشو). اقرأ 学ビテ而時ニ習フレ之ヲ: ترسل レ التي على 習 القارئ إلى 之 أولًا ثم تعيده، فيقرأ 之を習ふ؛ ولا علامة على 而 فلا يُقرأ. في النص العمودي تقف علامة العودة أسفل يسار حرفها وتأخذ نصف حرف من السطر، وتقف الـokurigana يمينه، ابتداءً من منتصف الحرف نزولًا، كما يضعها JIS X 4051. ويضبط cjk.kunten حجمها، نصف حجم النص، ولونها، الزنجفر الذي يكتبها به القارئ؛ والمقطع بحجم 15 pt على سطرين من الشبكة، فيبقى بجانب كل سطر 27 pt للـokurigana.

#2 · اقفز بـ一 二 وبـ一レ

تُرجع الأرقام القارئ أبعد من ذلك. في 不二亦タ説バシカラ一乎 يتخطى القارئ 不، التي عليها 二، فيقرأ 亦た説ばしから، وعند 一 يعود صعودًا إلى 不: 亦た説ばしからずや. وفي 為政 تؤدي العلامة المركّبة 一レ على 為 العملين معًا: يُقرأ 可二以テ為一レ師ト矣 هكذا: 以て، ثم 師と (レ)، ثم 為る، ثم العودة إلى 可 (من 一 إلى 二)، فتصير 以て師と為るべし. وتُقرأ نقاط الترميز الخاصة بالكانبون ㆑ ㆒ ㆓ على أنها الحروف التي تنوب عنها، فيحتفظ النص المنسوخ من مصدر آخر بعلاماته.

#3 · دع الـokurigana الطويلة تدفع الحرف التالي

الـokurigana الأطول من حرفها لا تجري بجانب الحرف التالي: في 花落ツルコト知ル多少 تأخذ حروف الكانا الأربعة ツルコト طول حرفين، فينزل 知 ليبدأ بعدها، كما يضعها JIS X 4051 (§5.6.4). يطول السطر، وينتقل موضع انكساره التالي معه.

#4 · أعطِ كل مقطع تسمية بارتفاع سطرين

script.js · الأسطر 56–71في الكود الكامل
const at = (down, across) => ({ anchor: { to: 'container', edge: 'top-left' },
  offset: { x: pt(down), y: pt(across) } });
const TITLE = 26; // pt
// gotcha: headings-drop-h1-break
const part = { level: 1, breakBefore: { enabled: true, parity: 'any' },
  advancedDesign: { enabled: true, minHeight: pt(5 * LEAD), slot: { elements: [
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: MINCHO, fontWeight: 700,
      fontSize: pt(TITLE), lineHeight: 1, letterSpacing: pt(TITLE / 2), color: col('ink'),
      placement: at(2 * BODY, 2 * LEAD - TITLE / 2) },
    { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: GOTHIC, fontSize: pt(8.5),
      letterSpacing: pt(2), color: col('vermilion'), placement: at(2 * BODY + 3 * TITLE + 18,
        2 * LEAD - 8.5 / 2) },
  ] } } };
// 2行取り, the passage's book and number in gothic, two characters down.
const passage = { level: 2, fontFamily: GOTHIC, fontWeight: 700, fontSize: pt(9.5),
  color: col('muted'), lineSpan: 2, indent: em(2) };

يفتتح كل باب من أبواب كتاب القراءة صفحةً بعنوانه، 論語 أو 唐詩، والأول مع ملاحظة صغيرة بلون الزنجفر عمّا يحويه؛ ويأخذ كل مقطع تسمية بخط غير مذيّل (gothic) بعرض سطرين من أسطر المتن (2行取り)، 学而第一 أو 春暁 孟浩然، فتبقى المقاطع التي تحتها على الشبكة. والقراءة سياج :::paragraphs{style="kudashi"} على بعد حرفين من الرأس، بخط المتن وحجمه، مع فوريغانا 熟語 (jukugo): يعطي {君子|くん|し} كل حرف قراءته الخاصة، فيمكن أن ينكسر السطر بينهما.

#5 · حمّل كل صوت بحروفه

script.js · الأسطر 175–188في الكود الكامل
const FENCE = /:::paragraphs\{style="([\w-]+)"\}\n([\s\S]*?)\n:::/g;
const fence = (style) => [...markdown.matchAll(FENCE)].filter((m) => m[1] === style)
  .map((m) => m[2]).join('');
const kanbun = `${fence('kanbun')}${fence('shi')}`; // the text and its marks' kana
const reading = `${fence('kudashi')}${fence('kudashi-shi')}`;
const heads = markdown.match(/^#+ [^{\n]*/gm).join('');
const colophon = markdown.match(/colophon="([^"]*)"/)[1];
await loadFonts(FONTS, markdown); // the Latin files: the colophon
await loadCjkFonts({ [MINCHO]: ['400'] }, kanbun, { vertical: true });
await loadCjkFonts({ [MINCHO]: ['700'] }, heads);
await loadCjkFonts({ [KYOKASHO]: ['400'] }, reading, { vertical: true });
await loadCjkFonts({ [GOTHIC]: ['400', '700'] },
  `${heads}${colophon}${markdown.match(/kicker="([^"]*)"/)[1]}漢文訓読一二三四五六七八九十`,
  { vertical: true });

يُنضَّد النص الكلاسيكي وقراءته بخطين مختلفين، فيحمّل كل منهما ملفات حروفه: Zen Old Mincho للمقاطع وعلاماتها، وحروف الكانا فيها داخل سمات :kunten، وKlee One للقراءات وفوريغاناها، وNoto Sans JP للتسميات. وتُنضَّد الرباعية بيتين في السطر بينهما مسافة إيديوغرافية، وهي طريقة شائعة لطباعتها، فتتسع القصيدة وقراءتها في الصفحة تحت العنوان.

الوصفة كاملة

Sandbox
// ═══ Postext Cookbook · Nº 122 · Kanbun with its reading marks, and the Japanese reading ═══
// https://postext.dev/en/cookbook/kanbun-kundoku
// Code: MIT · Text: 論語, 孟浩然「春暁」 (public domain); kunten and readings CC BY 4.0
// Fonts: Zen Old Mincho, Klee One, Noto Sans JP (SIL OFL 1.1) · Needs postext ≥ 1.16.1
import {
  buildDocument, renderPageToCanvas, clearMeasurementCache, loadVerticalAlternates,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';

const LANG = 'en'; // @lang: the language of the colophon; the page is Japanese in both
const RECIPE = 'kanbun-kundoku';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: ink, the vermilion of hand-written kunten, a school-book paper
const palette = {
  ink: '#1e1c1a', // the text
  vermilion: '#c0392b', // 朱: the reading marks, as a reader adds them by hand
  muted: '#6a635c', // the headings' labels, the folios
  paper: '#fcfbf7',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  { id: 'main-color', name: 'defaults', value: { hex: palette.vermilion, model: 'hex' } },
];
// #endregion
const [MINCHO, KYOKASHO, GOTHIC] = ['Zen Old Mincho', 'Klee One', 'Noto Sans JP'];
const [BODY, LEAD, CHARS, LINES] = [10.5, 21, 36, 16]; // pt, pt: 36字 × 16行, a 2 em pitch
const KANBUN = 15; // pt: the classical text, set larger on two lines of the grid

// #region answer: kanbun with 返り点 and 送り仮名, its yomikudashi beside it in furigana
// :kunten[習]{kaeri="レ" okuri="フ"} sets the return mark small at the character's lower
// left and the okurigana at its lower right, down the line; the text keeps its Chinese
// order, which the reader follows by the marks. cjk.kunten sizes and colours them: half
// the character, in vermilion. The Japanese reading under each passage is plain kana and
// kanji with furigana: {君子|くん|し} gives each character its reading (jukugo ruby).
const cjk = {
  grid: { enabled: true, charsPerLine: CHARS, linesPerPage: LINES },
  kunten: { fontSize: em(0.5), color: col('vermilion'), placement: 'inline' },
};
const paragraphStyles = [
  // The text: 15 pt on a pitch of two grid lines, room for the okurigana beside it. A
  // quatrain takes two lines, a couplet to the line with a space between its verses.
  { id: 'kanbun', fontFamily: MINCHO, fontSize: pt(KANBUN), lineHeight: pt(2 * LEAD),
    firstLineIndent: em(0), textAlign: 'justify' },
  { id: 'shi', fontFamily: MINCHO, fontSize: pt(KANBUN), lineHeight: pt(2 * LEAD),
    indent: em(2), firstLineIndent: em(0), marginTop: pt(0), marginBottom: pt(0) },
  // The reading (書き下し文), two characters down in a school-book hand.
  { id: 'kudashi', indent: em(2), firstLineIndent: em(0), marginTop: pt(LEAD) },
  { id: 'kudashi-shi', indent: em(4), firstLineIndent: em(0), marginTop: pt(0),
    marginBottom: pt(0) },
];
// #endregion

// #region headings: each part opens a page; each passage under a two-line label
const at = (down, across) => ({ anchor: { to: 'container', edge: 'top-left' },
  offset: { x: pt(down), y: pt(across) } });
const TITLE = 26; // pt
// gotcha: headings-drop-h1-break
const part = { level: 1, breakBefore: { enabled: true, parity: 'any' },
  advancedDesign: { enabled: true, minHeight: pt(5 * LEAD), slot: { elements: [
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: MINCHO, fontWeight: 700,
      fontSize: pt(TITLE), lineHeight: 1, letterSpacing: pt(TITLE / 2), color: col('ink'),
      placement: at(2 * BODY, 2 * LEAD - TITLE / 2) },
    { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: GOTHIC, fontSize: pt(8.5),
      letterSpacing: pt(2), color: col('vermilion'), placement: at(2 * BODY + 3 * TITLE + 18,
        2 * LEAD - 8.5 / 2) },
  ] } } };
// 2行取り, the passage's book and number in gothic, two characters down.
const passage = { level: 2, fontFamily: GOTHIC, fontWeight: 700, fontSize: pt(9.5),
  color: col('muted'), lineSpan: 2, indent: em(2) };
// #endregion

const foreEdge = (id, content, edge, y, pages) => ({ kind: 'text', id, content, pages,
  writingMode: 'vertical-rl', fontFamily: GOTHIC, fontSize: pt(7.5), letterSpacing: pt(1),
  color: col('muted'), placement: { anchor: { to: 'outer', edge }, offset: { y: em(y) } } });
const header = { elements: [
  foreEdge('hashira', '{title} {chapterTitle}', 'top', 3, 'body'),
  foreEdge('folio', '{pageNumber}', 'bottom', -3, 'all'),
] };
// The first part's opening page carries the colophon across the foot.
const first = { id: 'first', footer: { elements: [{ kind: 'text', id: 'colophon',
  content: '{attr.colophon}', pages: 'opener', fontFamily: GOTHIC, fontSize: pt(5.5),
  lineHeight: 1.45, color: col('muted'), overflow: 'wrap', align: 'center',
  placement: { anchor: { to: 'page', edge: 'bottom' }, offset: { y: mm(-7) },
    size: { width: mm(118) } } }] } };

const config = () => ({ // a factory: the engine caches resolved configs per object
  locale: 'ja', // written out, never LANG (gotcha: ja-locale-tag)
  colorPalette,
  page: {
    sizePreset: 'custom', width: mm(148), height: mm(210), dpi: 150, // A5
    backgroundColor: col('paper'),
    margins: { top: mm(40), bottom: mm(32), left: mm(13), right: mm(16), mirror: true },
    pageNumbering: { format: 'japanese-informal' },
  },
  layout: { layoutType: 'single', writingMode: 'vertical-rl' },
  cjk,
  bodyText: {
    fontFamily: KYOKASHO, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    textAlign: 'justify', firstLineIndent: em(1), indentAfterHeading: true,
  },
  headings: { fontFamily: MINCHO, color: col('ink'), levels: [part, passage],
    balancing: { enabled: false } }, // gotcha: cjk-grid-balancing
  headingStyles: [first],
  paragraphStyles,
  header,
  footer: { elements: [] },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
نموذج Markdown · أسطر: 50 · content.en.mdtitle: "漢文訓読" --- # 論語 {style="first" kicker="学而・為政より" colophon="Confucius, 論語 (the Analects), three sayings from the books 學而 and 爲政; Meng Haoran, 春曉 (Spring Dawn). Kunten and reading after the usual school reading, set for this recipe. Set in Zen Old Mincho, Klee One and Noto Sans JP (SIL OFL)."} ## 学而第一 :::paragraphs{style="kanbun"} 子:kunten[曰]{okuri="ハク"}、:kunten[学]{okuri="ビテ"}而:kunten[時]{okuri="ニ"}:kunten[習]{kaeri="レ" okuri="フ"}:kunten[之]{okuri="ヲ"}、:kunten[不]{kaeri="二"}:kunten[亦]{okuri="タ"}:kunten[説]{kaeri="一" okuri="バシカラ"}乎。:kunten[有]{kaeri="レ" okuri="リ"}朋:kunten[自]{kaeri="二" okuri="リ"}遠:kunten[方]{kaeri="一"}:kunten[来]{okuri="タル"}、:kunten[不]{kaeri="二"}:kunten[亦]{okuri="タ"}:kunten[楽]{kaeri="一" okuri="シカラ"}乎。人:kunten[不]{kaeri="レ" okuri="シテ"}:kunten[知]{okuri="ラ"}而:kunten[不]{kaeri="レ"}:kunten[慍]{okuri="ミ"}、:kunten[不]{kaeri="二"}:kunten[亦]{okuri="タ"}君:kunten[子]{kaeri="一" okuri="ナラ"}乎。 ::: :::paragraphs{style="kudashi"} 子{曰|い}はく、{学|まな}びて時に{之|これ}を習ふ、{亦|ま}た{説|よろこ}ばしからずや。{朋|とも}有り{遠方|えん|ぽう}より来たる、{亦|ま}た楽しからずや。人知らずして{慍|うら}みず、{亦|ま}た{君子|くん|し}ならずや。 ::: ## 学而第三 :::paragraphs{style="kanbun"} 子:kunten[曰]{okuri="ハク"}、巧言令色、:kunten[鮮]{okuri="ナシ"}矣仁。 ::: :::paragraphs{style="kudashi"} 子{曰|い}はく、{巧言令色|こう|げん|れい|しょく}、{鮮|すく}なし{仁|じん}。 ::: ## 為政第十一 :::paragraphs{style="kanbun"} 子:kunten[曰]{okuri="ハク"}、:kunten[温]{kaeri="レ" okuri="メテ"}:kunten[故]{okuri="キヲ"}而:kunten[知]{kaeri="レ" okuri="レバ"}:kunten[新]{okuri="シキヲ"}、:kunten[可]{kaeri="二" okuri="シ"}:kunten[以]{okuri="テ"}:kunten[為]{kaeri="一レ" okuri="ル"}:kunten[師]{okuri="ト"}矣。 ::: :::paragraphs{style="kudashi"} 子{曰|い}はく、{故|ふる}きを{温|あたた}めて新しきを知れば、{以|もっ}て師と{為|な}るべし。 ::: # 唐詩 ## 春暁 孟浩然 :::paragraphs{style="shi"} 春眠:kunten[不]{kaeri="レ"}:kunten[覚]{kaeri="レ" okuri="エ"}:kunten[暁]{okuri="ヲ"} 処処:kunten[聞]{kaeri="二" okuri="ク"}啼:kunten[鳥]{kaeri="一" okuri="ヲ"} 夜来風:kunten[雨]{okuri="ノ"}声 花:kunten[落]{okuri="ツルコト"}:kunten[知]{okuri="ル"}多少 ::: :::paragraphs{style="kudashi-shi"} {春眠|しゅん|みん}{暁|あかつき}を{覚|おぼ}えず {処処|しょ|しょ}{啼鳥|てい|ちょう}を聞く {夜来|や|らい}{風雨|ふう|う}の声 花落つること知る{多少|た|しょう} :::
`; // content.<lang>.md: the same Japanese text in both // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { 'Zen Old Mincho': ['400', '700'], // the classical text and its marks; the part titles 'Klee One': ['400'], // the reading, in a school-book hand 'Noto Sans JP': ['400', '700'], // labels, the hashira and folios, the colophon }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── // #region voices: each face loads the files that hold the characters it sets const FENCE = /:::paragraphs\{style="([\w-]+)"\}\n([\s\S]*?)\n:::/g; const fence = (style) => [...markdown.matchAll(FENCE)].filter((m) => m[1] === style) .map((m) => m[2]).join(''); const kanbun = `${fence('kanbun')}${fence('shi')}`; // the text and its marks' kana const reading = `${fence('kudashi')}${fence('kudashi-shi')}`; const heads = markdown.match(/^#+ [^{\n]*/gm).join(''); const colophon = markdown.match(/colophon="([^"]*)"/)[1]; await loadFonts(FONTS, markdown); // the Latin files: the colophon await loadCjkFonts({ [MINCHO]: ['400'] }, kanbun, { vertical: true }); await loadCjkFonts({ [MINCHO]: ['700'] }, heads); await loadCjkFonts({ [KYOKASHO]: ['400'] }, reading, { vertical: true }); await loadCjkFonts({ [GOTHIC]: ['400', '700'] }, `${heads}${colophon}${markdown.match(/kicker="([^"]*)"/)[1]}漢文訓読一二三四五六七八九十`, { vertical: true }); // #endregion const doc = await buildWithFonts(() => buildDocument({ markdown }, config()), markdown); showBook(doc, { title: t({ en: 'Kanbun with its reading marks, and the Japanese reading', es: 'Kanbun con sus marcas de lectura y la lectura japonesa' }) }); offerPdf(() => renderToPdf(doc, { fontProvider: cjkPdfProvider }), `${RECIPE}.pdf`);
العُدّة · core, fonts, viewer, pdf, cjk: نفسها في كل وصفة · أسطر: 453// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ───── // ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook ───────── function mm(value) { return { value, unit: 'mm' }; } function pt(value) { return { value, unit: 'pt' }; } function em(value) { return { value, unit: 'em' }; } /** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */ function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; } /** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */ function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; } // ─── Kit · fonts v1 ── the same in every recipe · postext.dev/cookbook ──────── // Postext measures text with the faces the browser has loaded, and caches the // widths, so every face must be ready before the first build. Faces come from // Fontsource: the same static files the PDF embeds, so screen and PDF agree. /** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With * `optional`, a face Fontsource does not ship is skipped instead of failing. * Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', }; const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin']; const jobs = []; let added = 0; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const meta = optional ? await fontsourceMeta(family) : null; for (const spec of new Set(specs)) { const weight = parseInt(spec, 10); const style = spec.endsWith('i') ? 'italic' : 'normal'; if (hasFace(family, weight, style)) continue; if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().then((ready) => { document.fonts.add(ready); added++; }, () => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return added; } /** Runs `build` (a buildDocument or buildBundle call) and checks the faces * the pages use. A regular face missing from FONTS is loaded with a warning; * bold and italic variants are loaded when the family ships them. Then the * measurement caches are cleared and the build runs again. */ async function buildWithFonts(build, text = '') { const tried = new Set(); for (let round = 0; round < 3; round++) { kitStatus('Laying out…'); await new Promise(requestAnimationFrame); // let the status paint first const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; }); const wanted = { base: {}, variants: {} }; for (const { font, base } of [result].flat().flatMap(fontStringsOf)) { const { family, weight, style } = parseFont(font); const key = `${family}|${weight}|${style}`; if (tried.has(key) || hasFace(family, weight, style)) continue; tried.add(key); (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`); } if (Object.keys(wanted.base).length) { console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`); } const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true }); if (added === 0) return result; clearMeasurementCache(); } throw new Error('The fonts did not settle after three builds.'); } /** Every font string of the layout. `base` marks a block's own face; its * bold, italic and bold-italic variants are listed whether or not used. */ function fontStringsOf(doc) { const found = new Map(); const walk = (node) => { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) { node.forEach(walk); return; } for (const [key, value] of Object.entries(node)) { if (typeof value === 'string' && /fontString$/i.test(key)) { found.set(value, found.get(value) || key === 'fontString'); } else if (value && typeof value === 'object') walk(value); } }; walk(doc.pages); walk(doc.blocks); return [...found].map(([font, base]) => ({ font, base })); } /** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }. * A string with no weight ('95.8px Young Serif', from a design text) is 400. */ function parseFont(font) { const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim()); if (!m) throw new Error(`Unexpected font string: ${font}`); const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]); return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' }; } /** True when a loaded FontFace covers exactly this family, weight and style * (document.fonts.check() is also true for families nobody declared). */ function hasFace(family, weight, style) { for (const face of document.fonts) { if (face.status !== 'loaded' || face.style !== style) continue; if (face.family.replace(/^["']|["']$/g, '') !== family) continue; const [low, high = low] = face.weight.split(' ').map(Number); if (weight >= low && weight <= high) return true; } return false; } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), or null. */ function fontsourceMeta(family) { fontsourceMeta.cache ??= new Map(); const id = fontsourceId(family); if (!fontsourceMeta.cache.has(id)) { fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`) .then((res) => (res.ok ? res.json() : null), () => null)); } return fontsourceMeta.cache.get(id); } // ─── Kit · viewer v1 ── the same in every recipe · postext.dev/cookbook ─────── /** Shows the pages as facing spreads on a dark desk: the first page is a * recto on its own, then verso | recto pairs, as in a bound book. Pages * are painted when they scroll near the screen. */ function showPages(docs, { title, width = 460 } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── Kit · pdf v1 ── the same in every recipe that exports a PDF ────────────── /** postext-pdf embeds TrueType bytes. Fetch the Fontsource file the screen * used, snapping to a weight the family ships and falling back to upright * when it has no italic: the PDF asks for every face a block could use. */ async function fontsourceProvider(family, weight, style) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); const weights = meta?.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style; const res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${w}-${s}.woff2`); if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} (${res.status})`); return decompressWoff2(new Uint8Array(await res.arrayBuffer())); } /** A "Build the PDF" button in the bar. Once built: "Open the PDF" (a new * tab, since CodePen's preview frame cannot show PDFs) and a download link. */ function offerPdf(makePdf, filename) { viewer(); const button = Object.assign(document.createElement('button'), { type: 'button', textContent: 'Build the PDF' }); button.dataset.postextPdf = filename; button.addEventListener('click', async () => { button.disabled = true; button.textContent = 'Building the PDF…'; try { const bytes = await makePdf(); const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' })); const size = `${Math.max(1, Math.round(bytes.length / 1024))} KB`; button.replaceWith( Object.assign(document.createElement('a'), { href: url, target: '_blank', rel: 'noopener', textContent: 'Open the PDF ↗' }), Object.assign(document.createElement('a'), { href: url, download: filename, textContent: `Download ${filename} · ${size}` })); } catch (error) { button.disabled = false; button.textContent = 'Build the PDF'; kitFail(error); } }); document.getElementById('pt-actions').append(button); } // ─── Kit · cjk v1 ── Chinese, Japanese and Korean books · postext.dev/cookbook ─ // Fontsource ships a CJK family as about a hundred files per weight, each // declared in its stylesheet with the unicode-range it covers. The screen // loads the files the sample touches; the PDF gets the same files for the // characters its pages set in each face, and embeds each as a subset. // A book bound on the right (vertical text) is shown with its spreads // mirrored: page 1 alone on the left of the spine, then [3 | 2]. /** The files of a Fontsource face, read from its stylesheet: { url, range, * ranges }, the last declared first (the order the browser tries them in). */ function cjkSlices(family, weight, style) { cjkSlices.cache ??= new Map(); const id = fontsourceId(family); const css = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/${weight}${style === 'italic' ? '-italic' : ''}.css`; if (!cjkSlices.cache.has(css)) { cjkSlices.cache.set(css, fetch(css) .then((res) => { if (!res.ok) throw new Error(`Fontsource has no ${family} ${weight} ${style} (${res.status})`); return res.text(); }) .then((text) => [...text.matchAll(/@font-face\s*{([^}]*)}/g)].map(([, rule]) => { const range = /unicode-range:\s*([^;]+);/.exec(rule)?.[1].trim() ?? 'U+0-10FFFF'; const ranges = range.split(',').map((part) => { const [lo, hi = lo] = part.trim().slice(2).split('-'); return [parseInt(lo, 16), parseInt(hi, 16)]; }); return { url: new URL(/url\(([^)]+?\.woff2)\)/.exec(rule)[1], css).href, range, ranges }; }).reverse())); } return cjkSlices.cache.get(css); } /** The file of `slices` that holds code point `cp`, if any. */ function cjkSliceFor(slices, cp) { return slices.find((slice) => slice.ranges.some(([lo, hi]) => cp >= lo && cp <= hi)); } /** Whether Fontsource serves `family` as a Chinese, Japanese or Korean * family (its subsets name the script). Fails when the API does not * answer: a CJK face taken for a Latin one would paint in a system face. */ async function isCjkFamily(family) { const meta = await fontsourceMeta(family); if (!meta) throw new Error(`api.fontsource.org did not describe ${family}: reload to try again`); return !!meta.subsets?.some((subset) => /^(chinese|japanese|korean)/.test(subset)); } /** faces = { 'Noto Serif TC': ['400', '700'] }, as for loadFonts: the * whole FONTS object may be passed, its other families are left to * loadFonts. Adds one FontFace per file of each CJK face with its * unicodeRange, then loads the files `text` touches. `text` is what the * faces set: the sample for the text face; a book in several voices calls * it once per voice (loadCjkFonts({ 'LXGW WenKai TC': ['400'] }, quotes)), * so the heading and quotation faces fetch and check only their own * characters. Fails when a character of `text` is in no file of a face. * List every weight the pages use: a weight left to buildWithFonts gets * the latin file only. With { vertical: true } it also loads each * family's vertical forms (brackets, quotes, pause marks) for the canvas, * which needs loadVerticalAlternates imported from postext. Resolves to * the number of files loaded. */ async function loadCjkFonts(faces, text, { vertical = false } = {}) { kitStatus('Loading fonts…'); let loaded = 0; try { if (vertical && typeof loadVerticalAlternates !== 'function') { throw new Error('loadCjkFonts(…, { vertical: true }) needs loadVerticalAlternates imported from postext'); } for (const [family, specs] of Object.entries(faces)) { if (!(await isCjkFamily(family))) continue; const twin = []; for (const spec of new Set(specs)) { const weight = parseInt(spec, 10); const style = spec.endsWith('i') ? 'italic' : 'normal'; const slices = await cjkSlices(family, weight, style); const missing = [...new Set(text)].filter((ch) => /\S/.test(ch) && !cjkSliceFor(slices, ch.codePointAt(0))); if (missing.length) { throw new Error(`${family} ${spec} has no file for ${missing.slice(0, 12).join(' ')}: ` + `give each face the text it sets (loadCjkFonts({ '${family}': ['${spec}'] }, text))`); } for (const slice of slices) { document.fonts.add(new FontFace(family, `url(${slice.url}) format('woff2')`, { weight: String(weight), style, unicodeRange: slice.range })); twin.push({ source: slice.url, weight: String(weight), style, unicodeRange: slice.range }); } const font = `${style === 'italic' ? 'italic ' : ''}${weight} 16px "${family}"`; loaded += (await document.fonts.load(font, text)).length; if (!document.fonts.check(font, text)) throw new Error(`${family} ${spec} did not load for the sample`); } // The same files under a twin name with the `vert` feature on: the // canvas paints the punctuation of vertical lines with it. if (vertical && twin.length) await loadVerticalAlternates(family, twin); } } catch (error) { kitFail(error); throw error; } return loaded; } /** The PDF font provider for recipes with CJK faces: a family whose * Fontsource subsets are Chinese, Japanese or Korean gets the files that * hold the characters its pages set (`request.codePoints`); any other * family gets the latin file fontsourceProvider fetches (the "pdf" block) * and, when the face sets letters only latin-ext has, that file too. */ async function cjkPdfProvider(family, weight, style, request) { if (!(await isCjkFamily(family))) return cjkLatinPdfFiles(family, weight, style, request); const meta = await fontsourceMeta(family); const weights = meta.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && !meta.styles.includes('italic') ? 'normal' : style; const slices = await cjkSlices(family, w, s); const picked = new Set(); for (const cp of request?.codePoints ?? []) { const slice = cjkSliceFor(slices, cp); if (slice) picked.add(slice); } if (!picked.size) picked.add(slices[0]); return Promise.all(slices.filter((slice) => picked.has(slice)).map(async (slice) => { const res = await fetch(slice.url); if (!res.ok) throw new Error(`Fontsource file ${slice.url} (${res.status})`); return decompressWoff2(new Uint8Array(await res.arrayBuffer())); })); } /** A Latin family set next to the CJK faces: its latin file, then its * latin-ext file when the face sets letters only latin-ext has (ō ū in * Hepburn rōmaji, ǎ in pinyin), the file loadFonts adds on screen for * them. Latin comes first: postext-pdf draws a character from the first * file that has it, as the browser takes a character both files hold from * latin. A face Fontsource ships without latin-ext, or whose file does * not come, gets latin alone, and the PDF names the letters it lacks. */ async function cjkLatinPdfFiles(family, weight, style, request) { const meta = await fontsourceMeta(family); const beyond = [...(request?.codePoints ?? [])].some(cjkLatinExtOnly); if (!beyond || !meta?.subsets?.includes('latin-ext')) return fontsourceProvider(family, weight, style); const weights = meta.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && !meta.styles.includes('italic') ? 'normal' : style; const id = fontsourceId(family); const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-ext-${w}-${s}.woff2`; const [latin, ext] = await Promise.all([fontsourceProvider(family, weight, style), fetch(url) .then(async (res) => (res.ok ? decompressWoff2(new Uint8Array(await res.arrayBuffer())) : null), () => null)]); return ext ? [latin, ext] : latin; } /** Whether code point `cp` is in Fontsource's latin-ext file and not in * its latin file: Latin Extended-A and -B, IPA, the spacing modifiers and * Latin Extended Additional (loadFonts's test for latin-ext), less the * few latin holds too (ı Œ œ ʻ ʼ ˆ ˚ ˜). */ function cjkLatinExtOnly(cp) { if (!((cp >= 0x100 && cp <= 0x2ff) || (cp >= 0x1e00 && cp <= 0x1eff))) return false; return ![0x131, 0x152, 0x153, 0x2bb, 0x2bc, 0x2c6, 0x2da, 0x2dc].includes(cp); } /** showPages for a book bound on either edge. A right-bound book (the * document says so: doc.binding is 'right' for page.binding 'right' and * for vertical text) lies on the desk as it opens: page 1 alone on the * left of the spine, then [3 | 2], the spine shade on each page's inner * edge. `binding` ('left' | 'right') overrides the document's. */ function showBook(docs, { binding, ...options } = {}) { const count = showPages(docs, options); const right = (binding ?? [docs].flat()[0]?.binding) === 'right'; if (!document.getElementById('pt-kit-cjk')) { // The pages keep direction ltr: a canvas draws text in the direction its // element inherits, and under rtl each run would end where the engine // starts it, its brackets mirrored. document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit-cjk"> .pt-spread[dir="rtl"] canvas { direction: ltr; } .pt-spread[dir="rtl"] figure:first-child canvas { box-shadow: inset 14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } </style>`); } // Each pair stays [verso, recto] in the page; right to left, the verso // sits on the right. Phones stack the pages in reading order either way. for (const spread of document.querySelectorAll('#pages > .pt-spread')) spread.dir = right ? 'rtl' : 'ltr'; document.getElementById('pages').dataset.binding = right ? 'right' : 'left'; return count; } // ─── /Kit ───────────────────────────────────────────────────────────────────────

يعمل ملف script.js المجمّع كما هو: الصقه في سكربت الوحدة (module) لأي صفحة، أو افتح الوصفة على CodePen. مجلد الوصفة على GitHub ↗ (يفتح في تبويب جديد)

تنويعات

#اطبع العلامات بالأسود

يضع كتاب القراءة المطبوع علامات 訓点 بحبر النص؛ احذف اللون.

-  kunten: { fontSize: em(0.5), color: col('vermilion'), placement: 'inline' },
+  kunten: { fontSize: em(0.5), placement: 'inline' },

#ضع العلامات بين الأسطر

يُخرج placement: 'interlinear' علامات العودة من السطر إلى الفراغ الذي بجانب النصف الأسفل من الحرف، فيحتفظ المقطع بتباعد النص المجرد (白文). ويضعها JIS داخل السطر، وهي القيمة الافتراضية.

-  kunten: { fontSize: em(0.5), color: col('vermilion'), placement: 'inline' },
+  kunten: { fontSize: em(0.5), color: col('vermilion'), placement: 'interlinear' },

أخطاء شائعة

خطأ شائع

اوسم النص الياباني بـ 'ja'، لا بـ zh-Hans ولا بـ LANG

نسختا الوصفة هما en وes، لكن المثال الياباني ياباني في كلتيهما: `locale: LANG` سيسمه بالإنجليزية أو الإسبانية، والوسم الصيني سينضّده بالقواعد الصينية (ترقيم Kaiming، وكانا صغيرة يجوز أن تبدأ بها الأسطر، و图 بدل 図، وأشكال صينية للحروف في PDF). اكتب 'ja': يختار منطقة اليابان (كسر الأسطر والترقيم وفق JLReq، ونقاط السمسم، وتوزيع الفوريغانا، وتسميتي 図 و表) ويوقف تقسيم الكلمات. ويُفشل الفحصُ نصًّا فيه كانا موسومًا بـ zh أو ko. كسر الأسطر الياباني (kinsoku) →

خطأ شائع

أي كائن headings يُلغي فاصل الصفحة قبل H1

ينتقل H1 افتراضيًا إلى صفحة فردية (always-odd)، لكن تمرير أي كائن headings يعيد ضبط هذا الافتراض، فتتوالى الفصول دون فاصل ولا يفعل span: 'page' شيئًا. أعد كتابة headings.levels[0].breakBefore: { enabled: true, parity } في كل إعداد. فصول تبدأ في صفحة فردية →

خطأ شائع

الخطوط الصينية تُحمَّل شرائح، عبر الكتلة cjk

يقدّم Fontsource العائلة الصينية أو اليابانية أو الكورية في نحو مئة ملف لكل وزن، يغطي كل منها نطاقًا من الحروف. لا يجلب loadFonts إلا ملف latin، فتأتي حروف الهان على الشاشة من خط النظام وتُقاس خطأً، ويسلّم fontsourceProvider ملف latin ذاك إلى PDF، فيطبعها مربعات فارغة. اذكر كتلة الأدوات cjk، واستدعِ loadCjkFonts(FONTS, markdown) بعد loadFonts (مرة لكل صوت، مع النص الذي يضعه، حين يستخدم الكتاب عدة خطوط CJK)، وأعطِ renderToPdf القيمة fontProvider: cjkPdfProvider: كلاهما يأخذ الملفات التي تحتوي حروف النص. الخطوط الصينية واليابانية والكورية →

خطأ شائع

الكتاب المجلَّد على اليمين يعرض صفحاته المتقابلة بـ showBook

في الكتاب المجلَّد على اليمين (نص عربي أو عبري أو فارسي، أو صيني عمودي، أو page.binding 'right') تبقى الصفحة 1 هي الصفحة الفردية، لكنها تقع على يسار الكعب، وتُقرأ الأزواج [3 | 2]. يعرض showPages كل كتاب مجلَّدًا على اليسار؛ أما showBook من الكتلة book (وتحمل الكتلة cjk الدالة نفسها) فيقرأ doc.binding ويعكس الأزواج. ولا يزال capture.hero يسمّي الصفحتين المتقابلتين بترتيب القراءة، [verso, recto]: [2, 3]. كتب مجلّدة من اليمين →

خطأ شائع

على شبكة الحروف، أوقف موازنة الأعمدة

تملأ موازنة الأعمدة الصفحة التي تنتهي قصيرة، كما حين يترك عنوانٌ مُبقًى مع نصه أسطرًا فارغة في الأسفل، بإضافة أسطر شبكة فوق العناوين وبتوسيع فقرة بمقدار سطر. والسطر الصيني الموسَّع يباعد بين حروفه (0.13 em في صفحة GB/T 9704، وهو أبعد كثيرًا من balancing.maxTracking)، فتخرج الحروف من أعمدة الشبكة وتخرج العناوين من أسطرها. الصفحة المحسوبة بالخلايا تنتهي قصيرة بدلًا من ذلك: اضبط headings.balancing: { enabled: false }. شبكة المحارف →

الحقوق

الوصفة
Ignacio Ferro
النص
  • 論語 (the Analects of Confucius), 學而 1 and 3, 爲政 11, and 孟浩然「春曉」 (Spring Dawn), in shinjitai; the kunten (返り点, 送り仮名) and the yomikudashi follow the usual Japanese school reading and were set for this recipe (CC BY 4.0) · Confucius and his disciples; Meng Haoran; Ignacio Ferro (kunten and reading) · ملك عام
الخطوط
Zen Old Mincho (SIL OFL 1.1) · Klee One (SIL OFL 1.1) · Noto Sans JP (SIL OFL 1.1)
SandboxPDF