Vai al contenuto principale
Ricetta numero 121

Ricettario · Capitolo 6 · Riquadri e note

Note sulla doppia pagina in un testo giapponese verticale

Il primo capitolo di Takekurabe di Ichiyō con sette note 傍注: (1) a destra della riga, le note di entrambe le pagine sul margine esterno della pagina sinistra.

In questa pagina
Output
Canvas · PDF
Livello
Intermedio
Postext
Provata con Postext 1.31.0
Richiede ≥ 1.25.0 · postext-pdf ≥ 1.25.0
Licenza
Aggiornata il 10 ott 2026
Codice MIT · Testo CC BY 4.0

pp. 二–三 · 2–3 di 4

  • Esempio in inglese: non c’è ancora un’edizione italiana
  • Formato 127 × 188 mm
  • 1 colonna
  • Shippori Mincho 9,3/16,3
  • Noto Sans JP
  • 4 pagine
  • Livello
  • Postext 1.30.0
  • Impaginato in 78 ms
  • 126 righe di codice

In breve

Un racconto classico giapponese con brevi spiegazioni dei suoi antichi toponimi. I piccoli numeri stanno accanto al testo, e le spiegazioni delle due pagine aperte sono raccolte sul bordo esterno della pagina sinistra.

Che cosa comporrai

Quattro pagine di un’edizione 四六判 di Takekurabe di Higuchi Ichiyō (たけくらべ, 1895–96), il suo primo capitolo, con sette note sull’antico quartiere di Yoshiwara che descrive. Le note sono 傍注 (bōchū), le note laterali dei classici giapponesi in edizione economica: un piccolo (1) sta a destra della riga dopo la parola che spiega, e le note citate in entrambe le pagine di una doppia pagina si compongono sul margine esterno della pagina sinistra, sotto un filetto lungo un terzo della riga, in un corpo più piccolo di un quinto. Il lettore trova ogni nota senza voltare pagina. Il testo, 42 caratteri in verticale per 17 righe in Shippori Mincho a 9,25 pt, conserva l’unico lungo paragrafo di Ichiyō e i suoi segni sesamo su ぎつちよんちよん. Le note al piede della colonna in un romanzo europeo sono la Nº 071.

Questa ricetta risponde a

  • Come compongo le note di un libro giapponese: (1) a fine capitolo, al piede della pagina o accanto al testo lungo la doppia pagina?
  • Come faccio le note a piè di pagina?
  • Come segno il testo giapponese con punti d’enfasi a sesamo o una linea laterale invece del corsivo?

La risposta breve

script.js · righe 32–48nel codice completo
// placement 'spread' gathers the notes cited on the two pages of a spread at the end of the
// left-hand page's text, the fore-edge in a book bound on the right, under a rule a third of
// the line long, and numbers them per spread. The marker (1) stands small at the right of
// the line, its digits upright, and the note opens with its number and a full note-em.
const footnotes = {
  placement: 'spread', // 傍注; 'chapterEnd' sets 後注 after the chapter instead
  numbering: 'spread',
  markerPosition: 'right', markerTemplate: '({n})', // written out: 'ja' sets them so
  numberGap: 'em', hangingIndent: em(2), // turnovers clear the number
  fontSize: em(0.8), lineHeight: em(1.5),
  separator: { width: 1 / 3, lineWidth: pt(0.5), color: col('plum') },
};
// Ichiyō's sesame marks (傍点) over ぎつちよんちよん: the mark Japan uses, right of the line.
const cjk = {
  grid: { enabled: true, charsPerLine: CHARS, linesPerPage: LINES },
  emphasisMark: { style: 'sesame', position: 'over' },
};

Ingredienti

Tipo
Shippori Mincho, Noto Sans JP (SIL OFL 1.1)
Risorse
Nessuna: ogni immagine è disegnata nel codice

Preparazione

#1 · Raccogli le note della doppia pagina sul margine esterno della pagina sinistra

Il codice è nella risposta breve qui sopra. footnotes.placement: 'spread' compone le note citate alle pagine 2 e 3 alla fine del testo di pagina 3, che in un libro rilegato a destra è il suo margine esterno, e numbering: 'spread' fa ripartire da (1) le note di ogni doppia pagina (Note a piè di pagina). Una nota citata sulla pagina destra aspetta quella sinistra; se le note lasciano alla pagina sinistra meno di una riga di testo, le altre restano al piede della pagina destra, come consente JLReq. Un documento con tag ja compone già i richiami di un libro verticale come ({n}) a destra della riga, a 0,7 em, con le cifre dritte: lo mostrano le due righe scritte per esteso. numberGap: 'em' mette un carattere di nota pieno dopo il numero e hangingIndent: em(2) allinea oltre di esso le righe che vanno a capo.

#2 · Scrivi i richiami dove li mette il giapponese

Il Markdown scrive 見返り柳[^1] dopo la parola che la nota spiega e le note come paragrafi [^1]: … alla fine del capitolo. Il richiamo segue la parola che la nota spiega; uno che chiude una frase va prima del suo 。 (…先生[^1]。), e il motore non lascia mai che 。 o un richiamo comincino una riga. Le sette note sono state scritte per questa ricetta: che cosa erano il 見返り柳 e la お齒ぐろ溝, dove si trovava 大音寺前, che cosa si vendeva al 酉の市, in un giapponese moderno semplice.

#3 · Conserva i segni sesamo della fonte

cjk.emphasisMark decide che cosa stampano *…* e :dots[…] in questo libro: punti sesamo, a destra di una riga verticale. Un documento ja li usa già; scritti per esteso, attivano i kenten nell’elenco delle funzioni. Il testo di Aozora segna ぎつちよんちよん con 傍点, e aozora.py l’ha scritto come :dots[ぎつちよんちよん]{style=sesame}; i libri cinesi mettono invece i punti sotto la riga (Segni, ruby e warichu).

#4 · Apri su una doppia pagina

script.js · righe 52–71nel codice completo
// Page-anchored design on a vertical page: x runs down the page from its top edge, y across
// it leftwards from its right edge. The title stands on the page's axis, the author lower.
const PT = 25.4 / 72; // mm in a point
const page = (down, across) => ({ anchor: { to: 'page', edge: 'top-left' },
  offset: { x: mm(down), y: mm(across) } });
const TITLE = 26; // pt
// parity 'any': the story opens on the next page, either side (the default waits for a recto).
const story = { level: 1, breakBefore: { enabled: true, parity: 'any' },
  advancedDesign: { enabled: true, minHeight: lines(LINES), 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: page(42, 127 / 2 - (TITLE * PT) / 2) },
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.6), color: col('plum'),
      placement: { ...page(42, 127 / 2 + (TITLE * PT) / 2 + 5), size: { width: mm(24) } } },
    { kind: 'text', id: 'author', content: '{author}', fontFamily: MINCHO, fontSize: pt(12),
      letterSpacing: pt(4), color: col('ink'),
      placement: page(118, 127 / 2 + (TITLE * PT) / 2 + 3) },
  ] } } };
const section = { level: 2, fontFamily: MINCHO, fontWeight: 700, fontSize: pt(11),
  lineSpan: 3, indent: em(7) }; // 3行取り, seven characters down, as the Aozora text marks it

La prima frase del capitolo cita quattro note nelle sue prime tre righe. Composte sotto un titolo a pagina 1, lascerebbero spazio a tre righe di testo: la pagina sinistra contiene le proprie note e quelle di nessun altro. Una pagina del titolo da sola a pagina 1 sposta il capitolo a pagina 2, una pagina destra, così la prima doppia pagina contiene il testo e tutte le sue note. La pagina del titolo è un titolo di livello 1 la cui grafica riempie la pagina, ancorata al rifilo perché il titolo stia sull’asse della pagina.

#5 · Metti hashira e folio lungo il margine esterno

script.js · righe 75–89nel codice completo
const foreEdge = (id, content, parity, edge, y, pages) => ({
  kind: 'text', id, content, parity, pages, writingMode: 'vertical-rl',
  fontFamily: GOTHIC, fontSize: pt(7), letterSpacing: pt(1), color: col('muted'),
  overflow: 'clip', placement: { anchor: { to: 'outer', edge }, offset: { y: em(y) } },
});
const header = { elements: [
  foreEdge('hashira', '{title}', 'odd', 'top', 2, 'body'),
  foreEdge('folio', '{pageNumber}', 'all', 'bottom', -2, 'body'),
] };
// The title page carries the colophon across the foot, in the edition's language.
const 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(-6) },
    size: { width: mm(100) } } }] };

Il titolo scende lungo il margine esterno delle pagine sinistre, fuori dalle note, e i folio in kanji stanno al piede di ogni pagina tranne quella del titolo.

La ricetta completa

Sandbox
// ═══ Postext Cookbook · Nº 121 · Notes beside a vertical Japanese text, across the spread ══
// https://postext.dev/en/cookbook/vertical-notes-and-sidenotes
// Code: MIT · Text: 樋口一葉『たけくらべ』, Aozora Bunko 389 (PD); notes CC BY 4.0 · Pictures: none
// Fonts: Shippori Mincho, Noto Sans JP (SIL OFL 1.1) · Needs postext ≥ 1.25.0
import {
  buildDocument, withLoadedFonts, renderPageToCanvas, 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 story is Japanese in both
const RECIPE = 'vertical-notes-and-sidenotes';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: ink, a muted plum for the note numbers' rule, cream paper
const palette = {
  ink: '#211d1b', // the text and the notes
  plum: '#6b3a55', // 紫: the rule over the notes, the engine's defaults
  muted: '#6b645d', // the hashira and the folios
  paper: '#fbf7ef',
};
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.plum, model: 'hex' } },
];
// #endregion
const [MINCHO, GOTHIC] = ['Shippori Mincho', 'Noto Sans JP'];
const [BODY, LEAD, CHARS, LINES] = [9.25, 16.25, 42, 17]; // pt, pt: 四六判 42字 × 17行
const lines = (n) => pt(n * LEAD);

// #region answer: 傍注, the notes of both pages of a spread at the fore-edge of the left one
// placement 'spread' gathers the notes cited on the two pages of a spread at the end of the
// left-hand page's text, the fore-edge in a book bound on the right, under a rule a third of
// the line long, and numbers them per spread. The marker (1) stands small at the right of
// the line, its digits upright, and the note opens with its number and a full note-em.
const footnotes = {
  placement: 'spread', // 傍注; 'chapterEnd' sets 後注 after the chapter instead
  numbering: 'spread',
  markerPosition: 'right', markerTemplate: '({n})', // written out: 'ja' sets them so
  numberGap: 'em', hangingIndent: em(2), // turnovers clear the number
  fontSize: em(0.8), lineHeight: em(1.5),
  separator: { width: 1 / 3, lineWidth: pt(0.5), color: col('plum') },
};
// Ichiyō's sesame marks (傍点) over ぎつちよんちよん: the mark Japan uses, right of the line.
const cjk = {
  grid: { enabled: true, charsPerLine: CHARS, linesPerPage: LINES },
  emphasisMark: { style: 'sesame', position: 'over' },
};
// #endregion

// #region opener: a title page (扉) on its own, so the story opens on a spread
// Page-anchored design on a vertical page: x runs down the page from its top edge, y across
// it leftwards from its right edge. The title stands on the page's axis, the author lower.
const PT = 25.4 / 72; // mm in a point
const page = (down, across) => ({ anchor: { to: 'page', edge: 'top-left' },
  offset: { x: mm(down), y: mm(across) } });
const TITLE = 26; // pt
// parity 'any': the story opens on the next page, either side (the default waits for a recto).
const story = { level: 1, breakBefore: { enabled: true, parity: 'any' },
  advancedDesign: { enabled: true, minHeight: lines(LINES), 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: page(42, 127 / 2 - (TITLE * PT) / 2) },
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.6), color: col('plum'),
      placement: { ...page(42, 127 / 2 + (TITLE * PT) / 2 + 5), size: { width: mm(24) } } },
    { kind: 'text', id: 'author', content: '{author}', fontFamily: MINCHO, fontSize: pt(12),
      letterSpacing: pt(4), color: col('ink'),
      placement: page(118, 127 / 2 + (TITLE * PT) / 2 + 3) },
  ] } } };
const section = { level: 2, fontFamily: MINCHO, fontWeight: 700, fontSize: pt(11),
  lineSpan: 3, indent: em(7) }; // 3行取り, seven characters down, as the Aozora text marks it
// #endregion

// #region furniture: the hashira and kanji folios down the fore-edge
const foreEdge = (id, content, parity, edge, y, pages) => ({
  kind: 'text', id, content, parity, pages, writingMode: 'vertical-rl',
  fontFamily: GOTHIC, fontSize: pt(7), letterSpacing: pt(1), color: col('muted'),
  overflow: 'clip', placement: { anchor: { to: 'outer', edge }, offset: { y: em(y) } },
});
const header = { elements: [
  foreEdge('hashira', '{title}', 'odd', 'top', 2, 'body'),
  foreEdge('folio', '{pageNumber}', 'all', 'bottom', -2, 'body'),
] };
// The title page carries the colophon across the foot, in the edition's language.
const 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(-6) },
    size: { width: mm(100) } } }] };
// #endregion

const config = () => ({
  locale: 'ja', // written out, never LANG (gotcha: ja-locale-tag)
  colorPalette,
  page: {
    sizePreset: 'custom', width: mm(127), height: mm(188), dpi: 150, // 四六判
    backgroundColor: col('paper'),
    margins: { top: mm(27), bottom: mm(21), left: mm(13), right: mm(16), mirror: true },
    pageNumbering: { format: 'japanese-informal' },
  },
  layout: { layoutType: 'single', writingMode: 'vertical-rl' },
  cjk,
  footnotes,
  bodyText: {
    fontFamily: MINCHO, 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: [story, section] },
  header,
  footer,
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
Esempio Markdown · 23 righe · content.en.mdtitle: "たけくらべ" author: "樋口一葉" --- # たけくらべ {colophon="Higuchi Ichiyō, たけくらべ (Child’s Play, 1895–96), chapter one; the notes are this recipe’s. Text: Aozora Bunko 389, public domain. Set in Shippori Mincho and Noto Sans JP (SIL OFL)."} ## 一 廻れば大門の見返り柳[^1]いと長けれど、お齒ぐろ溝[^2]に{燈火|ともしび}うつる三階[^3]の騷ぎも手に取る如く、明けくれなしの車の{行來|ゆきゝ}にはかり知られぬ全盛をうらなひて、{大音寺前|だいおんじまへ}[^4]と名は佛くさけれど、さりとは陽氣の町と住みたる人の申き、{三嶋神社|みしまさま}の角をまがりてより是れぞと見ゆる{大厦|いへ}もなく、かたぶく軒端の十軒長屋二十軒長や、商ひはかつふつ利かぬ處とて半さしたる雨戸の外に、あやしき{形|なり}に紙を切りなして、胡粉ぬりくり彩色のある{田樂|でんがく}みるやう、裏にはりたる串のさまもをかし、一軒ならず二軒ならず、朝日に干して夕日に仕舞ふ手當こと〴〵しく、一家内これにかゝりて夫れは何ぞと問ふに、知らずや霜月{酉|とり}の日[^5]例の神社に欲深樣のかつぎ給ふ是れぞ熊手の下ごしらへといふ、正月門松とりすつるよりかゝりて、一年うち通しの夫れは誠の商賣人、片手わざにも夏より手足を色どりて、{新年着|はるぎ}の支度もこれをば當てぞかし、南無や大鳥大明神、買ふ人にさへ大福をあたへ給へば製造もとの我等萬倍の利益をと人ごとに言ふめれど、さりとは思ひのほかなるもの、此あたりに大長者のうわさも聞かざりき、住む人の多くは{廓者|くるわもの}にて良人は小格子[^6]の何とやら、下足札そろへてがらんがらんの音もいそがしや夕暮より羽織引かけて立出れば、うしろに切火打かくる女房の顏もこれが見納めか十人ぎりの側杖無理{情死|しんぢう}のしそこね、恨みはかゝる身のはて危ふく、すはと言はゞ命がけの勤めに{遊山|ゆさん}らしく見ゆるもをかし、娘は{大籬|おほまがき}の{下新造|したしんぞ}とやら、七軒の何屋が客廻しとやら、{提燈|かんばん}さげてちよこちよこ走りの修業、卒業して何にかなる、とかくは檜舞臺と見たつるもをかしからずや、垢ぬけのせし三十あまりの年増、小ざつぱりとせし唐棧ぞろひに紺足袋はきて、雪駄ちやら〳〵忙がしげに横抱きの小包はとはでもしるし、茶屋が棧橋とんと沙汰して、廻り遠や此處からあげまする、誂へ物の仕事やさんと此あたりには言ふぞかし、一體の風俗よそと變りて、{女子|おなご}の後帶きちんとせし人少なく、がらを好みて巾廣の卷帶、年増はまだよし、十五六の小癪なるが{酸漿|ほゝづき}ふくんで此{姿|なり}はと目をふさぐ人もあるべし、所がら是非もなや、昨日河岸店に何紫の源氏名耳に殘れど、けふは地廻りの吉と手馴れぬ燒鳥の夜店を出して、身代たゝき骨になれば再び古巣への{内儀|かみさま}姿、どこやら素人よりは見よげに覺えて、これに染まらぬ子供もなし、秋は九月{仁和賀|にわか}[^7]の頃の大路を見給へ、さりとは宜くも學びし{露八|ろはち}が物眞似、{榮喜|えいき}が{處|しよ}作、孟子の母やおどろかん上達の速やかさ、うまいと褒められて今宵も一廻りと生意氣は七つ八つよりつのりて、やがては肩に置手ぬぐひ、鼻歌のそゝり節、十五の少年がませかた恐ろし、學校の唱歌にも:dots[ぎつちよんちよん]{style=sesame}と拍子を取りて、運動會に木やり音頭もなしかねまじき風情、さらでも教育はむづかしきに教師の苦心さこそと思はるゝ{入谷|いりや}ぢかくに育英舍とて、私立なれども生徒の數は千人近く、狹き校舍に目白押の窮屈さも教師が人望いよ〳〵あらはれて、唯學校と一ト口にて此あたりには呑込みのつくほど成るがあり、通ふ子供の數々に或は火消鳶人足、おとつさんは{刎橋|はねばし}の番屋に居るよと習はずして知る其道のかしこさ、梯子のりのまねびにアレ忍びがへしを折りましたと訴へのつべこべ、三百といふ代言の子もあるべし、お前の父さんは馬だねへと言はれて、名のりや{愁|つ}らき子心にも顏あからめるしほらしさ、出入りの{貸座敷|いへ}の祕藏息子寮住居に華族さまを氣取りて、ふさ付き帽子面もちゆたかに洋服かる〴〵と花々敷を、坊ちやん坊ちやんとて此子の追從するもをかし、多くの中に{龍華|りうげ}寺の{信如|しんによ}とて、{千筋|ちすぢ}となづる黒髮も今いく{歳|とせ}のさかりにか、やがては墨染にかへぬべき袖の色、{發心|ほつしん}は腹からか、坊は親ゆづりの勉強ものあり、性來をとなしきを友達いぶせく思ひて、さま〴〵の惡戲をしかけ、猫の死骸を繩にくゝりてお役目なれば{引導|いんだう}をたのみますと投げつけし事も有りしが、それは昔、今は校内一の人とて假にも{侮|あなど}りての處業はなかりき、歳は十五、{並背|なみぜい}にていが栗の{頭髮|つむり}も思ひなしか俗とは變りて、{藤本信如|ふぢもとのぶゆき}と{訓|よみ}にてすませど、何處やら{釋|しやく}といひたげの素振なり。 [^1]: 吉原遊廓の入口、大門へ下る衣紋坂の上の日本堤に立っていた柳。帰りの客がここで名残を惜しんで振り返ったという。 [^2]: 遊廓のまわりをめぐる堀。遊女が逃げ出さないように掘られたもので、水が黒く濁っていたのでこう呼ばれた。 [^3]: 廓の中に建ち並ぶ三階建ての大きな妓楼。 [^4]: 吉原の裏手、下谷龍泉寺町のあたりの通称。大音寺の門前にあたる。 [^5]: 十一月の酉の日に鷲(おおとり)神社で開かれる酉の市。縁起物の熊手が売られる。 [^6]: 格の低い、小さな遊女屋。 [^7]: 吉原で秋に催された俄(にわか)。芸者や幇間が茶番や踊りを見せて廓の中を練り歩いた。
`; // content.<lang>.md: the same Japanese text in both // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { 'Shippori Mincho': ['400', '700'], // the text and the notes; the title and 一 'Noto Sans JP': ['400'], // the hashira, the folios and the colophon }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── const title = markdown.match(/^title: "(.*)"$/m)[1]; const author = markdown.match(/^author: "(.*)"$/m)[1]; await loadFonts(FONTS, markdown); // the Latin files: the colophon // The notes' (1) and their upright digits come from the text face too. await loadCjkFonts({ [MINCHO]: ['400'] }, `${markdown}()0123456789`, { vertical: true }); await loadCjkFonts({ [MINCHO]: ['700'] }, `${title}${author}一`); await loadCjkFonts({ [GOTHIC]: ['400'] }, `${title}${markdown.match(/colophon="([^"]*)"/)[1]}一二三四五六七八九十`, { vertical: true }); const doc = await withLoadedFonts(() => buildDocument({ markdown }, config()), { ...kitFonts(FONTS), text: markdown }); showBook(doc, { title: t({ en: 'Notes beside a vertical Japanese text, across the spread', es: 'Notas junto a un texto japonés vertical, en la doble página' }) }); offerPdf(() => renderToPdf(doc, { fontProvider: cjkPdfProvider }), `${RECIPE}.pdf`);
Kit · core, fonts, viewer, pdf, cjk: uguale in ogni ricetta · 458 righe// ─── 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 v3 ── the same in every recipe · postext.dev/cookbook // Postext loads the faces a config asks for before layout (prepareFonts, // buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the // files the PDF embeds too. /** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts: * the Fontsource resolver, and the faces of FONTS loaded with the ones the * config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */ function kitFonts(FONTS = {}) { const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) => ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' }))); return { resolve: fontsourceResolver, faces }; } /** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek * when `text` needs them and the family ships them (kitSubsetsFor), each with * its unicode-range. Null for a weight or slant Fontsource does not ship. */ async function fontsourceResolver(family, weight, style, { text = '' } = {}) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null; kitStatus('Loading fonts…'); const at = await fontsourceAt(id); return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({ source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`, unicodeRange: kitRanges()[subset], })); } /** The unicode-range of each Fontsource file the kit loads (a function: the * kit is inlined after the pen's code, which runs before a const would be set). */ function kitRanges() { return { 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', greek: 'U+0370-03FF', }; } /** For the cjk, arabic and comics blocks, which add their own files: the * faces of `faces` = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With * `optional`, a face Fontsource does not ship is skipped instead of failing. * The files are added once all have loaded, in the order asked (latin, then * latin-ext, then greek), whatever order they arrive in: the last one added * wins a character two files hold. Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = kitRanges(); const jobs = []; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal']) .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await const at = fontsourceAt(id); const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null; const subsets = ['latin', ...kitSubsetsFor(text, meta)]; for (const [weight, style] of todo) { if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `${await at}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().catch(() => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return ready.filter((face) => face && document.fonts.add(face)).length; } /** A loaded FontFace covers this family, weight and style (fonts.check() would * also say yes 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; } /** The files beyond latin `text` needs that `meta`'s family ships. */ function kitSubsetsFor(text, meta) { return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']] .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x); } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** A package's folder at the one version @5 names now (#766). */ function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) { return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json()) .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`); } /** The family's Fontsource metadata (weights, styles, subsets), 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 v2 ── the same in every recipe · postext.dev/cookbook /** The pages as spreads on a dark desk, page 1 alone, then verso | recto, * each painted when it scrolls near. Other options go to renderPageToCanvas. */ function showPages(docs, { title, width = 460, ...render } = {}) { 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, { ...render, 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 v3 ── the same in every recipe that exports a PDF /** The Fontsource files the screen used, as TrueType, with their range and * place: the nearest weight the family ships, upright with no italic; * latin, then what its letters need (kitSubsetsFor). */ async function fontsourceProvider(family, weight, style, request) { 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 text = String.fromCodePoint(...(request?.codePoints ?? [])); const files = await Promise.all(['latin', ...kitSubsetsFor(text, meta)].map(async (subset, order) => { const res = await fetch(`${await fontsourceAt(id)}files/${id}-${subset}-${w}-${s}.woff2`); if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} ${subset}`); return Object.assign(await decompressWoff2(new Uint8Array(await res.arrayBuffer())), { unicodeRange: kitRanges()[subset], order }); })); return files.length === 1 ? files[0] : files; } /** A "Build the PDF" button; then "Open the PDF" (a new tab: CodePen's frame * shows no 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 v2 ── 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 = `${id}/${weight}${style === 'italic' ? '-italic' : ''}.css`; if (!cjkSlices.cache.has(css)) { let at; cjkSlices.cache.set(css, fontsourceAt(id).then((a) => fetch(at = a + css.slice(id.length + 1))) .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], at).href, range, ranges }; }).reverse())); } return cjkSlices.cache.get(css); } /** The file of `slices` the screen sets code point `cp` in, if any: the * last one loadCjkFonts adds whose range holds it. A numbered file that * repeats a letter of latin is added after latin, and wins it. */ function cjkSliceFor(slices, cp) { const holds = (slice) => slice.ranges.some(([lo, hi]) => cp >= lo && cp <= hi); return slices.reduce((last, slice) => (holds(slice) ? slice : last), undefined); } /** 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 the engine's later round * 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 the * screen set its pages' characters in (`request.codePoints`), each marked * with its unicode-range and its place in the order loadCjkFonts added * them: postext-pdf sets a character two files hold in the one added * last, as the browser does. 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.map(async (slice, order) => { if (!picked.has(slice)) return null; const res = await fetch(slice.url); if (!res.ok) throw new Error(`Fontsource file ${slice.url} (${res.status})`); const bytes = await decompressWoff2(new Uint8Array(await res.arrayBuffer())); return Object.assign(bytes, { unicodeRange: slice.range, order }); })).then((files) => files.filter(Boolean)); } /** 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, each marked with its unicode-range and its place in that order: * the PDF sets a character in the file the browser took it from. 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 = `${await fontsourceAt(id)}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)]); const ranges = kitRanges(); return ext ? [Object.assign(latin, { unicodeRange: ranges.latin, order: 0 }), Object.assign(ext, { unicodeRange: ranges['latin-ext'], order: 1 })] : 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 ───────────────────────────────────────────────────────────────────────

Lo script.js composto funziona così com’è: incollalo nello script modulo di qualsiasi pagina, oppure apri la ricetta su CodePen. Cartella della ricetta su GitHub ↗ (si apre in una nuova scheda)

Varianti

#Raccogli le note alla fine del capitolo

Senza placement, un libro ja verticale compone le sue note come 後注 (kōchū): dopo il testo del capitolo, sotto lo stesso filetto, numerate lungo tutto il capitolo. Qui riempiono la metà sinistra di pagina 4.

-  placement: 'spread', // 傍注; 'chapterEnd' sets 後注 after the chapter instead
-  numbering: 'spread',
+  placement: 'chapterEnd', // 後注, the default for a vertical ja book
+  numbering: 'chapter',

#Metti il richiamo accanto alla parola

markerPosition: 'side' compone un richiamo più piccolo nello spazio accanto alla riga, come si compone il ruby interlineare, così non occupa alcuno spazio nella riga.

-  markerPosition: 'right', markerTemplate: '({n})', // written out: 'ja' sets them so
+  markerPosition: 'side', markerTemplate: '{n}',

Errori frequenti

Errore frequente

Assegna a un testo giapponese il tag 'ja', mai zh-Hans o LANG

Le edizioni di una ricetta sono en ed es, ma un esempio giapponese è giapponese in entrambe: `locale: LANG` gli darebbe il tag inglese o spagnolo, e un tag cinese lo comporrebbe con le regole cinesi (punteggiatura Kaiming, kana piccoli liberi di aprire una riga, 图 per 図, forme cinesi dei glifi nel PDF). Da postext 1.30 il tag è anche la lingua usata per lo shaping del testo (misura, disegno e incorporamento), quindi qualunque tag diverso da 'ja' dà forme dei glifi diverse da quelle giapponesi (quelle cinesi con un tag cinese) e altri a capo. Scrivi 'ja': sceglie la regione Giappone (divisione in righe e punteggiatura JLReq, segni d’enfasi a sesamo, spaziatura dei furigana, etichette 図 e 表) e disattiva la sillabazione. Il lint fa fallire un testo con kana sotto un tag zh o ko. Divisione in righe del giapponese (kinsoku) →

Errore frequente

I caratteri cinesi si caricano a fette, tramite il blocco cjk

Fontsource serve una famiglia cinese, giapponese o coreana come un centinaio di file per peso, ognuno dei quali copre un intervallo di caratteri. loadFonts scarica solo il file latin, così a schermo i caratteri Han vengono da un font di sistema e sono misurati male, e fontsourceProvider passa al PDF quel file latin, che li stampa come riquadri vuoti. Elenca il blocco cjk del kit, chiama loadCjkFonts(FONTS, markdown) dopo loadFonts (una volta per voce tipografica, con il testo che compone, quando il libro usa più caratteri CJK) e passa a renderToPdf fontProvider: cjkPdfProvider: entrambi prendono i file che contengono i caratteri del testo. Font cinesi, giapponesi e coreani →

Errore frequente

Un libro rilegato a destra mostra le doppie pagine con showBook

In un libro rilegato a destra (testo arabo, ebraico o persiano, cinese verticale, o page.binding 'right') la pagina 1 resta il recto, ma sta a sinistra del dorso, e le coppie si leggono [3 | 2]. showPages dispone ogni libro come rilegato a sinistra; showBook del blocco book (il blocco cjk porta la stessa funzione) legge doc.binding e rispecchia le coppie. capture.hero indica comunque una doppia pagina in ordine di lettura, [verso, recto]: [2, 3]. Libri rilegati a destra →

Crediti

Testo
  • 樋口一葉「たけくらべ」 (Child’s Play, 1895–96), chapter one; Aozora Bunko card 389, from 「日本現代文學全集10 樋口一葉集」 (Kōdansha, 1962), input 青空文庫, proofreading 米田進 and 小林繁雄, converted with aozora.py; the seven notes were written for this recipe (CC BY 4.0) · Higuchi Ichiyō; Aozora Bunko volunteers · pubblico dominio
Font
Shippori Mincho (SIL OFL 1.1) · Noto Sans JP (SIL OFL 1.1)
SandboxPDF