In breve
La prima pagina di un diario del 1909 che il poeta Takuboku scrisse in giapponese con lettere latine, con una lettura in scrittura giapponese sotto ogni paragrafo.
Che cosa comporrai
Quattro pagine del Rōmaji nikki, il diario che Ishikawa Takuboku tenne a Tokyo nella primavera del 1909 in lettere latine, perché la moglie non potesse leggerlo: le righe del titolo del suo quaderno e la prima voce, il 7 aprile, fino a Kanasii koto da! Il diario è composto come edizione di studio su una pagina A5: il rōmaji in Source Serif 4, giustificato e mai sillabato, e sotto ogni paragrafo la sua lettura in kana e kanji in Noto Serif JP, più piccola, più leggera e rientrata. Takuboku scrive nel sistema Nippon-shiki, si, tu, hu, con l’accento circonflesso sulle vocali lunghe, Tôkyô; le righe del curatore usano lo Hepburn con i macron, Rōmaji, ed entrambi arrivano nel PDF.
Questa ricetta risponde a
- Come compongo il giapponese scritto in rōmaji: quale tag di lingua, quale sillabazione e i macron nel PDF?
- Quali font compongono il giapponese, con i kanji nelle loro forme giapponesi, a schermo e nel PDF?
La risposta breve
// ja-Latn is Japanese written in Latin script (BCP 47). Its language is ja, so the kana
// under each paragraph get the Japanese rules (kinsoku, 、。 spacing, the space after !)
// and the built-in strings are Japanese; and Japanese has no hyphenation patterns, so the
// rōmaji is never divided at a line end. English patterns would divide tatinobotta or
// Surigarasu by English syllables, not by the morae a Japanese reader hears.
const language = { locale: 'ja-Latn' }; // spread into the config
const bodyText = {
fontFamily: SERIF, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'),
boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
textAlign: 'justify', firstLineIndent: em(1.2), indentAfterHeading: false,
optimalLineBreaking: true, maxWordSpacing: 1.8, // no hyphens: the spaces take the slack
};
// The reading in kana, under its paragraph: smaller, a shade lighter, indented.
const kana = { id: 'kana', fontFamily: MINCHO, fontSize: pt(8.6), lineHeight: pt(LEAD),
color: col('kana'), textAlign: 'justify', indent: em(1.5), firstLineIndent: em(1),
marginTop: pt(LEAD / 4), marginBottom: pt(LEAD * 0.75) };
Ingredienti
- Funzionalità
- Kana, kanji e rōmajiSillabazione e lingua del documentoFont cinesi, giapponesi e coreaniSpaziatura della punteggiatura giapponese (yakumono)Divisione in righe del giapponese (kinsoku)Carattere del testo correnteAperture progettateStili di titoloAttributi dei titoliStili di paragrafoTestatine e folioTestatine secondo il ruolo della paginaMargini speculariColore della cartaTavolozza di colori semanticaEsportazione in PDFFont incorporati nel PDF
- Tipo
- Source Serif 4, Source Sans 3, Noto Serif JP (SIL OFL 1.1)
- Risorse
- Nessuna: ogni immagine è disegnata nel codice
Preparazione
#1 · Etichetta il testo ja-Latn
Il codice è la risposta breve qui sopra. ja-Latn è il tag BCP 47 per il giapponese scritto in alfabeto latino. La sua lingua è ja, quindi la lettura sotto ogni paragrafo riceve le regole giapponesi: kinsoku, spaziatura di 、。 e lo spazio em che il motore aggiunge dopo ! dentro una frase, come in 「ええ! ええ!」. Il giapponese non ha pattern di sillabazione, quindi il rōmaji non viene mai diviso; con en, i pattern inglesi dividerebbero parole come tatinobotta o Surigarasu secondo le sillabe inglesi, non secondo le more che sente un lettore giapponese. Senza trattini sono gli spazi ad assorbire il gioco, quindi la giustezza è di circa 70 caratteri e maxWordSpacing è 1,8 (sillabazione).

#2 · Metti i macron nel PDF
// Fontsource keeps ō ū ā in a latin-ext file. cjkPdfProvider serves Noto Serif JP from its
// slices, and adds that file after the latin one for a Latin face whose text needs it.
offerPdf(() => renderToPdf(doc, { fontProvider: cjkPdfProvider }), `${RECIPE}.pdf`);
Fontsource tiene ō ū ā in un file latin-ext a parte. Lo schermo lo carica (loadFonts vede la ō nell’esempio), e il cjkPdfProvider del kit fa lo stesso per il PDF. Serve Noto Serif JP dalle sue sezioni giapponesi, e quando un carattere latino compone una lettera che solo latin-ext contiene, risponde con due file, latin e poi latin-ext; postext-pdf prende ogni glifo dal primo file che lo contiene. Il semplice fontsourceProvider del kit passa al PDF solo il file latin: con quello, il PDF non avrebbe alcun glifo per la ō.
#3 · Apri con le righe del diario stesso
const onField = { color: col('paper'), align: 'center', overflow: 'wrap' };
const title = {
id: 'title', numbered: false, toc: false, span: 'page', runningChapter: false,
breakBefore: { enabled: true, parity: 'any' }, // its page is an opener: no running heads
advancedDesign: { enabled: true, minHeight: mm(166), slot: { elements: [
{ kind: 'box', id: 'field', style: { backgroundColor: col('indigo') },
placement: { anchor: { to: 'bleed', edge: 'top-left' }, size: { width: 'fill',
height: 'fill' } } },
{ kind: 'text', id: 'lines', content: '{attr.lines}', ...onField, fontFamily: SERIF,
fontWeight: 600, fontSize: pt(20), lineHeight: 1.9, letterSpacing: pt(5),
placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(18) } } },
{ kind: 'rule', id: 'rule', thickness: pt(0.5), color: col('paper'),
placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(122) },
size: { width: mm(24) } } },
{ kind: 'text', id: 'note', content: '{attr.note}', inlineMarks: true, ...onField,
fontFamily: SANS, fontSize: pt(8.5), lineHeight: 1.5, placement: { anchor: { to:
'container', edge: 'top' }, offset: { y: mm(130) }, size: { width: mm(84) } } },
] } },
};
// The entry: its day as a heading, the address under it from an attribute.
const entry = {
level: 2, fontFamily: SANS, fontWeight: 600, fontSize: pt(10), letterSpacing: pt(2.5),
color: col('indigo'), marginTop: pt(LEAD), marginBottom: pt(LEAD), advancedDesign: {
enabled: true, minHeight: pt(2 * LEAD), slot: { elements: [
{ kind: 'text', id: 'day', content: '{titleText}', fontFamily: SANS, fontWeight: 600,
fontSize: pt(10), letterSpacing: pt(2.5), color: col('indigo'),
placement: { anchor: { to: 'container', edge: 'top-left' } } },
{ kind: 'text', id: 'place', content: '{attr.place}', fontFamily: SANS, fontSize: pt(7.5),
letterSpacing: pt(1), color: col('muted'), overflow: 'wrap', align: 'left',
placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: pt(LEAD) },
size: { width: 'fill' } } },
] } },
};
Il quaderno comincia con NIKKI. I. MEIDI 42 NEN. 1909. APRIL. TOKYO., una voce per riga. Il titolo le prende da un attributo, ogni \n una nuova riga, e le compone in maiuscole spaziate su un campo indaco, la tela della copertina di un quaderno. La sua pagina è un’apertura, quindi le testatine la saltano.
#4 · Testatine per parità
const head = (id, content, parity, edge) => ({ kind: 'text', id, content, parity,
pages: 'body', fontFamily: SANS, fontSize: pt(7.5), letterSpacing: pt(1.5), color: col('muted'),
placement: { anchor: { to: 'page', edge }, offset: { y: mm(13), // 'top' centres on the page
x: mm(edge.endsWith('left') ? 16 : edge.endsWith('right') ? -16 : 0) } } });
const header = { elements: [
head('folio-even', '{pageNumber}', 'even', 'top-left'),
head('book', 'ROMAZI NIKKI · 1909', 'even', 'top'),
head('place', 'TOKYO · APRIL', 'odd', 'top'),
head('folio-odd', '{pageNumber}', 'odd', 'top-right'),
] };
Il verso porta il nome del diario e il recto il luogo e il mese, con i folio negli angoli esterni, in Source Sans 3 a 7,5 pt.
La ricetta completa
// ═══ Postext Cookbook · Nº 130 · Japanese in Latin letters: Takuboku's Rōmaji Diary ════ // https://postext.dev/en/cookbook/romaji-nikki // Code: MIT · Text: Ishikawa Takuboku, 1909 (public domain); transcription (CC BY 4.0) // Fonts: Source Serif 4, Source Sans 3, Noto Serif JP (SIL OFL 1.1) · Needs postext ≥ 1.25.0 // A diary written in Japanese with Latin letters, and its reading in kana under each paragraph. import { buildDocument, withLoadedFonts, renderPageToCanvas } from 'https://esm.sh/postext'; import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf'; const LANG = 'en'; // @lang: the language of the note; the diary is Japanese in both editions const RECIPE = 'romaji-nikki'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: diary ink, a faded indigo, the cream of a notebook const palette = { ink: '#22201d', // the rōmaji indigo: '#34497a', // the date and the title (8.4:1 on the paper) kana: '#5a554e', // the transcription, a step lighter than the text (7:1) rule: '#c9c1b2', muted: '#6f6a62', // running heads, folios, the note paper: '#fcfaf4', }; 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: 'indigo (defaults)', value: { hex: palette.indigo, model: 'hex' } }, ]; // #endregion const SERIF = 'Source Serif 4'; // the rōmaji: ô û â ê in latin, ō ū in latin-ext const SANS = 'Source Sans 3'; // labels, running heads, folios, the note const MINCHO = 'Noto Serif JP'; // the kana: Noto Serif JP's Latin is Source Serif's const [BODY, LEAD] = [10.5, 15]; // pt // #region answer: a Japanese text in Latin letters is tagged ja-Latn // ja-Latn is Japanese written in Latin script (BCP 47). Its language is ja, so the kana // under each paragraph get the Japanese rules (kinsoku, 、。 spacing, the space after !) // and the built-in strings are Japanese; and Japanese has no hyphenation patterns, so the // rōmaji is never divided at a line end. English patterns would divide tatinobotta or // Surigarasu by English syllables, not by the morae a Japanese reader hears. const language = { locale: 'ja-Latn' }; // spread into the config const bodyText = { fontFamily: SERIF, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'), textAlign: 'justify', firstLineIndent: em(1.2), indentAfterHeading: false, optimalLineBreaking: true, maxWordSpacing: 1.8, // no hyphens: the spaces take the slack }; // The reading in kana, under its paragraph: smaller, a shade lighter, indented. const kana = { id: 'kana', fontFamily: MINCHO, fontSize: pt(8.6), lineHeight: pt(LEAD), color: col('kana'), textAlign: 'justify', indent: em(1.5), firstLineIndent: em(1), marginTop: pt(LEAD / 4), marginBottom: pt(LEAD * 0.75) }; // #endregion // #region title: the diary's own heading lines on an indigo field, like a cloth notebook const onField = { color: col('paper'), align: 'center', overflow: 'wrap' }; const title = { id: 'title', numbered: false, toc: false, span: 'page', runningChapter: false, breakBefore: { enabled: true, parity: 'any' }, // its page is an opener: no running heads advancedDesign: { enabled: true, minHeight: mm(166), slot: { elements: [ { kind: 'box', id: 'field', style: { backgroundColor: col('indigo') }, placement: { anchor: { to: 'bleed', edge: 'top-left' }, size: { width: 'fill', height: 'fill' } } }, { kind: 'text', id: 'lines', content: '{attr.lines}', ...onField, fontFamily: SERIF, fontWeight: 600, fontSize: pt(20), lineHeight: 1.9, letterSpacing: pt(5), placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(18) } } }, { kind: 'rule', id: 'rule', thickness: pt(0.5), color: col('paper'), placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(122) }, size: { width: mm(24) } } }, { kind: 'text', id: 'note', content: '{attr.note}', inlineMarks: true, ...onField, fontFamily: SANS, fontSize: pt(8.5), lineHeight: 1.5, placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(130) }, size: { width: mm(84) } } }, ] } }, }; // The entry: its day as a heading, the address under it from an attribute. const entry = { level: 2, fontFamily: SANS, fontWeight: 600, fontSize: pt(10), letterSpacing: pt(2.5), color: col('indigo'), marginTop: pt(LEAD), marginBottom: pt(LEAD), advancedDesign: { enabled: true, minHeight: pt(2 * LEAD), slot: { elements: [ { kind: 'text', id: 'day', content: '{titleText}', fontFamily: SANS, fontWeight: 600, fontSize: pt(10), letterSpacing: pt(2.5), color: col('indigo'), placement: { anchor: { to: 'container', edge: 'top-left' } } }, { kind: 'text', id: 'place', content: '{attr.place}', fontFamily: SANS, fontSize: pt(7.5), letterSpacing: pt(1), color: col('muted'), overflow: 'wrap', align: 'left', placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: pt(LEAD) }, size: { width: 'fill' } } }, ] } }, }; // #endregion // #region heads: the diary's name on the verso, the place on the recto, folios outside const head = (id, content, parity, edge) => ({ kind: 'text', id, content, parity, pages: 'body', fontFamily: SANS, fontSize: pt(7.5), letterSpacing: pt(1.5), color: col('muted'), placement: { anchor: { to: 'page', edge }, offset: { y: mm(13), // 'top' centres on the page x: mm(edge.endsWith('left') ? 16 : edge.endsWith('right') ? -16 : 0) } } }); const header = { elements: [ head('folio-even', '{pageNumber}', 'even', 'top-left'), head('book', 'ROMAZI NIKKI · 1909', 'even', 'top'), head('place', 'TOKYO · APRIL', 'odd', 'top'), head('folio-odd', '{pageNumber}', 'odd', 'top-right'), ] }; // #endregion const config = () => ({ ...language, // ja-Latn, written out (gotcha: ja-locale-tag) colorPalette, page: { sizePreset: 'custom', width: mm(148), height: mm(210), dpi: 150, // A5 backgroundColor: col('paper'), // A measure of about 70 characters: with no hyphens, a narrower one sets loose lines. margins: { top: mm(22), bottom: mm(22), left: mm(17), right: mm(17), mirror: true }, }, layout: { layoutType: 'single' }, bodyText, headings: { fontFamily: SANS, fontWeight: 600, color: col('indigo'), levels: [{ level: 1, breakBefore: { enabled: true, parity: 'any' } }, entry] }, headingStyles: [title], paragraphStyles: [kana, { id: 'note', fontFamily: SANS, fontSize: pt(7.8), lineHeight: pt(11.5), color: col('muted'), boldColor: col('ink'), boldFontWeight: 600, textAlign: 'left', firstLineIndent: em(0), marginTop: pt(2 * LEAD) }], header, footer: { elements: [] }, }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Esempio Markdown · 61 righe · content.en.md
title: "ROMAZI NIKKI" author: "Isikawa Takuboku" --- # ROMAZI NIKKI {style="title" lines="NIKKI.\nI.\nMEIDI 42 NEN.\n1909.\nAPRIL.\nTOKYO." note="Ishikawa Takuboku, *Rōmaji nikki* (the Rōmaji Diary), the first entry, 7 April 1909, with a transcription into kana and kanji under each paragraph"} ## 7TH, WEDNESDAY. {place="HONGO-KU MORIKAWA-TYO 1 BANTI, SINSAKA 359 GO, GAIHEI-KAN-BESSO NITE."} Hareta Sora ni susamajii Oto wo tatete, hagesii Nisi-kaze ga huki areta. Sangai no Mado to yû Mado wa Taema mo naku gata-gata naru, sono Sukima kara wa, haruka Sita kara tatinobotta Suna-hokori ga sara-sara to hukikomu: sono kuse Sora ni tirabatta siroi Kumo wa titto mo ugokanu. Gogo ni natte Kaze wa yô-yô otituita. :::paragraphs{style="kana"} 晴れた空にすさまじい音を立てて、激しい西風が吹き荒れた。三階の窓という窓は絶え間もなくがたがた鳴る、そのすき間からは、はるか下から立ちのぼった砂ぼこりがさらさらと吹き込む。そのくせ空に散らばった白い雲はちっとも動かぬ。午後になって風はようよう落ち着いた。 ::: Haru rasii Hikage ga Mado no Surigarasu wo atataka ni somete, Kaze sae nakuba Ase de mo nagare sô na Hi de atta. Itu mo kuru Kasihonya no Oyadi, Te-no-hira de Hana wo kosuriage nagara, “Hidoku huki masu nâ.” to itte haitte kita. “Desuga, Kyô-dyû nya Tôkyô-dyû no Sakura ga nokorazu saki masu ze. Kaze ga attatte, anata, kono Tenki de gozai masu mono.” :::paragraphs{style="kana"} 春らしい日影が窓のすりガラスを暖かに染めて、風さえなくば汗でも流れそうな日であった。いつも来る貸本屋のおやじ、手のひらで鼻をこすり上げながら、「ひどく吹きますなあ。」と言って入って来た。「ですが、今日じゅうにゃ東京じゅうの桜が残らず咲きますぜ。風があったって、あなた、この天気でございますもの。」 ::: “Tôtô Haru ni nattyatta nê!” to Yo wa itta. Muron kono Kangai wa Oyadi ni wakarikko wa nai. “Eh! eh!” to Oyadi wa kotaeta: “Haru wa anata, watasidomo ni wa Kinmotu de gozai masu ne. Kasihon wa, moh, kara Dame de gasu: Hon nanka yomu yorya mata asonde aruita hô ga yô-gasu kara, Muri mo nai-ndesu ga, yonde kudasaru kata mo sizen to kô nagaku bakari narimasunde ne.” :::paragraphs{style="kana"} 「とうとう春になっちゃったねえ!」と予は言った。むろんこの感慨はおやじにわかりっこはない。「ええ!ええ!」とおやじは答えた。「春はあなた、わたしどもには禁物でございますね。貸本は、もう、からだめでがす。本なんか読むよりゃまた遊んで歩いたほうがようがすから、無理もないんですが、読んでくださる方も自然とこう長くばかりなりますんでね。」 ::: Kinô Sya kara Zensyaku sita Kane no nokori, 5 yen Sihei ga iti-mai Saihu no naka ni aru; Gozen-tyû wa sore bakkari Ki ni natte, Siyô ga nakatta. Kono Kimoti wa, heizei Kane no aru Hito ga kyû ni motanaku natta toki to onaji yô na Kigakari ka mo sirenu: dotira mo okasii koto da, onaji yô ni okasii ni wa tigai nai ga, sono Kô-hukô ni wa taisita Tigai ga aru. :::paragraphs{style="kana"} 昨日社から前借りした金の残り、五円紙幣が一枚財布の中にある。午前中はそればっかり気になって、しようがなかった。この気持ちは、平生金のある人が急に持たなくなったときと同じような気がかりかもしれぬ。どちらもおかしいことだ、同じようにおかしいには違いないが、その幸不幸には大した違いがある。 ::: Siyô koto nasi ni, Rôma-ji no Hyô nado wo tukutte mita. Hyô no naka kara, toki-doki, Tugaru-no-Umi no kanata ni iru Haha ya Sai no koto ga ukande Yo no Kokoro wo kasumeta. “Haru ga kita, Si-gatu ni natta. Haru ! Haru ! Hana mo saku ! Tokyô e kite mô iti-nen da! ...........Ga, Yo wa mada Yo no Kazoku wo yobiyosete yasinau Junbi ga dekinu !” Tika-goro, Hi ni nankwai to naku, Yo no Kokoro no naka wo atira e yuki, kotira e yuki siteru, Mondai wa kore da ........... :::paragraphs{style="kana"} しようことなしに、ローマ字の表などを作ってみた。表の中から、ときどき、津軽の海の彼方にいる母や妻のことが浮かんで予の心をかすめた。「春が来た、四月になった。春!春!花も咲く!東京へ来てもう一年だ!……が、予はまだ予の家族を呼び寄せて養う準備ができぬ!」近ごろ、日に何回となく、予の心の中をあちらへ行き、こちらへ行きしてる問題はこれだ…… ::: Sonnara naze kono Nikki wo Rômaji de kaku koto ni sitaka ? Naze da ? Yo wa Sai wo aisiteru ; aisiteru kara koso kono Nikki wo yomase taku nai no da. ------ Sikasi kore wa Uso da ! Aisiteru no mo Jijitu, yomase taku nai no mo Jijitu da ga, kono Hutatu wa kanarazu simo Kwankei site inai. :::paragraphs{style="kana"} そんならなぜこの日記をローマ字で書くことにしたか?なぜだ?予は妻を愛してる。愛してるからこそこの日記を読ませたくないのだ。――しかしこれはうそだ!愛してるのも事実、読ませたくないのも事実だが、この二つは必ずしも関係していない。 ::: Sonnara Yo wa Jakusya ka ? Ina, Tumari kore wa Hûhu-kwankei to yû matigatta Seido ga aru tame ni okoru no da. Hûhu ! nan to yû Baka na Seido darô ! Sonnara dô sureba yoi ka ? :::paragraphs{style="kana"} そんなら予は弱者か?否、つまりこれは夫婦関係という間違った制度があるために起こるのだ。夫婦!なんというばかな制度だろう!そんならどうすればよいか? ::: Kanasii koto da ! :::paragraphs{style="kana"} 悲しいことだ! ::: :::paragraphs{style="note"} **A note on the text.** Takuboku kept this diary in Tokyo from 7 April to 16 June 1909, in Latin letters, so that his wife Setsuko could not read it. He spells it in the Nippon-shiki system of 1885, not in Hepburn: *si, ti, tu, hu* for shi, chi, tsu, fu, *dy* for the voiced *t*-row kana (*Kyô-dyû*), a circumflex on long vowels (*Tôkyô*) and, his own habit, a capital on most nouns. The text follows the Iwanami Bunko edition of 1977; the transcription under each paragraph was made for this recipe, in modern kana. Set in Source Serif 4, Source Sans 3 and Noto Serif JP (SIL OFL) · Diary: public domain · Transcription and note: CC BY 4.0 :::`; // content.<lang>.md: the same diary in both // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { 'Source Serif 4': ['400', '400i', '600'], 'Source Sans 3': ['400', '400i', '600'], 'Noto Serif JP': ['400'] }; const kanaText = (markdown.match(/:::paragraphs\{style="kana"\}[\s\S]*?:::/g) ?? []).join(''); // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); // and the latin-ext files, for the ō of the note await loadCjkFonts({ [MINCHO]: FONTS[MINCHO] }, kanaText); const doc = await withLoadedFonts(() => buildDocument({ markdown }, config()), { ...kitFonts(FONTS), text: markdown }); showPages(doc, { title: t({ en: 'The Rōmaji Diary', es: 'El diario en rōmaji' }) }); // #region macrons: the PDF gets each Latin face's latin-ext file too // Fontsource keeps ō ū ā in a latin-ext file. cjkPdfProvider serves Noto Serif JP from its // slices, and adds that file after the latin one for a Latin face whose text needs it. offerPdf(() => renderToPdf(doc, { fontProvider: cjkPdfProvider }), `${RECIPE}.pdf`); // #endregionKit · 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
#Scrivi il rōmaji in Hepburn
Un’edizione moderna può riscrivere il diario in Hepburn con i macron, Tōkyō per Tôkyô. Il PDF porta già la ō, quindi cambia solo il testo; aggiungi una nota che dica che la grafia è del curatore.
#Sillaba tra le more
Se una colonna stretta ha bisogno di trattini, dai al rōmaji dei trattini morbidi (U+00AD) tra le more, Su\u00adna-ho\u00adko\u00adri nel codice o un U+00AD digitato nel testo: un documento etichettato ja-Latn non sillaba mai da solo, ma va a capo su un trattino morbido quando una parola non ci sta.
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
Componi il giapponese in un carattere giapponese
Noto Serif SC e TC hanno i kana, ma disegnano i kanji in forme cinesi (直, 骨 e 角 sono diversi) e i kana con un disegno cinese. Componi il testo in Noto Serif JP o Shippori Mincho B1 e i titoli in Noto Sans JP, caricati con loadCjkFonts. I file giapponesi di Fontsource non contengono hentaigana né altri kana storici (U+1B000–1B16F): loadCjkFonts fallisce su di essi e il PDF stampa riquadri, quindi scrivi i kana moderni o includi negli asset della ricetta un carattere che li abbia. A Shippori Mincho mancano anche le vocali con macron ō e ū: componi il rōmaji in un carattere latino. Kana, kanji e rōmaji →
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 →
- Shippori Mincho non ha né ō né ū; Noto Serif JP e Source Serif 4 le hanno nei loro file
latin-ext. Controlla il carattere prima di sceglierlo per un testo in Hepburn. - Il cinese scritto in lettere latine è il pinyin, composto come ruby sopra i caratteri in Un primo libro di lettura con il pinyin sopra ogni carattere; il rōmaji di Takuboku è il testo stesso, quindi si compone come testo latino con la sua spaziatura, non come annotazione.
Crediti
- Ricetta
- Ignacio Ferro
- Testo
- Ishikawa Takuboku, ROMAZI NIKKI (the Rōmaji Diary), the entry of 7 April 1909 to “Kanasii koto da!”, in Latin letters as he wrote it, after the Iwanami Bunko edition (ed. Kuwabara Takeo, 1977) as transcribed on the blog 啄木の息 · Ishikawa Takuboku (1886–1912) · pubblico dominio
- The transcription into kana and kanji under each paragraph and the note on the text, written for this recipe · Postext Cookbook · CC BY 4.0
- Font
- Source Serif 4 (SIL OFL 1.1) · Source Sans 3 (SIL OFL 1.1) · Noto Serif JP (SIL OFL 1.1)
- Codice
- MIT, come Postext
Modifica questo testo ↗ (si apre in una nuova scheda)Cartella della ricetta su GitHub ↗ (si apre in una nuova scheda)


