In breve
Una breve antologia di un classico romanzo cinese con un indice dei nomi dei personaggi. Gli stessi segni danno due edizioni: una ordinata per suono, l’altra per numero di tratti del pennello.
Che cosa comporrai
Un’agile antologia del 紅樓夢 (Il sogno della camera rossa) che ristampa, dai capitoli 1–3, i passi in cui i personaggi compaiono per la prima volta, e si chiude con un indice dei suoi 47 nomi, più quattro rimandi da altri nomi. Esce in due edizioni dagli stessi segni. Quella della Cina continentale, in caratteri semplificati, ordina i nomi per iniziale pinyin, F, H, J…; quella di Taiwan, in caratteri tradizionali, per numero di tratti del primo carattere, 三畫, 四畫… Entrambe sono tascabili 大32开, 140 × 203 mm, 26 caratteri per riga in Noto Serif, con i titoli dei capitoli in un carattere Kai e gli occhielli, i folio e i gruppi dell’indice in Noto Sans. La scheda ingrandisce i primi gruppi dell’indice semplificato, dalla F alla L. Un indice dei nomi e un indice degli argomenti costruisce lo stesso tipo di indice per una monografia in inglese.
Questa ricetta risponde a
- Come raggruppo un indice di nomi cinesi per pinyin o per numero di tratti?
- Come porto i caratteri cinesi nel PDF senza riquadri vuoti?
La risposta breve
// Marks in the Markdown, one per passage; main is the page where the character enters:
// 字:index[士隐]{term="甄士隐" main} :index{term="贾化" see="贾雨村"}
// Printed at the end of each edition: # 人名索引 {style="index"}, then :::index
const index = (e) => ({
// 'auto' reads the same from the locale: pinyin for zh-Hans, strokes for zh-Hant.
groupBy: e.locale === 'zh-Hans' ? 'pinyin' : 'stroke', // A B C… or 三畫 四畫…
fontFamily: e.serif, fontSize: pt(9), lineHeight: pt(14),
separator: '\u3000', locatorSeparator: ',', // 贾宝玉 4,6: an ideographic space, then ,
see: { italic: false }, // 见 / 見 upright: Chinese has no italic (gotcha: cjk-no-italic)
groups: { fontFamily: e.sans, fontSize: pt(9.5), fontWeight: 700, color: col('cinnabar'),
marginTop: pt(6.25) }, // the three names of 九畫 stay whole at the foot of their column
});
const indexHeading = { id: 'index', numbered: false, breakBefore: { enabled: true, parity: 'any' },
layout: { layoutType: 'double', gutterWidth: pt(21) } }; // two columns, 2 characters apart
Ingredienti
- Funzionalità
- Indice analiticoFont cinesi, giapponesi e coreaniStili di titoloCapitoli non numeratiAperture progettateAttributi dei titoliNumeri cinesiGriglia dei caratteriDivisione in righe del cineseTestatine e folioMargini speculariBilanciamento delle colonneStili di paragrafoTavolozza di colori semanticaEsportazione in PDF
- Usa anche
- Colore della cartaTestatine secondo il ruolo della paginaFont incorporati nel PDFGeometria della sezioneA capo nei titoli
- Tipo
- Noto Serif SC, Noto Sans SC, Ma Shan Zheng, Noto Serif TC, Noto Sans TC, LXGW WenKai TC (SIL OFL 1.1)
- Risorse
- Nessuna: ogni immagine è disegnata nel codice
Preparazione
#1 · Segna ogni nome dove il lettore lo incontra
Il codice è la risposta breve qui sopra; il resto sta nel Markdown. Un segno registra il nome completo qualunque cosa stampi il testo: 字:index[士隐]{term="甄士隐" main} stampa 士隐 e registra una pagina sotto 甄士隐. main mette la pagina in grassetto, e la nota sotto il titolo dell’indice spiega che cosa significa qui il grassetto: la pagina in cui il personaggio entra in scena di persona. Jia Baoyu nasce nel capitolo 2 senza essere nominato (又生了一位公子), quindi il segno si inserisce senza nulla da stampare, :index{term="贾宝玉"}, e la sua voce dice 4, 6. Gli altri nomi e soprannomi hanno un proprio rimando, :index[凤辣子]{term="凤辣子" see="王熙凤"}, che Postext stampa alla maniera cinese, dopo un punto fermo e senza spazio: 凤辣子。见王熙凤 (segni di indice).
#2 · Lascia che sia il locale a scegliere l’ordine
// The mainland edition in Simplified characters and the Taiwan edition in Traditional ones.
// Each takes the Song (serif), Hei (sans) and Kai faces cut for its script. Fontsource has no
// Kai text face with mainland forms: LXGW WenKai TC draws 冷 the Taiwan way, so the mainland
// titles take Ma Shan Zheng, a brush Kai.
const EDITIONS = {
hans: { locale: 'zh-Hans', serif: 'Noto Serif SC', sans: 'Noto Sans SC', kai: 'Ma Shan Zheng' },
hant: { locale: 'zh-Hant', serif: 'Noto Serif TC', sans: 'Noto Sans TC', kai: 'LXGW WenKai TC' },
};
Le due edizioni condividono la configurazione, e locale è l’unico interruttore di cui l’indice ha bisogno: groupBy: 'auto' raggruppa un indice zh-Hans per pinyin e uno zh-Hant per tratti, e la risposta breve scrive per esteso i due valori perché tu li veda. Le voci si ordinano secondo il criterio da cui vengono i gruppi, quindi gli stessi nomi cambiano posto: 甄费 viene prima di 甄士隐 sotto la Z (fèi prima di shì), e dopo sotto 十四畫 (士 ha tre tratti, 費 dodici); 宁国公 e 荣国公 si separano tra N e R, mentre 寧國公 e 榮國公 restano insieme tra i nomi di quattordici tratti (indice analitico). Le letture e i numeri di tratti sono quelli dell’ordinamento cinese del browser; un cognome che legge male (曾, 单, 解) riceve una chiave sort in caratteri con la lettura giusta.
#3 · Carica ogni carattere con i segni che compone
const DIGITS = '0123456789';
const HEADS = '第回一二三四五六七八九十画畫ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // 第一回, the index heads
const all = (md, re) => [...md.matchAll(re)].map((m) => m[1]).join('\n');
for (const [key, md] of [['hans', markdown], ['hant', traditional]]) {
e = EDITIONS[key];
await loadCjkFonts({ [e.serif]: ['400'] }, md); // the text, the index, the running heads
await loadCjkFonts({ [e.serif]: ['700'] }, DIGITS); // the bold page numbers
await loadCjkFonts({ [e.sans]: ['700'] }, all(md, /kicker="([^"]*)"/g) + HEADS + DIGITS);
await loadCjkFonts({ [e.kai]: ['400'] }, all(md, /^# ([^{\n]*)/gm)); // the titles alone
}
Fontsource divide ogni peso cinese in circa cento file, e loadCjkFonts scarica quelli toccati dal suo testo. Il carattere Song riceve tutto il libro, quindi le testatine non gli costano nulla; il suo grassetto, le cifre delle pagine principali; il carattere Hei, il testo degli attributi kicker, i gruppi dell’indice e le cifre dei folio; il carattere Kai, i tre distici e 人名索引. Il PDF segue la stessa strada: cjkPdfProvider incorpora quei file, ognuno ridotto ai caratteri usati dalle sue pagine, e nessun carattere esce come un quadratino vuoto. Il carattere Kai cambia con l’edizione. LXGW WenKai TC, l’unico Kai da testo su Fontsource, disegna le forme ereditate, 冷 con il vecchio piede 卩 di 令, e centra virgole e punti fermi, quindi i titoli semplificati sono composti in Ma Shan Zheng, un Kai a pennello con le forme continentali (font cinesi, giapponesi e coreani).
#4 · Metti la nota nell’apertura dell’indice
const opener = (e, sink, kicker, note = []) => ({
enabled: true,
minHeight: pt(LEAD * sink), // the text starts on the same line after every opener
slot: { elements: [
{ kind: 'text', id: 'kicker', content: kicker, fontFamily: e.sans, fontSize: pt(9),
fontWeight: 700, letterSpacing: pt(2.7), color: col('cinnabar'), align: 'center',
placement: { ...at('container', 'top', 0, 8), size: { width: 'fill', height: 'auto' } } },
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: e.kai, fontSize: pt(16),
lineHeight: 1.45, color: col('ink'), align: 'center',
overflow: 'wrap', // design text ends in an ellipsis by default
placement: { ...at('#kicker', 'below', 0, 3), size: { width: 'fill', height: 'auto' } } },
{ kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.5), color: col('rule'),
placement: { ...at('#title', 'below', (MEASURE - 12) / 2, 4), size: { width: mm(12) } } },
...note,
] },
});
// The note (note="…" on the heading) sits above the columns: no paragraph for balancing to loosen.
const indexNote = (e) => [{ kind: 'text', id: 'note', content: '{attr.note}',
fontFamily: e.serif, fontSize: pt(9), lineHeight: 1.6, color: col('muted'), align: 'center',
overflow: 'wrap', placement: { ...at('#rule', 'below', -(MEASURE - 12) / 2, 4),
size: { width: 'fill', height: 'auto' } } }];
La nota è un attributo del titolo, # 人名索引 {style="index" kicker="按汉语拼音排列" note="…"}, composta su tutta la giustezza sopra le due colonne. Come paragrafo dentro le colonne era una leva per il bilanciamento delle colonne: per pareggiare l’ultima pagina dell’edizione tradizionale, l’impaginazione la allargava a dodici caratteri per riga in una colonna di quattordici. La nota è una sola frase, 本索引收录前三回选文中的人物,粗体页码为人物出场之页;别名与原名另立参见条目。, mandata a capo dal progetto grafico: il testo grafico in un libro cinese prende le regole di divisione in righe e le larghezze dei segni del testo corrente, quindi ,; sono a mezza larghezza nell’edizione semplificata e a larghezza piena in quella tradizionale, come nel testo, e nessuna riga della nota comincia con un segno di punteggiatura. Le aperture dei capitoli usano lo stesso progetto grafico, con 第{numberHan}回 come occhiello, che stampa 第一回 nella scrittura del documento.
#5 · Mostra le due pagine d’indice affiancate
const indexPage = (doc) => doc.pages[doc.blocks.find((b) => b.headingStyleId === 'index')
?.pageIndex ?? doc.pages.length - 1];
const labels = { hans: t({ en: 'Simplified · by pinyin', es: 'Simplificado · por pinyin' }),
hant: t({ en: 'Traditional · by strokes', es: 'Tradicional · por trazos' }) };
document.getElementById('pages').insertAdjacentHTML('beforebegin', `<section id="indexes" style="
display:flex;flex-wrap:wrap;justify-content:center;gap:24px;padding:28px 16px;background:#e9e4da">
${Object.keys(docs).map((key) => `<figure style="margin:0;width:min(340px,44vw)">
<canvas id="index-${key}" role="img" style="display:block;width:100%;background:#fff;
box-shadow:0 18px 36px -18px rgb(0 0 0 / .45)"></canvas><figcaption style="margin-top:10px;
font:600 11px/1 system-ui,sans-serif;letter-spacing:.14em;text-transform:uppercase;color:#5b534a">
${labels[key]}</figcaption></figure>`).join('')}</section>`);
for (const [key, doc] of Object.entries(docs)) {
const canvas = document.getElementById(`index-${key}`);
const page = indexPage(doc);
canvas.setAttribute('aria-label', `${labels[key]}, ${page.pageLabel}`);
renderPageToCanvas(page, doc, canvas, { scale: (2 * canvas.clientWidth) / page.width });
}
Il pen costruisce entrambe le edizioni e mostra sul tavolo quella semplificata. I pulsanti 简体版 e 繁體版 cambiano il tavolo, e ogni cambio offre il PDF dell’edizione che vi si trova, chinese-name-index-hans.pdf o chinese-name-index-hant.pdf. La cattura registra entrambe le build (capture.doc: ['first', 'last']): le prime otto pagine qui sopra sono dell’edizione semplificata, e le quattro che seguono vengono da quella tradizionale, la sua prima pagina e il suo indice per tratti. La striscia sopra il tavolo contiene la prima pagina d’indice di ciascuna edizione. Il PDF offerto con la ricetta è quello dell’edizione semplificata.
La ricetta completa
// ═══ Postext Cookbook · Nº 079 · An index of Chinese names, by pinyin and by strokes ═══ // https://postext.dev/en/cookbook/chinese-name-index // Code: MIT · Text: 紅樓夢 (1792), zh.wikisource revision 9685985 (CC BY-SA 4.0) · Pictures: none // Fonts: Noto Serif/Sans SC/TC, Ma Shan Zheng, LXGW WenKai TC (OFL 1.1) · Needs postext ≥ 1.28.0 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 sample document ('en' | 'es') const RECIPE = 'chinese-name-index'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: ink on a warm paper, one cinnabar for the heads and the index letters const palette = { ink: '#1f1b18', // text cinnabar: '#a3301f', // the one accent: 回 numbers, index heads muted: '#6b635a', // running heads, folios, colophon rule: '#d6cdbf', // the short rule under each title paper: '#fbf8f1', }; // Each colour carries its hex and the palette entry it follows. 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' } })), // The engine's defaults link to main-color: point it at the accent, never the default blue. { id: 'main-color', name: 'accent (defaults)', value: { hex: palette.cinnabar, model: 'hex' } }, ]; // #endregion // #region editions: one book, two scripts; the locale decides the index order // The mainland edition in Simplified characters and the Taiwan edition in Traditional ones. // Each takes the Song (serif), Hei (sans) and Kai faces cut for its script. Fontsource has no // Kai text face with mainland forms: LXGW WenKai TC draws 冷 the Taiwan way, so the mainland // titles take Ma Shan Zheng, a brush Kai. const EDITIONS = { hans: { locale: 'zh-Hans', serif: 'Noto Serif SC', sans: 'Noto Sans SC', kai: 'Ma Shan Zheng' }, hant: { locale: 'zh-Hant', serif: 'Noto Serif TC', sans: 'Noto Sans TC', kai: 'LXGW WenKai TC' }, }; // #endregion const LEAD = 18; // body leading in pt: 1.7 × the 10.5 pt text (五号) const MEASURE = (26 * 10.5 * 25.4) / 72; // the grid's 26 characters of 10.5 pt, in mm const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } }); // #region answer: the same marks, grouped by pinyin in zh-Hans and by strokes in zh-Hant // Marks in the Markdown, one per passage; main is the page where the character enters: // 字:index[士隐]{term="甄士隐" main} :index{term="贾化" see="贾雨村"} // Printed at the end of each edition: # 人名索引 {style="index"}, then :::index const index = (e) => ({ // 'auto' reads the same from the locale: pinyin for zh-Hans, strokes for zh-Hant. groupBy: e.locale === 'zh-Hans' ? 'pinyin' : 'stroke', // A B C… or 三畫 四畫… fontFamily: e.serif, fontSize: pt(9), lineHeight: pt(14), separator: '\u3000', locatorSeparator: ',', // 贾宝玉 4,6: an ideographic space, then , see: { italic: false }, // 见 / 見 upright: Chinese has no italic (gotcha: cjk-no-italic) groups: { fontFamily: e.sans, fontSize: pt(9.5), fontWeight: 700, color: col('cinnabar'), marginTop: pt(6.25) }, // the three names of 九畫 stay whole at the foot of their column }); const indexHeading = { id: 'index', numbered: false, breakBefore: { enabled: true, parity: 'any' }, layout: { layoutType: 'double', gutterWidth: pt(21) } }; // two columns, 2 characters apart // #endregion // #region opener: 第一回 in Hei over the couplet in Kai; the index adds its note const opener = (e, sink, kicker, note = []) => ({ enabled: true, minHeight: pt(LEAD * sink), // the text starts on the same line after every opener slot: { elements: [ { kind: 'text', id: 'kicker', content: kicker, fontFamily: e.sans, fontSize: pt(9), fontWeight: 700, letterSpacing: pt(2.7), color: col('cinnabar'), align: 'center', placement: { ...at('container', 'top', 0, 8), size: { width: 'fill', height: 'auto' } } }, { kind: 'text', id: 'title', content: '{titleText}', fontFamily: e.kai, fontSize: pt(16), lineHeight: 1.45, color: col('ink'), align: 'center', overflow: 'wrap', // design text ends in an ellipsis by default placement: { ...at('#kicker', 'below', 0, 3), size: { width: 'fill', height: 'auto' } } }, { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.5), color: col('rule'), placement: { ...at('#title', 'below', (MEASURE - 12) / 2, 4), size: { width: mm(12) } } }, ...note, ] }, }); // The note (note="…" on the heading) sits above the columns: no paragraph for balancing to loosen. const indexNote = (e) => [{ kind: 'text', id: 'note', content: '{attr.note}', fontFamily: e.serif, fontSize: pt(9), lineHeight: 1.6, color: col('muted'), align: 'center', overflow: 'wrap', placement: { ...at('#rule', 'below', -(MEASURE - 12) / 2, 4), size: { width: 'fill', height: 'auto' } } }]; // #endregion // #region running-heads: book title on the verso, the chapter on the recto, folios outside // The heads in the Song face, which has every character of the book loaded; Hei for the folios. const head = (e, id, content, parity, edge, y, extra = {}) => ({ kind: 'text', id, content, parity, fontFamily: e.sans, fontSize: pt(7.5), fontWeight: 700, letterSpacing: pt(0.8), color: col('muted'), align: edge.endsWith('left') ? 'left' : 'right', placement: at('container', edge, 0, y), ...extra, }); const song = (e) => ({ pages: 'body', fontFamily: e.serif, fontWeight: 400, fontSize: pt(8) }); const header = (e) => ({ elements: [ head(e, 'verso-title', '{title}', 'even', 'top-left', 14, song(e)), head(e, 'recto-title', '{chapterTitle}', 'odd', 'top-right', 14, song(e)), ] }); const footer = (e) => ({ elements: [ // folios at the outer foot, openers included head(e, 'verso-folio', '{pageNumber}', 'even', 'bottom-left', -12), head(e, 'recto-folio', '{pageNumber}', 'odd', 'bottom-right', -12), ] }); // #endregion let e = EDITIONS.hans; // the edition config() lays out: the build loop below sets it const config = () => ({ locale: e.locale, // zh-Hans or zh-Hant, never LANG (gotcha: cjk-locale-tag) colorPalette, page: { sizePreset: 'custom', width: mm(140), height: mm(203), dpi: 150, // 大32开 backgroundColor: col('paper'), // Minimums: the character grid centres its type area in what they leave, 天头 over 地脚. margins: { top: mm(24), bottom: mm(20), left: mm(16), right: mm(20), mirror: true } }, layout: { layoutType: 'single' }, cjk: { grid: { enabled: true, charsPerLine: 26, linesPerPage: 24 } }, bodyText: { fontFamily: e.serif, fontSize: pt(10.5), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'), textAlign: 'justify', firstLineIndent: em(2), indentAfterHeading: true, // 2 characters avoidWidows: true, avoidOrphans: true }, // Off by default on a character grid. On, an index's last page is cut between whole entries. headings: { fontFamily: e.kai, fontWeight: 400, color: col('ink'), balancing: { enabled: true }, levels: [{ level: 1, fontSize: pt(16), marginBottom: pt(0), advancedDesign: opener(e, 6, '第{numberHan}回'), // 第一回, 第二回… in the edition's script breakBefore: { enabled: true, parity: 'any' } }] }, // each 回 on a new page headingStyles: [{ ...indexHeading, span: 'page', marginBottom: pt(0), advancedDesign: opener(e, 7, '{attr.kicker}', indexNote(e)) }], index: index(e), paragraphStyles: [{ id: 'colophon', fontFamily: e.serif, fontSize: pt(7), lineHeight: pt(10.5), color: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) }], header: header(e), footer: footer(e), }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Esempio Markdown · 63 righe · content.en.md
title: "红楼梦人物初见" subtitle: "程乙本前三回选读" --- # 甄士隐梦幻识通灵 \\ 贾雨村风尘怀闺秀 却说那:index[女娲氏]{term="女娲"}炼石补天之时,于大荒山无稽崖炼成高十二丈、见方二十四丈大的顽石三万六千五百零一块。那娲皇只用了三万六千五百块,单单剩下一块未用,弃在青埂峰下。谁知此石自经锻炼之后,灵性已通,自去自来,可大可小。因见众石俱得补天,独自己无才,不得入选,遂自怨自愧,日夜悲哀。 又不知过了几世几劫,因有个:index[空空道人]{term="空空道人" main}访道求仙,从这大荒山无稽崖青埂峰下经过,忽见一块大石,上面字迹分明,编述历历。空空道人乃从头一看,原来是无才补天,幻形入世,被那:index[茫茫大士]{term="茫茫大士" main}:index[渺渺真人]{term="渺渺真人" main}携入红尘,引登彼岸的一块顽石。 按那石头上书云:当日地陷东南,这东南有个姑苏城,城中阊门,最是红尘中一二等富贵风流之地。这阊门外有个十里街,街内有个仁清巷,巷内有个古庙,因地方狭窄,人皆呼作“葫芦庙”。庙旁住着一家乡宦,姓甄,名费:index{term="甄费" see="甄士隐"},字:index[士隐]{term="甄士隐" main},嫡妻:index[封氏]{term="封氏" main}。性情贤淑,深明礼义。家中虽不甚富贵,然本地也推他为望族了。因这甄士隐禀性恬淡,不以功名为念,每日只以观花种竹、酌酒吟诗为乐,倒是神仙一流人物。只是一件不足:年过半百,膝下无儿,只有一女,乳名:index[英莲]{term="英莲" main},年方三岁。 那僧道:“此事说来好笑。只因当年这个石头,娲皇未用,自己却也落得逍遥自在,各处去游玩。……一日,来到:index[警幻仙子]{term="警幻仙子" main}处,那仙子知他有些来历,因留他在赤霞宫中,名他为赤霞宫:index[神瑛侍者]{term="神瑛侍者" main}。他却常在西方灵河岸上行走,看见那灵河岸上三生石畔有棵:index[绛珠仙草]{term="绛珠仙草" main},十分娇娜可爱,遂日以甘露灌溉,‘这绛珠草’始得久延岁月。……” 这士隐正在痴想,忽见隔壁葫芦庙内寄居的一个穷儒——姓贾名化:index{term="贾化" see="贾雨村"},表字时飞,别号:index[雨村]{term="贾雨村" main}的——走来。这贾雨村原系湖州人氏,也是诗书仕宦之族。因他生于末世,父母祖宗根基已尽,人口衰丧,只剩得他一身一口,在家乡无益,因进京求取功名,再整基业。自前岁来此,又淹蹇住了,暂寄庙中安身,每日卖文作字为生,故士隐常与他交接。 真是闲处光阴易过,倏忽又是元宵佳节。士隐令家人:index[霍启]{term="霍启" main}抱了英莲去看社火花灯。半夜中,霍启因要小解,便将英莲放在一家门坎上坐着。待他小解完了来抱时,那有英莲的踪影?急的霍启直寻了半夜,至天明不见,那霍启也不敢回来见主人,便逃往他乡去了。 他岳丈名唤:index[封肃]{term="封肃" main},本贯大如州人氏,虽是务农,家中却还殷实。 # 贾夫人仙逝扬州城 \\ 冷子兴演说荣国府 却说:index[娇杏]{term="娇杏"}那丫头便是当年回顾:index[雨村]{term="贾雨村"}的。因偶然一看,便弄出这段奇缘,也是意想不到之事。谁知他命运两济:不承望自到雨村身边,只一年,便生一子;又半载,雨村嫡配忽染疾下世,雨村便将他扶作正室夫人。正是:“偶因一回顾,便为人上人。” 那日偶又游至维扬地方,闻得今年盐政点的是:index[林如海]{term="林如海" main}。 这林如海,姓林,名海,表字如海,乃是前科的探花,今已升兰台寺大夫。本贯姑苏人氏,今钦点为巡盐御史,到任未久。……只嫡妻:index[贾氏]{term="贾敏"}生得一女,乳名:index[黛玉]{term="林黛玉"},年方五岁,夫妻爱之如掌上明珠。 刚入肆门,只见座上吃酒之客,有一人起身大笑,接了出来,口内说:“奇遇,奇遇!”雨村忙看时,此人是都中古董行中贸易,姓:index[冷号子兴]{term="冷子兴" main}的,旧日在都相识。 子兴叹道:“正说的是这两门呢!待我告诉你:当日:index[宁国公]{term="宁国公"}与:index[荣国公]{term="荣国公"}是一母同胞弟兄两个。宁公居长,生了两个儿子。宁公死后,长子:index[贾代化]{term="贾代化"}袭了官,也养了两个儿子。长子名贾敷,八九岁上死了。只剩了一个次子:index[贾敬]{term="贾敬"},袭了官,如今一味好道,只爱烧丹炼汞,别事一概不管。幸而早年留下一个儿子,名唤:index[贾珍]{term="贾珍"},因他父亲一心想作神仙,把官倒让他袭了。他父亲又不肯住在家里,只在都中城外和那些道士们胡羼。这位珍爷也生了一个儿子,今年才十六岁,名叫:index[贾蓉]{term="贾蓉"}。如今敬老爷不管事了。这珍爷那里干正事?只一味高乐不了,把那宁国府竟翻过来了,也没有敢来管他的人。……自荣公死后,长子:index[贾代善]{term="贾代善"}袭了官,娶的是金陵世勋史侯家的小姐:index{term="贾母"}为妻,生了两个儿子:长名:index[贾赦]{term="贾赦"},次名:index[贾政]{term="贾政"}。如今代善早已去世,太夫人尚在。……这政老爷的夫人:index[王氏]{term="王夫人"},头胎生的公子名叫:index[贾珠]{term="贾珠"},十四岁进学,后来娶了妻,生了子,不到二十岁,一病就死了。第二胎生了一位小姐,生在大年初一,就奇了。不想隔了十几年又生了一位公子:index{term="贾宝玉"},说来更奇:一落胞胎,嘴里便衔下一块五彩晶莹的玉来,还有许多字迹。你道是新闻不是?” 子兴道:“便是贾府中现在三个也不错。政老爷的长女名:index[元春]{term="贾元春"},因贤孝才德选入宫作女史去了。二小姐乃是赦老爷姨娘所出,名:index[迎春]{term="贾迎春"};三小姐,政老爷庶出,名:index[探春]{term="贾探春"};四小姐乃宁府珍爷的胞妹,名:index[惜春]{term="贾惜春"}。因史老夫人极爱孙女,都跟在祖母这边一处读书,听得个个不错。” 若问那赦老爷,也有一子,名叫:index[贾琏]{term="贾琏"},今已二十多岁了,亲上做亲,娶的是政老爷夫人王氏内侄女:index{term="王熙凤"},今已娶了四五年。 # 托内兄如海荐西宾 \\ 接外孙贾母惜孤女 却说雨村忙回头看时,不是别人,乃是当日同僚一案参革的:index[张如圭]{term="张如圭" main}。 :index[黛玉]{term="林黛玉" main}方进房,只见两个人扶着一位鬓发如银的老母迎上来。黛玉知是:index[外祖母]{term="贾母" main},正欲下拜,早被外祖母抱住,搂入怀中,“心肝儿肉”叫着大哭起来。当下侍立之人无不落泪,黛玉也哭个不休。待众人慢慢劝解住了,那黛玉方拜见了外祖母,贾母方一一指与黛玉道:“这是你:index[大舅母]{term="邢夫人" main}。这是:index[二舅母]{term="王夫人" main}。这是你先前珠大哥的媳妇:index[珠大嫂子]{term="李纨" main}。”黛玉一一拜见了。 一语未完,只听后院中有笑语声,说:“我来迟了,没得迎接远客!”黛玉思忖道:“这些人个个皆敛声屏气如此,这来者是谁,这样放诞无礼?……”心下想时,只见一群媳妇丫鬟拥着一个丽人:index{term="王熙凤" main}从后房门进来。……一双丹凤三角眼,两弯柳叶吊梢眉。身量苗条,体格风骚。粉面含春威不露,丹唇未启笑先闻。 黛玉连忙起身接见。贾母笑道:“你不认得他。他是我们这里有名的一个‘泼辣货’,南京所谓‘辣子’你只叫他:index[凤辣子]{term="凤辣子" see="王熙凤"}就是了。”黛玉正不知以何称呼,众姊妹都忙告诉黛玉道:“这是琏二嫂子。”黛玉虽不曾识面,听见他母亲说过:“大舅贾赦之子贾琏娶的就是二舅母王氏的内侄女,自幼假充男儿教养,学名叫做:index[王熙凤]{term="王熙凤"}。”黛玉忙陪笑见礼,以“嫂”呼之。 一语未了,只听外面一阵脚步响,丫鬟进来报道宝玉来了:index{term="贾宝玉" main}。黛玉心想:“这个宝玉不知是怎样个惫懒人呢。……”及至进来一看,却是位青年公子。……面若中秋之月,色如春晓之花,鬓若刀裁,眉如墨画,鼻如悬胆,睛若秋波。虽怒时而似笑,即瞋视而有情。项上金螭缨络,又有一根五色丝绦,系着一块美玉。 黛玉只带了两个人来:一个是自己的奶娘:index[王嬷嬷]{term="王嬷嬷" main},一个是十岁的小丫头,名唤:index[雪雁]{term="雪雁" main}。贾母见雪雁甚小,一团孩气,王嬷嬷又极老,料黛玉皆不遂心,将自己身边一个二等小丫头,名唤:index[鹦哥]{term="鹦哥" main}的,与了黛玉。 当下王嬷嬷与鹦哥陪侍黛玉在碧纱橱内;宝玉乳母:index[李嬷嬷]{term="李嬷嬷" main}并大丫头名唤:index[袭人]{term="袭人" main}的陪侍在外面大床上。 原来这袭人亦是贾母之婢,本名蕊珠:index{term="蕊珠" see="袭人"}。贾母因溺爱宝玉,恐宝玉之婢不中使,素喜蕊珠心地纯良,遂与宝玉。宝玉因知他本姓花,又曾见旧人诗句有“花气袭人”之句,遂回明贾母,即把蕊珠更名袭人。 黛玉虽不知原委,探春等却晓得是议论金陵城中居住的薛家姨母之子——表兄:index[薛蟠]{term="薛蟠"},倚财仗势打死人命,现在应天府案下审理。如今舅舅:index[王子腾]{term="王子腾"}得了信,遣人来告诉这边,意欲唤取进京之意。 # 人名索引 {style="index" kicker="按汉语拼音排列" note="本索引收录前三回选文中的人物,粗体页码为人物出场之页;别名与原名另立参见条目。"} :::index :::paragraphs{style="colophon"} Dream of the Red Chamber, chapters 1–3, Simplified edition: the passages where the characters first appear, from the Cheng–Gao text of 1792 (zh.wikisource, revision 9685985, CC BY-SA 4.0), converted to Simplified characters with OpenCC, with —— where the source has --. Cuts inside a paragraph are marked ……. The index files the names by pinyin initial, the mainland order; a bold number is the page where a character enters. Set in Noto Serif SC, Noto Sans SC and Ma Shan Zheng (SIL OFL). :::`; // the Simplified edition, content.<lang>.md const traditional = String.raw`---Esempio Markdown · 63 righe · content.hant.en.md
title: "紅樓夢人物初見" subtitle: "程乙本前三回選讀" --- # 甄士隱夢幻識通靈 \\ 賈雨村風塵懷閨秀 卻說那:index[女媧氏]{term="女媧"}煉石補天之時,於大荒山無稽崖煉成高十二丈、見方二十四丈大的頑石三萬六千五百零一塊。那媧皇只用了三萬六千五百塊,單單剩下一塊未用,棄在青埂峰下。誰知此石自經鍛鍊之後,靈性已通,自去自來,可大可小。因見眾石俱得補天,獨自己無才,不得入選,遂自怨自愧,日夜悲哀。 又不知過了幾世幾劫,因有個:index[空空道人]{term="空空道人" main}訪道求仙,從這大荒山無稽崖青埂峰下經過,忽見一塊大石,上面字跡分明,編述歷歷。空空道人乃從頭一看,原來是無才補天,幻形入世,被那:index[茫茫大士]{term="茫茫大士" main}:index[渺渺真人]{term="渺渺真人" main}攜入紅塵,引登彼岸的一塊頑石。 按那石頭上書云:當日地陷東南,這東南有個姑蘇城,城中閶門,最是紅塵中一二等富貴風流之地。這閶門外有個十里街,街內有個仁清巷,巷內有個古廟,因地方狹窄,人皆呼作「葫蘆廟」。廟旁住著一家鄉宦,姓甄,名費:index{term="甄費" see="甄士隱"},字:index[士隱]{term="甄士隱" main},嫡妻:index[封氏]{term="封氏" main}。性情賢淑,深明禮義。家中雖不甚富貴,然本地也推他為望族了。因這甄士隱稟性恬淡,不以功名為念,每日只以觀花種竹、酌酒吟詩為樂,倒是神仙一流人物。只是一件不足:年過半百,膝下無兒,只有一女,乳名:index[英蓮]{term="英蓮" main},年方三歲。 那僧道:「此事說來好笑。只因當年這個石頭,媧皇未用,自己卻也落得逍遙自在,各處去遊玩。……一日,來到:index[警幻仙子]{term="警幻仙子" main}處,那仙子知他有些來歷,因留他在赤霞宮中,名他為赤霞宮:index[神瑛侍者]{term="神瑛侍者" main}。他卻常在西方靈河岸上行走,看見那靈河岸上三生石畔有棵:index[絳珠仙草]{term="絳珠仙草" main},十分嬌娜可愛,遂日以甘露灌溉,『這絳珠草』始得久延歲月。……」 這士隱正在痴想,忽見隔壁葫蘆廟內寄居的一個窮儒——姓賈名化:index{term="賈化" see="賈雨村"},表字時飛,別號:index[雨村]{term="賈雨村" main}的——走來。這賈雨村原係湖州人氏,也是詩書仕宦之族。因他生於末世,父母祖宗根基已盡,人口衰喪,只剩得他一身一口,在家鄉無益,因進京求取功名,再整基業。自前歲來此,又淹蹇住了,暫寄廟中安身,每日賣文作字為生,故士隱常與他交接。 真是閒處光陰易過,倏忽又是元宵佳節。士隱令家人:index[霍啟]{term="霍啟" main}抱了英蓮去看社火花燈。半夜中,霍啟因要小解,便將英蓮放在一家門坎上坐著。待他小解完了來抱時,那有英蓮的蹤影?急的霍啟直尋了半夜,至天明不見,那霍啟也不敢回來見主人,便逃往他鄉去了。 他岳丈名喚:index[封肅]{term="封肅" main},本貫大如州人氏,雖是務農,家中卻還殷實。 # 賈夫人仙逝揚州城 \\ 冷子興演說榮國府 卻說:index[嬌杏]{term="嬌杏"}那丫頭便是當年回顧:index[雨村]{term="賈雨村"}的。因偶然一看,便弄出這段奇緣,也是意想不到之事。誰知他命運兩濟:不承望自到雨村身邊,只一年,便生一子;又半載,雨村嫡配忽染疾下世,雨村便將他扶作正室夫人。正是:「偶因一回顧,便為人上人。」 那日偶又遊至維揚地方,聞得今年鹽政點的是:index[林如海]{term="林如海" main}。 這林如海,姓林,名海,表字如海,乃是前科的探花,今已升蘭臺寺大夫。本貫姑蘇人氏,今欽點為巡鹽御史,到任未久。……只嫡妻:index[賈氏]{term="賈敏"}生得一女,乳名:index[黛玉]{term="林黛玉"},年方五歲,夫妻愛之如掌上明珠。 剛入肆門,只見座上吃酒之客,有一人起身大笑,接了出來,口內說:「奇遇,奇遇!」雨村忙看時,此人是都中古董行中貿易,姓:index[冷號子興]{term="冷子興" main}的,舊日在都相識。 子興嘆道:「正說的是這兩門呢!待我告訴你:當日:index[寧國公]{term="寧國公"}與:index[榮國公]{term="榮國公"}是一母同胞弟兄兩個。寧公居長,生了兩個兒子。寧公死後,長子:index[賈代化]{term="賈代化"}襲了官,也養了兩個兒子。長子名賈敷,八九歲上死了。只剩了一個次子:index[賈敬]{term="賈敬"},襲了官,如今一味好道,只愛燒丹鍊汞,別事一概不管。幸而早年留下一個兒子,名喚:index[賈珍]{term="賈珍"},因他父親一心想作神仙,把官倒讓他襲了。他父親又不肯住在家裡,只在都中城外和那些道士們胡羼。這位珍爺也生了一個兒子,今年才十六歲,名叫:index[賈蓉]{term="賈蓉"}。如今敬老爺不管事了。這珍爺那裡幹正事?只一味高樂不了,把那寧國府竟翻過來了,也沒有敢來管他的人。……自榮公死後,長子:index[賈代善]{term="賈代善"}襲了官,娶的是金陵世勳史侯家的小姐:index{term="賈母"}為妻,生了兩個兒子:長名:index[賈赦]{term="賈赦"},次名:index[賈政]{term="賈政"}。如今代善早已去世,太夫人尚在。……這政老爺的夫人:index[王氏]{term="王夫人"},頭胎生的公子名叫:index[賈珠]{term="賈珠"},十四歲進學,後來娶了妻,生了子,不到二十歲,一病就死了。第二胎生了一位小姐,生在大年初一,就奇了。不想隔了十幾年又生了一位公子:index{term="賈寶玉"},說來更奇:一落胞胎,嘴裡便銜下一塊五彩晶瑩的玉來,還有許多字跡。你道是新聞不是?」 子興道:「便是賈府中現在三個也不錯。政老爺的長女名:index[元春]{term="賈元春"},因賢孝才德選入宮作女史去了。二小姐乃是赦老爺姨娘所出,名:index[迎春]{term="賈迎春"};三小姐,政老爺庶出,名:index[探春]{term="賈探春"};四小姐乃寧府珍爺的胞妹,名:index[惜春]{term="賈惜春"}。因史老夫人極愛孫女,都跟在祖母這邊一處讀書,聽得個個不錯。」 若問那赦老爺,也有一子,名叫:index[賈璉]{term="賈璉"},今已二十多歲了,親上做親,娶的是政老爺夫人王氏內侄女:index{term="王熙鳳"},今已娶了四五年。 # 託內兄如海薦西賓 \\ 接外孫賈母惜孤女 卻說雨村忙回頭看時,不是別人,乃是當日同僚一案參革的:index[張如圭]{term="張如圭" main}。 :index[黛玉]{term="林黛玉" main}方進房,只見兩個人扶著一位鬢髮如銀的老母迎上來。黛玉知是:index[外祖母]{term="賈母" main},正欲下拜,早被外祖母抱住,摟入懷中,「心肝兒肉」叫著大哭起來。當下侍立之人無不落淚,黛玉也哭個不休。待眾人慢慢勸解住了,那黛玉方拜見了外祖母,賈母方一一指與黛玉道:「這是你:index[大舅母]{term="邢夫人" main}。這是:index[二舅母]{term="王夫人" main}。這是你先前珠大哥的媳婦:index[珠大嫂子]{term="李紈" main}。」黛玉一一拜見了。 一語未完,只聽後院中有笑語聲,說:「我來遲了,沒得迎接遠客!」黛玉思忖道:「這些人個個皆斂聲屏氣如此,這來者是誰,這樣放誕無禮?……」心下想時,只見一群媳婦丫鬟擁著一個麗人:index{term="王熙鳳" main}從後房門進來。……一雙丹鳳三角眼,兩彎柳葉吊梢眉。身量苗條,體格風騷。粉面含春威不露,丹脣未啟笑先聞。 黛玉連忙起身接見。賈母笑道:「你不認得他。他是我們這裡有名的一個『潑辣貨』,南京所謂『辣子』你只叫他:index[鳳辣子]{term="鳳辣子" see="王熙鳳"}就是了。」黛玉正不知以何稱呼,眾姊妹都忙告訴黛玉道:「這是璉二嫂子。」黛玉雖不曾識面,聽見他母親說過:「大舅賈赦之子賈璉娶的就是二舅母王氏的內侄女,自幼假充男兒教養,學名叫做:index[王熙鳳]{term="王熙鳳"}。」黛玉忙陪笑見禮,以「嫂」呼之。 一語未了,只聽外面一陣腳步響,丫鬟進來報道寶玉來了:index{term="賈寶玉" main}。黛玉心想:「這個寶玉不知是怎樣個憊懶人呢。……」及至進來一看,卻是位青年公子。……面若中秋之月,色如春曉之花,鬢若刀裁,眉如墨畫,鼻如懸膽,睛若秋波。雖怒時而似笑,即瞋視而有情。項上金螭纓絡,又有一根五色絲絛,繫著一塊美玉。 黛玉只帶了兩個人來:一個是自己的奶孃:index[王嬤嬤]{term="王嬤嬤" main},一個是十歲的小丫頭,名喚:index[雪雁]{term="雪雁" main}。賈母見雪雁甚小,一團孩氣,王嬤嬤又極老,料黛玉皆不遂心,將自己身邊一個二等小丫頭,名喚:index[鸚哥]{term="鸚哥" main}的,與了黛玉。 當下王嬤嬤與鸚哥陪侍黛玉在碧紗櫥內;寶玉乳母:index[李嬤嬤]{term="李嬤嬤" main}並大丫頭名喚:index[襲人]{term="襲人" main}的陪侍在外面大床上。 原來這襲人亦是賈母之婢,本名蕊珠:index{term="蕊珠" see="襲人"}。賈母因溺愛寶玉,恐寶玉之婢不中使,素喜蕊珠心地純良,遂與寶玉。寶玉因知他本姓花,又曾見舊人詩句有「花氣襲人」之句,遂回明賈母,即把蕊珠更名襲人。 黛玉雖不知原委,探春等卻曉得是議論金陵城中居住的薛家姨母之子——表兄:index[薛蟠]{term="薛蟠"},倚財仗勢打死人命,現在應天府案下審理。如今舅舅:index[王子騰]{term="王子騰"}得了信,遣人來告訴這邊,意欲喚取進京之意。 # 人名索引 {style="index" kicker="依筆畫排列" note="本索引收錄前三回選文中的人物,粗體頁碼為人物出場之頁;別名與原名另立參見條目。"} :::index :::paragraphs{style="colophon"} Dream of the Red Chamber, chapters 1–3, Traditional edition: the passages where the characters first appear, from the Cheng–Gao text of 1792 (zh.wikisource, revision 9685985, CC BY-SA 4.0), with 原係 where the source has 原系 and —— where it has --. Cuts inside a paragraph are marked ……. The index files the names by stroke count, the Taiwan order. Set in Noto Serif TC, Noto Sans TC and LXGW WenKai TC (SIL OFL). :::`; // the Traditional edition, content.hant.<lang>.md // ─── 3 · Fonts ────────────────────────────────────────────────────────────── // Every face the design uses. The CJK families load by slices. const FONTS = { 'Noto Serif SC': ['400', '700'], 'Noto Sans SC': ['700'], 'Ma Shan Zheng': ['400'], 'Noto Serif TC': ['400', '700'], 'Noto Sans TC': ['700'], 'LXGW WenKai TC': ['400'] }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown + traditional); // #region voices: each face loads the files of the characters it sets (gotcha: cjk-fonts-slices) const DIGITS = '0123456789'; const HEADS = '第回一二三四五六七八九十画畫ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // 第一回, the index heads const all = (md, re) => [...md.matchAll(re)].map((m) => m[1]).join('\n'); for (const [key, md] of [['hans', markdown], ['hant', traditional]]) { e = EDITIONS[key]; await loadCjkFonts({ [e.serif]: ['400'] }, md); // the text, the index, the running heads await loadCjkFonts({ [e.serif]: ['700'] }, DIGITS); // the bold page numbers await loadCjkFonts({ [e.sans]: ['700'] }, all(md, /kicker="([^"]*)"/g) + HEADS + DIGITS); await loadCjkFonts({ [e.kai]: ['400'] }, all(md, /^# ([^{\n]*)/gm)); // the titles alone } // #endregion const docs = {}; for (const [key, md] of [['hans', markdown], ['hant', traditional]]) { e = EDITIONS[key]; docs[key] = await withLoadedFonts(() => buildDocument({ markdown: md }, config()), { ...kitFonts(FONTS), text: md }); } const title = t({ en: 'A Chinese name index', es: 'Un índice de nombres chinos' }); // The desk shows either edition, and the PDF is built from the one on it. const switches = ['hans', 'hant'].map((key) => Object.assign(document.createElement('button'), { type: 'button', value: key, textContent: key === 'hans' ? '简体版' : '繁體版' })); const show = (key) => { showPages(docs[key], { title }); for (const b of switches) b.ariaPressed = String(b.value === key); // the edition on the desk document.querySelectorAll('#pt-actions a, [data-postext-pdf]').forEach((old) => old.remove()); offerPdf(() => renderToPdf(docs[key], { fontProvider: cjkPdfProvider }), `${RECIPE}-${key}.pdf`); }; for (const b of switches) b.addEventListener('click', () => show(b.value)); show('hans'); document.getElementById('pt-actions').prepend(...switches); document.head.insertAdjacentHTML('beforeend', '<style>#pt-actions [aria-pressed=true] { text-decoration: underline }</style>'); // #region side-by-side: the first index page of each edition, above the desk const indexPage = (doc) => doc.pages[doc.blocks.find((b) => b.headingStyleId === 'index') ?.pageIndex ?? doc.pages.length - 1]; const labels = { hans: t({ en: 'Simplified · by pinyin', es: 'Simplificado · por pinyin' }), hant: t({ en: 'Traditional · by strokes', es: 'Tradicional · por trazos' }) }; document.getElementById('pages').insertAdjacentHTML('beforebegin', `<section id="indexes" style=" display:flex;flex-wrap:wrap;justify-content:center;gap:24px;padding:28px 16px;background:#e9e4da"> ${Object.keys(docs).map((key) => `<figure style="margin:0;width:min(340px,44vw)"> <canvas id="index-${key}" role="img" style="display:block;width:100%;background:#fff; box-shadow:0 18px 36px -18px rgb(0 0 0 / .45)"></canvas><figcaption style="margin-top:10px; font:600 11px/1 system-ui,sans-serif;letter-spacing:.14em;text-transform:uppercase;color:#5b534a"> ${labels[key]}</figcaption></figure>`).join('')}</section>`); for (const [key, doc] of Object.entries(docs)) { const canvas = document.getElementById(`index-${key}`); const page = indexPage(doc); canvas.setAttribute('aria-label', `${labels[key]}, ${page.pageLabel}`); renderPageToCanvas(page, doc, canvas, { scale: (2 * canvas.clientWidth) / page.width }); } // #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
#Raggruppa per pinyin un indice tradizionale
Un’edizione tradizionale per lettori che cercano i nomi in base alla lettura può ordinarli per pinyin. Imposta groupBy: 'pinyin' in entrambe le edizioni: i gruppi vanno dalla A alla Z e le voci conservano le forme tradizionali. L’occhiello dice come è ordinato l’indice, quindi cambialo anche nel Markdown tradizionale, in kicker="依漢語拼音排列".
- groupBy: e.locale === 'zh-Hans' ? 'pinyin' : 'stroke', // A B C… or 三畫 四畫…
+ groupBy: 'pinyin', // A B C… in both scripts#Stampa i nomi senza intestazioni di gruppo
Un breve elenco di nomi si legge bene con una riga bianca tra i gruppi e nessuna intestazione sopra. Conserva il raggruppamento e disattiva le intestazioni. groupBy: 'none' non basterebbe: separa solo simboli, numeri e parole, e qui ogni nome comincia con un carattere Han, quindi i 47 scorrerebbero in un unico blocco.
- groups: { fontFamily: e.sans, fontSize: pt(9.5), fontWeight: 700, color: col('cinnabar'),
- marginTop: pt(6.25) }, // the three names of 九畫 stay whole at the foot of their column
+ groups: { enabled: false, marginTop: pt(14) }, // no heads; a blank line between groupsErrori frequenti
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
Il cinese non ha corsivo
Le famiglie CJK non includono corsivi, e la tipografia cinese segna l’enfasi con punti accanto ai caratteri, non con un’inclinazione. In un documento con tag cinese, *…* mette i punti d’enfasi sui caratteri cinesi che contiene e riserva il corsivo alle parole latine (cjk.emphasis: 'dots', il valore predefinito per il cinese); :dots[…] li mette ovunque. Con cjk.emphasis: 'italic', o in un documento con tag en o es, *…* fa inclinare al canvas i glifi in tondo, un obliquo sintetico che la tipografia cinese non usa mai (il PDF ripiega sulla variante in tondo): mantieni il tag cinese, oppure componi l’enfasi in grassetto o in un carattere Kai (LXGW WenKai) tramite uno stile di paragrafo o di chip. Font cinesi, giapponesi e coreani →
Errore frequente
Assegna al documento il tag zh-Hans o zh-Hant, non LANG
Le edizioni di una ricetta sono en ed es, ma un esempio cinese è cinese in entrambe: `locale: LANG` gli darebbe il tag inglese o spagnolo, sillaberebbe le sue parole latine, etichetterebbe le figure Figure o Figura e darebbe al PDF la lingua sbagliata. Da postext 1.30 il tag è anche la lingua usata per lo shaping del testo (misura, disegno e incorporamento), quindi con un tag inglese o spagnolo un carattere cinese compone in altre forme le cifre e la punteggiatura che condivide con il testo latino, e le righe vanno a capo altrove. Scrivi tu il tag: 'zh-Hans' (convenzioni della Cina continentale: divisione in righe GB, punteggiatura Kaiming) o 'zh-Hant' (Taiwan: punteggiatura centrata a tutta larghezza); 'zh-HK' per Hong Kong. Un 'zh' da solo vale come semplificato, Cina continentale. Un esempio giapponese prende 'ja' (errore frequente ja-locale-tag). Divisione in righe del cinese →
Errore frequente
Componi la punteggiatura di ogni regione in un carattere di quella regione
Le larghezze della punteggiatura spostano il bianco accanto a ogni segno dal lato in cui lo mette la regione del documento, non da quello in cui lo disegna il font: con zh-Hans una virgola Kaiming conserva la metà sinistra del suo quadratino, dove un carattere semplificato disegna il glifo, e cede la metà destra. Un carattere tradizionale centra ,。 nel quadratino, quindi la metà che se ne va contiene parte del glifo e la virgola si schiaccia sul carattere successivo. LXGW WenKai TC, l’unico carattere Kai da testo su Fontsource, fa questo nel testo semplificato. Componi il testo, le note e le citazioni della Cina continentale in Noto Serif SC o Noto Sans SC, tieni i caratteri TC per zh-Hant e usa un carattere a pennello semplificato (Ma Shan Zheng) solo per le righe di titolazione, dove il testo grafico non applica le larghezze della punteggiatura. Disegna anche forme ereditate che né lo standard continentale né quello di Taiwan stampano, 為 con la parte alta 爫 di 爲 e 令 (in 冷, 領) con un piede 卩: controlla i caratteri che una pagina compone con esso. Larghezze della punteggiatura cinese →
- La configurazione
indexappartiene al documento, quindi un documento stampa un solo ordine. Un libro che vuole sia un indice per pinyin sia un elenco di ricerca per tratti fa due build, come questo pen. - Su una griglia di caratteri il bilanciamento delle colonne è disattivato per impostazione predefinita, e l’ultima pagina di ciascun indice riempirebbe la prima colonna fino al piede: il colophon dell’edizione semplificata comincerebbe sotto la Z e finirebbe in testa alla seconda colonna.
headings.balancing: { enabled: true }accorcia la prima colonna di quella pagina, così i nomi proseguono nella seconda e il colophon resta intero sotto di loro (bilanciamento delle colonne). Il taglio cade tra le voci e non lascia mai un solo nome di un gruppo da solo al piede o in testa a una colonna (index.groups.splitMinEntries, 2 per impostazione predefinita), quindi le due colonne finiscono alla pari solo dove le lunghezze lo permettono. Qui ogni colophon contiene una frase sull’ordine del suo indice, e le colonne di entrambe le pagine finali terminano entro un terzo di riga l’una dall’altra.
Crediti
- Ricetta
- Ignacio Ferro
- Testo
- Dream of the Red Chamber (紅樓夢), chapters 1–3, passages where the characters first appear, in the Cheng–Gao edition of 1792 (程乙本); Wikisource transcription, revision 9685985. The Traditional edition reads the source's 原系 as 原係; both set its -- as ——; the Simplified edition converted with OpenCC · Cao Xueqin, Cheng Weiyuan and Gao E; transcription by Wikisource editors · CC BY-SA 4.0
- Font
- Noto Serif SC (SIL OFL 1.1) · Noto Sans SC (SIL OFL 1.1) · Ma Shan Zheng (SIL OFL 1.1) · Noto Serif TC (SIL OFL 1.1) · Noto Sans TC (SIL OFL 1.1) · LXGW WenKai TC (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)


