باختصار
فصل قصير يشرح الكلمات التي يستعملها المنضّدون اليابانيون، وفي آخره فهرس. تحمل كل كلمة مفهرسة نطقها، ويرتّب به Postext الفهرس بترتيب الكانا الياباني، تحت رؤوس الصفوف あ行 وか行 وما يليها.
ما الذي ستنضده
فصل من دليل متخيَّل للتنضيد الياباني، 組版の言葉 («كلمات التنضيد»)، وفهرسه. يشرح الفصل أسماء أجزاء الصفحة وأحجام الحروف وقواعد علامات الترقيم، على صفحة A5 من 36 حرفًا في 28 سطرًا، وكل مصطلح معلَّم للفهرس في الموضع الذي يُشرح فيه. والفهرس هو موضوع الوصفة. لا ترتيب أبجديًا للكانجي في اليابانية، فيصنّف الفهرس كل مدخل بقراءته بالكانا، بترتيب جدول gojūon (五十音)، تحت رؤوس الصفوف あ行 وか行 وさ行. المصطلح الذي يحمل فوريغانا يعطي قراءته بنفسه، وأي مصطلح آخر فيه كانجي يذكرها في السمة yomi. أما وصفة فهرس الأسماء الصينية فترتّب الأسماء الصينية بالبينيين وبعدد الضربات.
تجيب هذه الوصفة عن
- كيف أرتّب فهرسًا يابانيًّا بترتيب gojūon بحسب قراءاته؟
الجواب المختصر
// Each mark gives the reading the entry sorts by. A term written with furigana gives it
// itself: :index[{版面|はん|めん}] files 版面 as はんめん. Any other term with a kanji says it
// with yomi: :index[明朝体]{yomi="みんちょうたい"}. Kana and Latin terms need nothing. The
// reading orders the entries in JIS X 4061 order (katakana as hiragana, small kana as large,
// voiced after plain, ー as the vowel before it); Latin terms come first, under A to Z.
const index = {
groupBy: 'gojuon', // what 'auto' picks in a 'ja' document: heads あ行 か行 さ行 …
fontFamily: MINCHO, fontSize: pt(8.5), lineHeight: pt(14),
separator: ' ', locatorSeparator: '、', // 版面 3、5: an ideographic space, then 、
see: { italic: false }, // → ルビ, → 柱も見よ: upright, no italic in Japanese
groups: { fontFamily: GOTHIC, fontSize: pt(9), fontWeight: 700, color: col('vermilion'),
marginTop: pt(8) },
};
المكونات
- الميزات
- فهرس بترتيب gojūonالفهرس الأبجدي في آخر الكتابفوريغانا: قراءات الكانا فوق الكانجيقراءات ruby: pinyin وzhuyinالخطوط الصينية واليابانية والكوريةكسر الأسطر الياباني (kinsoku)تباعد علامات الترقيم اليابانية (yakumono)أنماط العناوينفصول بلا أرقامالعناوين المرقّمةعناوين تشغل أسطرًا من المتن (gyōdori)عمود واحد أو عمودانشبكة المحارفصفحات افتتاح مصمَّمةسمات العنوانالترويسات وأرقام الصفحاتهوامش متناظرةأنماط الفقراتالتصدير إلى PDF
- تستخدم أيضًا
- تقسيم الأسطر في النص الصينيموازنة الأعمدةالترويسات بحسب دور الصفحةالخطوط المضمَّنة في PDFهندسة القسم
- الخطوط
- Noto Serif JP, Noto Sans JP (SIL OFL 1.1)
- الأصول
- لا شيء: كل صورة مرسومة بالكود
طريقة التحضير
#1 · قراءة لكل مصطلح فيه كانجي
الشيفرة هي الجواب المختصر أعلاه. في النص يطبع :index[{版面|はん|めん}]{main} المصطلح 版面 وفوقه はんめん، ويصنّف المدخل تحت はんめん؛ ويطبع :index[明朝体]{yomi="みんちょうたい" main} المصطلح بلا قراءة ويصنّفه تحت みんちょうたい. ولا تحتاج المصطلحات المكتوبة بالكانا، مثل ノンブル أو のど، ولا المصطلحات اللاتينية، مثل DTP أو JIS X 4051، إلى شيء (علامات الفهرس). والقراءة لا تحدد إلا الترتيب، ويُطبع المدخل كما كُتب. ولا سبيل إلى تخمين قراءات الكانجي، فـ 天 تُقرأ てん هنا وあめ في موضع آخر، لذلك لا يحاول المحرّك ذلك أبدًا.
#2 · ترتيب JIS ورؤوس الصفوف
تختار 'auto' القيمة groupBy: 'gojuon' حين يكون locale: 'ja'. وتُرتَّب المداخل بترتيب JIS X 4061: الكاتاكانا كالهيراغانا، والكانا الصغيرة كالكبيرة، والكانا المجهورة بعد غير المجهورة مباشرة، وー كالصائت الذي قبلها. لذا يأتي 級 (きゅう) قبل 行取り (ぎょうどり)، وゴシック体 (ごしっくたい) بعد 小口 (こぐち). وتأتي المصطلحات اللاتينية أولًا، تحت حروفها، كما يرتّبها JIS؛ ولو وُجدت رموز وأرقام لسبقتها (الفهرس الأبجدي).

#3 · → وも見よ
إحالة «انظر» علامةٌ فيها see، مثل :index{term="振り仮名" yomi="ふりがな" see="ルビ"}، وتطبع 振り仮名 →ルビ. وإحالة «انظر أيضًا» تستعمل seealso وتطبع الأهداف قبل も見よ، 級 10 →歯も見よ، كما تصوغها الفهارس اليابانية. ويُبقي see: { italic: false } التسميات قائمة، لأن الخط الياباني لا مائل فيه. وتأتي المسافة الإيديوغرافية بعد كل مصطلح والعلامة 、 بين الصفحات من separator وlocatorSeparator.
#4 · الفهرس على عمودين
const band = { kind: 'box', id: 'band', reserve: false, style: { backgroundColor: col('tint') },
placement: { anchor: { to: 'page', edge: 'top-left' },
size: { width: 'fill', height: mm(56) } } };
const opener = (sink, ground = []) => ({ enabled: true, minHeight: pt(PITCH * sink),
slot: { elements: [...ground,
{ kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: GOTHIC, fontSize: pt(9),
fontWeight: 700, letterSpacing: pt(2), color: col('vermilion'), align: 'left',
placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: mm(10) } } },
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: GOTHIC, fontSize: pt(22),
fontWeight: 700, color: col('ink'), align: 'left', overflow: 'wrap',
placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { y: mm(3) } } },
{ kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.5),
color: col('vermilion'), placement: { anchor: { to: '#title', edge: 'below' },
offset: { y: mm(4) }, size: { width: mm(12) } } },
] } });
عنوان الفهرس نمط عنوان فيه span: 'page' وlayout بعمودين: يمتد تصميمه، وهو شريط شاحب عليه العنوان، عبر الصفحة، ويجري :::index في عمودين تحته. وتُنضَّد رؤوس الصفوف بخط Gothic بلون الزنجفر، والمداخل بخط Mincho الذي يُنضَّد به النص، بحجم 8.5 نقطة على سطر من 14 نقطة.
الوصفة كاملة
// ═══ Postext Cookbook · Nº 126 · A Japanese index in gojūon order, read from its readings ═══ // https://postext.dev/en/cookbook/gojuon-index // Code: MIT · Text: original (CC BY 4.0) · Pictures: none // Fonts: Noto Serif JP, Noto Sans JP (SIL OFL 1.1) · Needs postext ≥ 1.16.1 import { buildDocument, renderPageToCanvas, clearMeasurementCache } from 'https://esm.sh/postext'; import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf'; const LANG = 'en'; // @lang: the language of the frame; the chapter is Japanese in both const RECIPE = 'gojuon-index'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: ink, one vermilion for the row heads and the chapter label, pale rules const palette = { ink: '#211e1c', // text: a warm near-black vermilion: '#b33a22', // the one accent: あ行 heads, the kicker, folios tint: '#f5ece6', // the index heading's band rule: '#d3c8c0', // the rule under each title muted: '#6c625b', // running heads, the colophon paper: '#ffffff', }; const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id }); const colorPalette = Object.entries({ ...palette, 'main-color': palette.vermilion }) .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })); // #endregion const [MINCHO, GOTHIC] = ['Noto Serif JP', 'Noto Sans JP']; const [BODY, PITCH] = [9, 16]; // pt: 9 pt text on a 16 pt line const [CHARS, LINES] = [36, 28]; // the type area in characters // #region answer: the index in gojūon order: by reading, under あ行 か行 さ行… // Each mark gives the reading the entry sorts by. A term written with furigana gives it // itself: :index[{版面|はん|めん}] files 版面 as はんめん. Any other term with a kanji says it // with yomi: :index[明朝体]{yomi="みんちょうたい"}. Kana and Latin terms need nothing. The // reading orders the entries in JIS X 4061 order (katakana as hiragana, small kana as large, // voiced after plain, ー as the vowel before it); Latin terms come first, under A to Z. const index = { groupBy: 'gojuon', // what 'auto' picks in a 'ja' document: heads あ行 か行 さ行 … fontFamily: MINCHO, fontSize: pt(8.5), lineHeight: pt(14), separator: ' ', locatorSeparator: '、', // 版面 3、5: an ideographic space, then 、 see: { italic: false }, // → ルビ, → 柱も見よ: upright, no italic in Japanese groups: { fontFamily: GOTHIC, fontSize: pt(9), fontWeight: 700, color: col('vermilion'), marginTop: pt(8) }, }; // #endregion // #region opener: the chapter label, the title and a short rule; the index's on a band const band = { kind: 'box', id: 'band', reserve: false, style: { backgroundColor: col('tint') }, placement: { anchor: { to: 'page', edge: 'top-left' }, size: { width: 'fill', height: mm(56) } } }; const opener = (sink, ground = []) => ({ enabled: true, minHeight: pt(PITCH * sink), slot: { elements: [...ground, { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: GOTHIC, fontSize: pt(9), fontWeight: 700, letterSpacing: pt(2), color: col('vermilion'), align: 'left', placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: mm(10) } } }, { kind: 'text', id: 'title', content: '{titleText}', fontFamily: GOTHIC, fontSize: pt(22), fontWeight: 700, color: col('ink'), align: 'left', overflow: 'wrap', placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { y: mm(3) } } }, { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.5), color: col('vermilion'), placement: { anchor: { to: '#title', edge: 'below' }, offset: { y: mm(4) }, size: { width: mm(12) } } }, ] } }); // #endregion // Running heads: the book on the verso, the chapter on the recto, folios outside. const head = (id, content, parity, edge, x, extra) => ({ kind: 'text', id, content, parity, pages: 'body', fontFamily: GOTHIC, fontSize: pt(7.5), color: col('muted'), align: edge.endsWith('left') ? 'left' : 'right', placement: { anchor: { to: 'container', edge }, offset: { x: mm(x), y: mm(12) } }, ...extra }); const folio = { fontWeight: 700, color: col('vermilion') }; const config = () => ({ // a factory: the engine caches resolved configs per object locale: 'ja', // written out, never LANG (gotcha: ja-locale-tag) colorPalette, index, page: { sizePreset: 'custom', width: mm(148), height: mm(210), dpi: 150, // A5 margins: { top: mm(23), bottom: mm(19), left: mm(18), right: mm(15), mirror: true } }, layout: { layoutType: 'single' }, cjk: { grid: { enabled: true, charsPerLine: CHARS, linesPerPage: LINES }, ruby: { fontSize: em(0.5) } }, // furigana on a term's first mention, in the 7 pt gap bodyText: { fontFamily: MINCHO, fontSize: pt(BODY), lineHeight: pt(PITCH), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'), textAlign: 'justify', firstLineIndent: em(1), indentAfterHeading: true, avoidRunts: true }, headings: { fontFamily: GOTHIC, fontWeight: 700, color: col('ink'), balancing: { enabled: false }, // heads stay on the grid levels: [ // Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break). { level: 1, breakBefore: { enabled: true, parity: 'odd' }, advancedDesign: opener(6) }, { level: 2, numberingTemplate: '{1}.{2}', numberSeparator: ' ', fontSize: pt(10.5), lineSpan: 3 }, // 3行取り ] }, headingStyles: [{ id: 'index', numbered: false, span: 'page', // the title over both columns breakBefore: { enabled: true, parity: 'any' }, advancedDesign: opener(5, [band]), // two columns, two characters apart layout: { layoutType: 'double', gutterWidth: pt(2 * BODY) } }], paragraphStyles: [{ id: 'colophon', fontFamily: GOTHIC, fontSize: pt(6.5), lineHeight: pt(9), color: col('muted'), firstLineIndent: pt(0), textAlign: 'left', marginTop: pt(PITCH) }], header: { elements: [ head('v-folio', '{pageNumber}', 'even', 'top-left', 0, folio), head('v-title', '{title}', 'even', 'top-left', 8), head('r-title', '{chapterTitle}', 'odd', 'top-right', -8), head('r-folio', '{pageNumber}', 'odd', 'top-right', 0, folio), ] }, footer: { elements: [{ kind: 'text', id: 'drop-folio', content: '{pageNumber}', pages: 'opener', fontFamily: GOTHIC, fontSize: pt(7.5), fontWeight: 700, color: col('vermilion'), align: 'center', placement: { anchor: { to: 'container', edge: 'bottom' }, offset: { y: mm(-10) } } }] }, }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---نموذج Markdown · أسطر: 38 · content.en.md
title: "組版の言葉" --- # 紙面を呼ぶ言葉 {kicker="第1章"} 本を作る現場には、紙面のあちこちを呼び分ける言葉がある。編集者と組版者とデザイナーが同じ言葉を使えば、指示は短く、間違いも少なくなる。本章では、日本語の本づくりで使われる基本的な用語を、紙面の名前、文字の大きさと書体、約物と組版の規則の三つに分けて紹介する。 ## 紙面の名前 ページのうち、本文が組まれる範囲を:index[{版面|はん|めん}]{main}という。版面の外側の余白には、それぞれ名前がある。上の余白が:index[{天|てん}]、下の余白が:index[{地|ち}]、綴じる側の余白が:index[のど]、その反対側が:index[{小口|こ|ぐち}]である。横組みの本では、のどは左ページの右側と右ページの左側にくる。 余白に置かれる書名や章の名前を:index[{柱|はしら}]{main}と呼び、ページの番号を:index[ノンブル]{main}と呼ぶ。ノンブルはフランス語のnombreから来た言葉で、印刷所ではいまも普通に使われている。:index{term="ノンブル" seealso="柱"}柱とノンブルは同じ行に並べることが多く、まとめて「柱」と呼ぶ人もいる。 ## 文字の大きさと書体 金属活字の時代、文字の大きさは:index[{号数|ごう|すう}]で呼ばれていた。本文に多く使われた五号は、いまの10.5ポイントに当たる。:index[ポイント]{main}は欧米から入った単位で、DTPでは1ポイントを72分の1インチ、約0.353ミリとする。 写真植字、略して:index[{写植|しゃ|しょく}]{main}の時代には、文字の大きさを:index[{級|きゅう}]、行の送りを:index[{歯|は}]で測った。どちらも0.25ミリを単位とし、13級は3.25ミリの大きさになる。:index{term="級" seealso="歯"}写植の文字盤は、1990年代に:index[DTP]{main}が広まると使われなくなったが、級と歯はいまもデザインの指定に残っている。:index{term="写植" seealso="DTP"} 書体では、縦の線が太く横の線が細い:index[明朝体]{yomi="みんちょうたい" main}が本文に、線の太さがそろった:index[ゴシック体]{yomi="ごしっくたい"}が見出しや注に使われる。明朝体の名は中国の明の時代の木版の文字に、ゴシック体の名は欧文のサンセリフを指した英語の呼び名に由来する。 ## 約物と組版の規則 句読点や括弧のように、文字以外の記号を:index[{約物|やく|もの}]{main}という。:index[{句読点|く|とう|てん}]の「、」と「。」は全角の幅を持ち、括弧と並ぶと間を詰めて組む。行の頭に「。」や「」」が来たり、行の末に「「」が残ったりしないように行の分け方を調整する処理を、:index[禁則処理]{yomi="きんそくしょり" main}という。日本語の組版の規則は、日本工業規格の:index[JIS X 4051]「日本語文書の組版方法」にまとめられ、W3Cの:index[JLReq]「日本語組版処理の要件」がその内容を英語でも紹介している。 漢字の読みを小さな仮名で示すものを:index[ルビ]{main}と呼ぶ。:index{term="振り仮名" yomi="ふりがな" see="ルビ"}ルビの名は、5.5ポイントの活字を英国でrubyと呼んだことに由来する。語を強調するために文字の横に打つ点は:index[{圏点|けん|てん}]{main}といい、:index{term="傍点" yomi="ぼうてん" see="圏点"}縦組みでは文字の右に、横組みでは上に置く。 縦組みの中で二桁の数字などを横に並べて一字分に収めるのが:index[{縦中横|たて|ちゅう|よこ}]、本文の行の中に小さな文字の注を二行に割って入れるのが:index[{割注|わり|ちゅう}]である。見出しを本文の何行分かの幅に収めて組むことを:index[{行取|ぎょう|ど}り]といい、3行取りの見出しなら、前後の本文の行の位置を変えずに済む。 これらの言葉の多くは、活字の時代から写植、DTPへと道具が変わっても使われ続けてきた。道具が変わっても、紙面の上で解くべき問題が同じだからである。 # 索引 {style="index" kicker="五十音順"} :::index :::paragraphs{style="colophon"} A specimen chapter written for the Postext Cookbook; the book is fictitious. The index files each term by its reading, taken from the furigana or from yomi. Set in Noto Serif JP and Noto Sans JP (SIL OFL). Text: CC BY 4.0. :::`; // content.<lang>.md: the same Japanese chapter in both // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { // every face the pages use, loaded before the build (gotcha: fonts-first) 'Noto Serif JP': ['400', '700'], // 明朝: the text and the index, its main pages bold 'Noto Sans JP': ['400', '700'], // ゴシック: titles, heads, row heads, folios, colophon }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── // Each face loads the files of what it sets (gotcha: cjk-fonts-slices): the gothic the heads // and the row heads the index prints, あ行 to わ行 and A to Z. const all = (re) => (markdown.match(re) ?? []).join(''); const ROWS = 'あかさたなはまやらわ行ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; await loadFonts(FONTS, markdown); await loadCjkFonts({ [MINCHO]: FONTS[MINCHO] }, `${markdown}→も見よ、`); await loadCjkFonts({ [GOTHIC]: FONTS[GOTHIC] }, `${all(/^#+ .*$/gm)}${all(/colophon"\}\n[^\n]*/g)}組版の言葉${ROWS}`); // Page 1 is page 9 of the book, a recto; the index follows the chapter. const continuation = { pageIndexOffset: 8, pageNumbering: { startAt: 9 } }; const doc = await buildWithFonts(() => buildDocument({ markdown, continuation }, config()), markdown); showPages(doc, { title: t({ en: 'A Japanese index in gojūon order', es: 'Un índice japonés en orden gojūon' }) }); 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 ↗ (يفتح في تبويب جديد)
تنويعات
#رؤوس بأول حرف كانا
تجعل 'kana' رأس كل مجموعة أول حرف كانا في مداخلها، كما تفعل المعاجم وفهارس الأسماء الطويلة: か، き، く بدل رأس واحد هو か行.
- groupBy: 'gojuon', // what 'auto' picks in a 'ja' document: heads あ行 か行 さ行 …
+ groupBy: 'kana', // heads か き く け こ…#قراءة بلا فوريغانا في النص
لتُبقي أول ذكر للمصطلح بلا قراءة فوقه وتعطيه قراءته مع ذلك، انقل القراءة إلى yomi.
-ページのうち、本文が組まれる範囲を:index[{版面|はん|めん}]{main}という。
+ページのうち、本文が組まれる範囲を:index[版面]{yomi="はんめん" main}という。أخطاء شائعة
خطأ شائع
اوسم النص الياباني بـ 'ja'، لا بـ zh-Hans ولا بـ LANG
نسختا الوصفة هما en وes، لكن المثال الياباني ياباني في كلتيهما: `locale: LANG` سيسمه بالإنجليزية أو الإسبانية، والوسم الصيني سينضّده بالقواعد الصينية (ترقيم Kaiming، وكانا صغيرة يجوز أن تبدأ بها الأسطر، و图 بدل 図، وأشكال صينية للحروف في PDF). اكتب 'ja': يختار منطقة اليابان (كسر الأسطر والترقيم وفق JLReq، ونقاط السمسم، وتوزيع الفوريغانا، وتسميتي 図 و表) ويوقف تقسيم الكلمات. ويُفشل الفحصُ نصًّا فيه كانا موسومًا بـ zh أو ko. كسر الأسطر الياباني (kinsoku) →
خطأ شائع
انضد اليابانية بخط ياباني
في Noto Serif SC وTC كانا، لكنهما ترسمان الكانجي بأشكال صينية (يختلف 直 و骨 و角) والكانا بتصميم صيني. انضد النص بـ Noto Serif JP أو Shippori Mincho B1 والعناوين بـ Noto Sans JP، محمَّلة بـ loadCjkFonts. لا تحوي ملفات Fontsource اليابانية الهينتايغانا ولا غيرها من الكانا التاريخية (U+1B000–1B16F): يفشل loadCjkFonts عندها ويطبع PDF مربعات، فاكتب الكانا الحديثة أو ضع في أصول الوصفة خطًّا يحويها. ولا يحوي Shippori Mincho أيضًا الصائتين ō وū بالمدّة: انضد الروماجي بخط لاتيني. الكانا والكانجي والروماجي →
خطأ شائع
الخطوط الصينية تُحمَّل شرائح، عبر الكتلة cjk
يقدّم Fontsource العائلة الصينية أو اليابانية أو الكورية في نحو مئة ملف لكل وزن، يغطي كل منها نطاقًا من الحروف. لا يجلب loadFonts إلا ملف latin، فتأتي حروف الهان على الشاشة من خط النظام وتُقاس خطأً، ويسلّم fontsourceProvider ملف latin ذاك إلى PDF، فيطبعها مربعات فارغة. اذكر كتلة الأدوات cjk، واستدعِ loadCjkFonts(FONTS, markdown) بعد loadFonts (مرة لكل صوت، مع النص الذي يضعه، حين يستخدم الكتاب عدة خطوط CJK)، وأعطِ renderToPdf القيمة fontProvider: cjkPdfProvider: كلاهما يأخذ الملفات التي تحتوي حروف النص. الخطوط الصينية واليابانية والكورية →
خطأ شائع
لا مائل في الصينية
لا توفّر عائلات CJK وجهًا مائلًا، والطباعة الصينية تميّز التوكيد بنقاط بجانب الحروف، لا بالإمالة. في مستند موسوم بالصينية، يضع *…* نقاط التوكيد على الحروف الصينية التي يحتويها ويُبقي المائل للكلمات اللاتينية (cjk.emphasis: 'dots'، القيمة الافتراضية للصينية)؛ ويضعها :dots[…] في أي مكان. أما مع cjk.emphasis: 'italic'، أو في مستند موسوم بـ en أو es، فيجعل *…* لوحة Canvas تُميل الحروف القائمة، وهو مائل مصطنع لا تستخدمه الطباعة الصينية أبدًا (ويعود PDF إلى الوجه القائم): أبقِ الوسم الصيني، أو ضع التوكيد بالعريض أو بخط Kai (LXGW WenKai) عبر نمط فقرة أو نمط شارة. الخطوط الصينية واليابانية والكورية →
خطأ شائع
أي كائن headings يُلغي فاصل الصفحة قبل H1
ينتقل H1 افتراضيًا إلى صفحة فردية (always-odd)، لكن تمرير أي كائن headings يعيد ضبط هذا الافتراض، فتتوالى الفصول دون فاصل ولا يفعل span: 'page' شيئًا. أعد كتابة headings.levels[0].breakBefore: { enabled: true, parity } في كل إعداد. فصول تبدأ في صفحة فردية →
خطأ شائع
حمّل كل أوجه الخط قبل الإخراج
يقيس الإخراج النص بأوجه الخط التي حمّلها المتصفح ويخزّن العروض مؤقتًا، فالوجه الذي يصل بعد البناء الأول يترك فواصل أسطر خاطئة وملف PDF لم يعد يطابق الشاشة. حمّل كل وزن وكل نمط أولًا، واستدعِ clearMeasurementCache() قبل إعادة البناء إذا تأخر وصول أحدها. الخطوط قبل الإخراج →
تحذير الإخراج · indexReadingMissing
مدخل فهرس بلا قراءة
السبب. يرتّب الفهرس الياباني المداخل حسب قراءتها، ولا توجد خوارزمية تقرأ الكانجي بموثوقية: هذا المدخل فيه حرف كانجي ولا تعطي أي من علاماته قراءة (yomi، أو روبي بالكانا على النص المعلَّم، أو مفتاح sort بالكانا)، فيُرتَّب بعد مداخل الكانا حسب نقطة الترميز.
الحل. أضف yomi="…" بالقراءة بالكانا إلى إحدى علامات المدخل (:index[東京]{yomi="とうきょう"})، أو علّم نصًّا يحمل روبي ({東京|とう|きょう}). التوثيق →
- المصطلح الذي فيه كانجي بلا قراءة يثير
indexReadingMissing. فمن دونyomiنزل 明朝体 إلى آخر الفهرس، بعد わ行، بلا رأس، وحذّر البناء من أن للمدخل «كانجي بلا قراءة». - تحتاج
:ruby[…]داخل:index[…]إلى تهريب أقواسها، لأن العلامة تنتهي عند أول]؛ فاكتب الصيغة المختصرة،{版面|はん|めん}. - لا تُحتسب قراءة الروبي إلا إذا غطّت كل كانجي في المصطلح:
:index[行{取|ど}り]تترك 行 بلا قراءة وتحذّر، أما{行取|ぎょう|ど}りفتعطي ぎょうどり.
الحقوق
- الوصفة
- Ignacio Ferro
- النص
- نص أصلي, CC BY 4.0
- الخطوط
- Noto Serif JP (SIL OFL 1.1) · Noto Sans JP (SIL OFL 1.1)
- الكود
- MIT، مثل Postext
حرّر هذا الشرح ↗ (يفتح في تبويب جديد)مجلد الوصفة على GitHub ↗ (يفتح في تبويب جديد)


