Em poucas palavras
Um capítulo de livro chinês composto em colunas de cima para baixo. Cada fonte é citada numa notinha de duas linhas dentro da coluna, e as obras citadas vêm listadas no fim.
O que você vai compor
O fim de um capítulo de um livro inventado sobre a imprensa chinesa, 版刻叢談 (“Conversas sobre a impressão xilográfica”), composto em caracteres tradicionais, de cima para baixo, em colunas lidas a partir da direita, numa página 大32開 de 140 × 203 mm encadernada pela direita. O capítulo trata da diagramação das edições Song e cita cinco obras reais. Cada citação é composta como os livros chineses compõem o comentário há séculos: como um 夾注, uma nota em duas fileiras com metade do corpo do texto, dentro da coluna onde está a citação. A nota que não cabe no que resta de uma coluna continua na cabeça da seguinte, e uma delas atravessa a virada de página. As notas seguem a forma de nota da GB/T 7714—2015, a norma de referências da China continental, e o capítulo termina com uma lista numerada das obras citadas, com as inglesas giradas de lado. O mesmo capítulo com as notas no pé da página seria composto como na Nº 071.
Esta receita responde a
- Como cito segundo a norma chinesa GB/T 7714, também em texto vertical?
- Como coloco as citações em notas de rodapé, no estilo Chicago, com forma abreviada nas citações seguintes?
A resposta curta
// The GB/T 7714 note style turns each citation into a note. notes: 'warichu' sets the note
// in the column where the citation stands, in two rows at half the text size, the right
// row read first; a note longer than what is left of the column goes on in the next one.
// The list at the chapter's end comes from :::bibliography{scope=chapter}, numbered in
// the order the works are cited. Latin words turn sideways in the notes and in the list.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
style: 'china-national-standard-gb-t-7714-2015-note',
notes: 'warichu', // 夾注; 'footnote' sets the same notes at the foot of the page
bibliography: { fontSize: em(0.9), entrySpacing: pt(0) },
};
const cjk = {
grid: { enabled: true, charsPerLine: 36, linesPerPage: 15 },
warichu: { fontSize: em(0.5), color: col('ink') }, // no brackets: the half size sets it off
};
Ingredientes
- Funcionalidades
- Citações em notasCitações em um estilo de citaçãoWarichu: notas em duas linhas dentro da linhaBibliografia a partir das referênciasTexto verticalLivros encadernados pela direitaGrade de caracteresNumeração chinesaLargura da pontuação chinesaFontes chinesas, japonesas e coreanasAberturas desenhadasTítulos numeradosCabeços e fóliosCabeços por tipo de páginaMargens espelhadasPaleta de cores semânticaExportação para PDF
- Também usa
- Quebra de linha em chinêsEquilíbrio de colunasAtributos de títuloCor do papelFontes incorporadas ao PDFElementos pré-textuais em romanos
- Tipografia
- Noto Serif SC, Noto Sans SC, LXGW WenKai TC (SIL OFL 1.1)
- Materiais
- Nenhum: todas as imagens são desenhadas em código
Preparo
#1 · Um estilo de notas, composto dentro da linha
O código é a resposta curta acima. O estilo de notas da GB/T 7714 (china-national-standard-gb-t-7714-2015-note) é um estilo de notas: cada citação vira uma nota que nomeia a obra por inteiro (Citações). Por padrão, um estilo de notas manda as notas para o pé da página. notes: 'warichu' mantém cada nota na coluna, composta como warichu: duas fileiras em cjk.warichu.fontSize, metade do corpo do texto, centralizadas na coluna, lendo-se primeiro a fileira da direita (Marcas, rubi e warichu). O Markdown é o de qualquer estilo, [@su1999] depois das palavras que a citação sustenta, e a nota ocupa o lugar da citação, antes do 。 que fecha a frase.

As fileiras são preenchidas por trechos. No pé da terceira coluna só cabem 宿白. 唐宋時期, que se dividem em duas fileiras de três e quatro casas; o resto da nota preenche a cabeça da coluna seguinte, de novo em duas fileiras. Na página 2, a nota de Twitchett começa no pé da última coluna, e o leitor vira a página para terminá-la.
#2 · As obras citadas, no fim do capítulo
:::bibliography{title="本章引用書目" scope=chapter} põe a lista onde ela está no texto e inclui só as obras que este capítulo cita (A bibliografia). As referências vêm de um bloco BibTeX. Cada entrada traz um langid, chinese ou english, que o estilo lê como a língua da obra, e os nomes escritos em caracteres ficam inteiros. A lista é numerada pela ordem da primeira citação, e as colunas de continuação de cada entrada começam abaixo do seu rótulo.
#3 · Caracteres tradicionais com as regras do continente
locale: 'zh-Hant-CN' pede caracteres tradicionais com as regras da China continental, como as editoras de Pequim e Xangai compõem os estudos clássicos: a vírgula e o ponto ficam no canto superior direito da sua casa, e um livro vertical é encadernado pela direita. As citações seguem a etiqueta: a escrita tradicional escolhe o locale CSL zh-TW, então as palavras que o estilo acrescenta também saem em caracteres tradicionais. A Noto Serif SC desenha os caracteres tradicionais nas formas do continente, que é o que esta edição pede; a LXGW WenKai TC, uma fonte de Taiwan, compõe só o título do capítulo, que não tem pontuação (Números em texto vertical explica o que acontece com os algarismos).
Nas notas e na lista, um título latino e um ano de quatro algarismos giram de lado e descem pela coluna. Um número curto ao lado de texto chinês, como o 1 de um rótulo, fica em pé na sua casa.
#4 · A abertura entre fios
// In the flow frame x runs down the column and y across the page, right to left, so a
// 'horizontal' rule is a vertical line on the sheet: three of them rule the two columns.
const [TITLE, PITCH] = [15, 1.6 * LEAD]; // pt: the title's size and its column pitch
const rule = (n) => ({ kind: 'rule', id: `rule-${n}`, direction: 'horizontal',
thickness: pt(n === 1 ? 0.4 : 0.8), color: col('vermilion'),
placement: { anchor: { to: 'container', edge: 'top-left' },
offset: { y: pt(LEAD + n * PITCH) }, size: { width: 'fill' } } });
// A line of PITCH in column n (0, 1), its characters centred between two rules.
const title = (id, content, n, family, size, weight) => ({ kind: 'text', id, content,
fontFamily: family, fontWeight: weight, fontSize: pt(size), lineHeight: PITCH / size,
color: col('ink'), letterSpacing: pt(size * 0.25), align: 'left',
placement: { anchor: { to: 'container', edge: 'top-left' },
offset: { x: pt(2 * BODY), y: pt(LEAD + n * PITCH) } } }); // two characters down
const opener = { enabled: true, minHeight: cols(5), slot: { elements: [
rule(0), rule(1), rule(2),
title('number', '{number}', 0, HEI, 12, 700), // 第三章 in the first column, in 黑
title('title', '{titleText}', 1, KAI, TITLE, 400), // the title in the second, in 楷
] } };
Numa página vertical, o desenho de um título é disposto no quadro do fluxo: x desce pela coluna e y atravessa a página a partir da borda direita, então um fio 'horizontal' é uma linha vertical na folha. Três fios emolduram duas colunas a 30,4 pt uma da outra. 第三章 fica na primeira, em Hei negrito, e o título na segunda, em Kai (Span e design avançado). O número do capítulo vem de startAt=3 no título e do modelo 第{1:一}章, e os fólios de pageNumbering: trad-chinese-informal a partir de 45. Os cabeços e os fólios descem pela margem externa, como na Nº 075 (Elementos de texto vertical).
const palette = {
ink: '#22201c', vermilion: '#a8352a', muted: '#6d665d', paper: '#fbf8f1',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.vermilion })
.map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
A receita completa
// ═══ Postext Cookbook · Nº 091 · Vertical Chinese with citations in inline notes ══════ // https://postext.dev/en/cookbook/vertical-book-with-jiazhu-citations // Code: MIT · Text: original (CC BY 4.0) · Pictures: none // Fonts: Noto Serif SC, Noto Sans SC, LXGW WenKai TC (SIL OFL 1.1) · Needs postext ≥ 1.12.0 import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerCitationEngine, loadVerticalAlternates, } from 'https://esm.sh/postext'; import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf'; import { createCiteprocEngine, STYLES, LOCALES } from 'https://esm.sh/postext-citeproc'; const LANG = 'en'; // @lang: the language of the frame; the chapter is Chinese in both editions const RECIPE = 'vertical-book-with-jiazhu-citations'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: black ink, one vermilion for the opener's rules, the paper of the page const palette = { ink: '#22201c', vermilion: '#a8352a', muted: '#6d665d', paper: '#fbf8f1', }; const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id }); const colorPalette = Object.entries({ ...palette, 'main-color': palette.vermilion }) .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })); // #endregion const [SONG, HEI, KAI] = ['Noto Serif SC', 'Noto Sans SC', 'LXGW WenKai TC']; // 宋, 黑, 楷 const [BODY, LEAD] = [10.5, 19]; // pt: 五號 on a column pitch of 1.8 em const cols = (n) => pt(n * LEAD); // n columns across the page // #region answer: a note style set as 夾注, two half-size rows inside the column // The GB/T 7714 note style turns each citation into a note. notes: 'warichu' sets the note // in the column where the citation stands, in two rows at half the text size, the right // row read first; a note longer than what is left of the column goes on in the next one. // The list at the chapter's end comes from :::bibliography{scope=chapter}, numbered in // the order the works are cited. Latin words turn sideways in the notes and in the list. registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES })); const citations = { style: 'china-national-standard-gb-t-7714-2015-note', notes: 'warichu', // 夾注; 'footnote' sets the same notes at the foot of the page bibliography: { fontSize: em(0.9), entrySpacing: pt(0) }, }; const cjk = { grid: { enabled: true, charsPerLine: 36, linesPerPage: 15 }, warichu: { fontSize: em(0.5), color: col('ink') }, // no brackets: the half size sets it off }; // #endregion // #region opener: 第三章 and the title in two ruled columns, the text after a blank one // In the flow frame x runs down the column and y across the page, right to left, so a // 'horizontal' rule is a vertical line on the sheet: three of them rule the two columns. const [TITLE, PITCH] = [15, 1.6 * LEAD]; // pt: the title's size and its column pitch const rule = (n) => ({ kind: 'rule', id: `rule-${n}`, direction: 'horizontal', thickness: pt(n === 1 ? 0.4 : 0.8), color: col('vermilion'), placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: pt(LEAD + n * PITCH) }, size: { width: 'fill' } } }); // A line of PITCH in column n (0, 1), its characters centred between two rules. const title = (id, content, n, family, size, weight) => ({ kind: 'text', id, content, fontFamily: family, fontWeight: weight, fontSize: pt(size), lineHeight: PITCH / size, color: col('ink'), letterSpacing: pt(size * 0.25), align: 'left', placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { x: pt(2 * BODY), y: pt(LEAD + n * PITCH) } } }); // two characters down const opener = { enabled: true, minHeight: cols(5), slot: { elements: [ rule(0), rule(1), rule(2), title('number', '{number}', 0, HEI, 12, 700), // 第三章 in the first column, in 黑 title('title', '{titleText}', 1, KAI, TITLE, 400), // the title in the second, in 楷 ] } }; // #endregion // The fore-edge: the book's title or the chapter's down the outer margin, the folio low. const foreEdge = (id, content, parity, edge, y, pages) => ({ kind: 'text', id, content, parity, pages, writingMode: 'vertical-rl', fontFamily: HEI, fontSize: pt(8), color: col('muted'), overflow: 'clip', placement: { anchor: { to: 'outer', edge }, offset: { y: em(y) } }, }); const header = { elements: [ foreEdge('book', '{title}', 'even', 'top', 4, 'body'), foreEdge('chapter', '{chapterNumber} {chapterTitle}', 'odd', 'top', 4, 'body'), foreEdge('folio', '{pageNumber}', 'all', 'bottom', -5, 'all'), // Across the foot of the opening page, in the edition's language: what is invented. { kind: 'text', id: 'imprint', content: '{subtitle}', pages: 'opener', fontFamily: SONG, fontSize: pt(6.5), color: col('muted'), align: 'center', overflow: 'wrap', placement: { anchor: { to: 'page', edge: 'bottom' }, offset: { y: mm(-9) }, size: { width: mm(100) } } }, ] }; const config = () => ({ // a factory: the engine caches resolved configs per object // Traditional characters to the mainland's rules, as Beijing and Shanghai publishers set // classical scholarship (gotcha: cjk-locale-tag). The citations follow it: zh-TW terms. locale: 'zh-Hant-CN', colorPalette, citations, cjk, page: { sizePreset: 'custom', width: mm(140), height: mm(203), dpi: 150, // 大32開 backgroundColor: col('paper'), margins: { top: mm(34), bottom: mm(26), left: mm(15), right: mm(19), mirror: true }, pageNumbering: { format: 'trad-chinese-informal', startAt: 45 } }, // 四十五 layout: { layoutType: 'single', writingMode: 'vertical-rl' }, // bound on the right bodyText: { fontFamily: SONG, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'), textAlign: 'justify', firstLineIndent: em(2), indentAfterHeading: true, }, headings: { fontFamily: SONG, fontWeight: 700, color: col('ink'), balancing: { enabled: false }, // the columns stay on the grid levels: [{ level: 1, numberingTemplate: '第{1:一}章', numberSeparator: ' ', breakBefore: { enabled: true, parity: 'odd' }, // gotcha: headings-drop-h1-break marginBottom: pt(0), advancedDesign: opener }] }, header, footer: { elements: [] }, }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Amostra em Markdown · 28 linhas · content.en.md
title: "版刻叢談" author: "沈硯秋" subtitle: "A specimen chapter written for the Postext Cookbook. The book and its author are invented; the five works cited are real." --- # 宋刻本的版式 {startAt=3} 雕版印書,始於唐而盛於宋。唐代的印本流傳至今的極少,所見多是佛經、曆書一類的零星之物,敦煌所出的咸通九年《金剛經》卷子,是其中有確切紀年的一件[@su1999]。到了宋代,經史子集都有了刻本,官府、書院、私家和書坊競相開雕,書的樣子也大體定了下來[@zhang1989]。後世講版本的人談到書葉的格局,總是從宋本說起。 一塊書版刻成一葉,四周用欄線圍住,叫作版框。欄線只有一道的稱「四周單邊」,外粗內細兩道的稱「四周雙邊」,只在左右兩邊用雙線的稱「左右雙邊」。框內用直線分行,每半葉幾行、每行幾字,叫作行款。同一地方的書坊往往沿用一種行款,所以著錄一部書,行款總是要記的,鑑定版本時也常用它來推斷刻書的地點和年代。 版框正中留出窄窄的一行,不刻正文,是為版心。印好的紙沿這一行對折,版心便落在書口上,翻檢時一眼可見。版心裏刻書名、卷次和葉碼,有時還刻上刻工的姓名和這一版的字數,那是當年計算工錢用的。版心上下各有一個形如魚尾的標記,叫作魚尾;魚尾到欄線之間若刻一道黑線,叫作黑口,空著不刻的叫作白口。 大體說來,浙江一帶的官刻和家刻,字體方整,多作白口、左右雙邊;福建建陽的坊刻,字體瘦勁,版式緊湊,常在框外左上角加一個小方格,刻上篇名,稱為書耳,便於讀者翻檢[@huang2005]。坊刻的書要賣得出去,處處替讀者打算,這一點從版式上也看得出來。 書末或目錄之後,常有一塊刻記,寫明何人於何年在何處刊行,有時兼作書坊的廣告,叫作牌記。牌記的形制有長方的,也有鐘形、亞字形的。後人鑑定版本,牌記是最直接的依據,可也最容易被書賈挖改作偽,所以不能單憑牌記下斷語,還要和行款、字體、紙墨、避諱字一起看。 西方學者系統地研究中國的雕版書,是二十世紀的事。Carter的專著第一次把中國印刷術的發明及其西傳完整地介紹給英語讀者[@carter1925];Twitchett則從書業和讀者的角度,討論中古時期的印刷與出版[@twitchett1983]。兩書所談雖不限於版式,卻都把宋代看作雕版書定型的時代,和中國學者的看法相合。 :::bibliography{title="本章引用書目" scope=chapter} :::references{format=bibtex} @book{su1999, author = {宿白}, title = {唐宋時期的雕版印刷}, publisher = {文物出版社}, address = {北京}, year = 1999, langid = {chinese}} @book{zhang1989, author = {張秀民}, title = {中國印刷史}, publisher = {上海人民出版社}, address = {上海}, year = 1989, langid = {chinese}} @book{huang2005, author = {黃永年}, title = {古籍版本學}, publisher = {江蘇教育出版社}, address = {南京}, year = 2005, langid = {chinese}} @book{carter1925, author = {Carter, Thomas Francis}, title = {The invention of printing in China and its spread westward}, publisher = {Columbia University Press}, address = {New York}, year = 1925, langid = {english}} @book{twitchett1983, author = {Twitchett, Denis}, title = {Printing and publishing in medieval China}, publisher = {Frederic C. Beil}, address = {New York}, year = 1983, langid = {english}} :::`; // content.<lang>.md: the same Chinese text in both // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { // every face the pages use, loaded before the build (gotcha: fonts-first) 'Noto Serif SC': ['400', '700'], // SONG: the text, notes, list, imprint; the list's title 'Noto Sans SC': ['400', '700'], // HEI: the fore-edge heads and folios; 第三章 'LXGW WenKai TC': ['400'], // KAI: the chapter's title }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── // Each voice loads the files of what it sets; the Song also what the style writes. const heading = markdown.match(/^# (.*?)(?: \{.*\})?$/m)[1]; // the title, no attributes const book = markdown.match(/^title: "(.*)"/m)[1]; // 版刻叢談, the head of the even pages const numerals = '第章一二三四五六七八九十'; await loadFonts(FONTS, markdown); await loadCjkFonts({ [SONG]: ['400'] }, `${markdown}卷等頁`, { vertical: true }); await loadCjkFonts({ [HEI]: ['400'] }, `${heading}${book}${numerals}`, { vertical: true }); await loadCjkFonts({ [SONG]: ['700'] }, markdown.match(/bibliography\{title="([^"]*)"/)[1], { vertical: true }); await loadCjkFonts({ [HEI]: ['700'] }, numerals); await loadCjkFonts({ [KAI]: ['400'] }, heading); const doc = await buildWithFonts(() => buildDocument({ markdown }, config()), markdown); showBook(doc, { title: t({ en: 'Vertical Chinese with citations in inline notes', es: 'Chino vertical con citas en notas dentro del renglón' }) }); offerPdf(() => renderToPdf(doc, { fontProvider: cjkPdfProvider }), `${RECIPE}.pdf`);Kit · core, fonts, viewer, pdf, cjk: igual em todas as receitas · 459 linhas
// ─── 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 v2 ── the same in every recipe · postext.dev/cookbook // Postext measures with the loaded faces and caches the widths: load every face // before the first build, from Fontsource, the files the PDF embeds too. /** 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. * Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', greek: 'U+0370-03FF', }; const jobs = []; let added = 0; 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 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 = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().then((ready) => { document.fonts.add(ready); added++; }, () => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return added; } /** Runs `build` and loads any face the pages use that FONTS missed (a regular * one with a warning), then clears the measurement cache and builds again. */ async function buildWithFonts(build, text = '') { const tried = new Set(); for (let round = 0; round < 3; round++) { kitStatus('Laying out…'); await new Promise(requestAnimationFrame); // let the status paint first const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; }); const wanted = { base: {}, variants: {} }; for (const { font, base } of [result].flat().flatMap(fontStringsOf)) { const { family, weight, style } = parseFont(font); const key = `${family}|${weight}|${style}`; if (tried.has(key) || hasFace(family, weight, style)) continue; tried.add(key); (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`); } if (Object.keys(wanted.base).length) { console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`); } const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true }); if (added === 0) return result; clearMeasurementCache(); } throw new Error('The fonts did not settle after three builds.'); } /** Every font string of the layout; `base` marks a block's own face. */ function fontStringsOf(doc) { const found = new Map(); const walk = (node) => { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) { node.forEach(walk); return; } for (const [key, value] of Object.entries(node)) { if (typeof value === 'string' && /fontString$/i.test(key)) { found.set(value, found.get(value) || key === 'fontString'); } else if (value && typeof value === 'object') walk(value); } }; walk(doc.pages); walk(doc.blocks); return [...found].map(([font, base]) => ({ font, base })); } /** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }. * A string with no weight ('95.8px Young Serif', from a design text) is 400. */ function parseFont(font) { const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim()); if (!m) throw new Error(`Unexpected font string: ${font}`); const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]); return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' }; } /** 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, '-'); } /** 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 v1 ── 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. */ function showPages(docs, { title, width = 460 } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── Kit · pdf v2 ── the same in every recipe that exports a PDF /** The Fontsource files the screen used, as TrueType: the nearest weight the * family ships, upright if it has no italic; latin, then what the face's * 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 more = kitSubsetsFor(text, meta); const files = await Promise.all(['latin', ...more].map(async (subset) => { const res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-${subset}-${w}-${s}.woff2`); if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} ${subset}`); return decompressWoff2(new Uint8Array(await res.arrayBuffer())); })); 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 v1 ── Chinese, Japanese and Korean books · postext.dev/cookbook // Fontsource ships a CJK family as about a hundred files per weight, each // declared in its stylesheet with the unicode-range it covers. The screen // loads the files the sample touches; the PDF gets the same files for the // characters its pages set in each face, and embeds each as a subset. // A book bound on the right (vertical text) is shown with its spreads // mirrored: page 1 alone on the left of the spine, then [3 | 2]. /** The files of a Fontsource face, read from its stylesheet: { url, range, * ranges }, the last declared first (the order the browser tries them in). */ function cjkSlices(family, weight, style) { cjkSlices.cache ??= new Map(); const id = fontsourceId(family); const css = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/${weight}${style === 'italic' ? '-italic' : ''}.css`; if (!cjkSlices.cache.has(css)) { cjkSlices.cache.set(css, fetch(css) .then((res) => { if (!res.ok) throw new Error(`Fontsource has no ${family} ${weight} ${style} (${res.status})`); return res.text(); }) .then((text) => [...text.matchAll(/@font-face\s*{([^}]*)}/g)].map(([, rule]) => { const range = /unicode-range:\s*([^;]+);/.exec(rule)?.[1].trim() ?? 'U+0-10FFFF'; const ranges = range.split(',').map((part) => { const [lo, hi = lo] = part.trim().slice(2).split('-'); return [parseInt(lo, 16), parseInt(hi, 16)]; }); return { url: new URL(/url\(([^)]+?\.woff2)\)/.exec(rule)[1], css).href, range, ranges }; }).reverse())); } return cjkSlices.cache.get(css); } /** The file of `slices` that holds code point `cp`, if any. */ function cjkSliceFor(slices, cp) { return slices.find((slice) => slice.ranges.some(([lo, hi]) => cp >= lo && cp <= hi)); } /** Whether Fontsource serves `family` as a Chinese, Japanese or Korean * family (its subsets name the script). Fails when the API does not * answer: a CJK face taken for a Latin one would paint in a system face. */ async function isCjkFamily(family) { const meta = await fontsourceMeta(family); if (!meta) throw new Error(`api.fontsource.org did not describe ${family}: reload to try again`); return !!meta.subsets?.some((subset) => /^(chinese|japanese|korean)/.test(subset)); } /** faces = { 'Noto Serif TC': ['400', '700'] }, as for loadFonts: the * whole FONTS object may be passed, its other families are left to * loadFonts. Adds one FontFace per file of each CJK face with its * unicodeRange, then loads the files `text` touches. `text` is what the * faces set: the sample for the text face; a book in several voices calls * it once per voice (loadCjkFonts({ 'LXGW WenKai TC': ['400'] }, quotes)), * so the heading and quotation faces fetch and check only their own * characters. Fails when a character of `text` is in no file of a face. * List every weight the pages use: a weight left to buildWithFonts gets * the latin file only. With { vertical: true } it also loads each * family's vertical forms (brackets, quotes, pause marks) for the canvas, * which needs loadVerticalAlternates imported from postext. Resolves to * the number of files loaded. */ async function loadCjkFonts(faces, text, { vertical = false } = {}) { kitStatus('Loading fonts…'); let loaded = 0; try { if (vertical && typeof loadVerticalAlternates !== 'function') { throw new Error('loadCjkFonts(…, { vertical: true }) needs loadVerticalAlternates imported from postext'); } for (const [family, specs] of Object.entries(faces)) { if (!(await isCjkFamily(family))) continue; const twin = []; for (const spec of new Set(specs)) { const weight = parseInt(spec, 10); const style = spec.endsWith('i') ? 'italic' : 'normal'; const slices = await cjkSlices(family, weight, style); const missing = [...new Set(text)].filter((ch) => /\S/.test(ch) && !cjkSliceFor(slices, ch.codePointAt(0))); if (missing.length) { throw new Error(`${family} ${spec} has no file for ${missing.slice(0, 12).join(' ')}: ` + `give each face the text it sets (loadCjkFonts({ '${family}': ['${spec}'] }, text))`); } for (const slice of slices) { document.fonts.add(new FontFace(family, `url(${slice.url}) format('woff2')`, { weight: String(weight), style, unicodeRange: slice.range })); twin.push({ source: slice.url, weight: String(weight), style, unicodeRange: slice.range }); } const font = `${style === 'italic' ? 'italic ' : ''}${weight} 16px "${family}"`; loaded += (await document.fonts.load(font, text)).length; if (!document.fonts.check(font, text)) throw new Error(`${family} ${spec} did not load for the sample`); } // The same files under a twin name with the `vert` feature on: the // canvas paints the punctuation of vertical lines with it. if (vertical && twin.length) await loadVerticalAlternates(family, twin); } } catch (error) { kitFail(error); throw error; } return loaded; } /** The PDF font provider for recipes with CJK faces: a family whose * Fontsource subsets are Chinese, Japanese or Korean gets the files that * hold the characters its pages set (`request.codePoints`); any other * family gets the latin file fontsourceProvider fetches (the "pdf" block) * and, when the face sets letters only latin-ext has, that file too. */ async function cjkPdfProvider(family, weight, style, request) { if (!(await isCjkFamily(family))) return cjkLatinPdfFiles(family, weight, style, request); const meta = await fontsourceMeta(family); const weights = meta.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && !meta.styles.includes('italic') ? 'normal' : style; const slices = await cjkSlices(family, w, s); const picked = new Set(); for (const cp of request?.codePoints ?? []) { const slice = cjkSliceFor(slices, cp); if (slice) picked.add(slice); } if (!picked.size) picked.add(slices[0]); return Promise.all(slices.filter((slice) => picked.has(slice)).map(async (slice) => { const res = await fetch(slice.url); if (!res.ok) throw new Error(`Fontsource file ${slice.url} (${res.status})`); return decompressWoff2(new Uint8Array(await res.arrayBuffer())); })); } /** A Latin family set next to the CJK faces: its latin file, then its * latin-ext file when the face sets letters only latin-ext has (ō ū in * Hepburn rōmaji, ǎ in pinyin), the file loadFonts adds on screen for * them. Latin comes first: postext-pdf draws a character from the first * file that has it, as the browser takes a character both files hold from * latin. A face Fontsource ships without latin-ext, or whose file does * not come, gets latin alone, and the PDF names the letters it lacks. */ async function cjkLatinPdfFiles(family, weight, style, request) { const meta = await fontsourceMeta(family); const beyond = [...(request?.codePoints ?? [])].some(cjkLatinExtOnly); if (!beyond || !meta?.subsets?.includes('latin-ext')) return fontsourceProvider(family, weight, style); const weights = meta.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && !meta.styles.includes('italic') ? 'normal' : style; const id = fontsourceId(family); const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-ext-${w}-${s}.woff2`; const [latin, ext] = await Promise.all([fontsourceProvider(family, weight, style), fetch(url) .then(async (res) => (res.ok ? decompressWoff2(new Uint8Array(await res.arrayBuffer())) : null), () => null)]); return ext ? [latin, ext] : latin; } /** Whether code point `cp` is in Fontsource's latin-ext file and not in * its latin file: Latin Extended-A and -B, IPA, the spacing modifiers and * Latin Extended Additional (loadFonts's test for latin-ext), less the * few latin holds too (ı Œ œ ʻ ʼ ˆ ˚ ˜). */ function cjkLatinExtOnly(cp) { if (!((cp >= 0x100 && cp <= 0x2ff) || (cp >= 0x1e00 && cp <= 0x1eff))) return false; return ![0x131, 0x152, 0x153, 0x2bb, 0x2bc, 0x2c6, 0x2da, 0x2dc].includes(cp); } /** showPages for a book bound on either edge. A right-bound book (the * document says so: doc.binding is 'right' for page.binding 'right' and * for vertical text) lies on the desk as it opens: page 1 alone on the * left of the spine, then [3 | 2], the spine shade on each page's inner * edge. `binding` ('left' | 'right') overrides the document's. */ function showBook(docs, { binding, ...options } = {}) { const count = showPages(docs, options); const right = (binding ?? [docs].flat()[0]?.binding) === 'right'; if (!document.getElementById('pt-kit-cjk')) { // The pages keep direction ltr: a canvas draws text in the direction its // element inherits, and under rtl each run would end where the engine // starts it, its brackets mirrored. document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit-cjk"> .pt-spread[dir="rtl"] canvas { direction: ltr; } .pt-spread[dir="rtl"] figure:first-child canvas { box-shadow: inset 14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } </style>`); } // Each pair stays [verso, recto] in the page; right to left, the verso // sits on the right. Phones stack the pages in reading order either way. for (const spread of document.querySelectorAll('#pages > .pt-spread')) spread.dir = right ? 'rtl' : 'ltr'; document.getElementById('pages').dataset.binding = right ? 'right' : 'left'; return count; } // ─── /Kit ───────────────────────────────────────────────────────────────────────
O script.js montado funciona como está: cole-o como script de módulo em qualquer página ou abra a receita no CodePen. Pasta da receita no GitHub ↗ (abre em uma nova aba)
Variações
#Colchetes em volta de cada nota
Algumas edições fecham o 夾注 entre colchetes no corpo do texto. cjk.warichu aplica os colchetes a todas as notas, inclusive às das citações.
- warichu: { fontSize: em(0.5), color: col('ink') }, // no brackets: the half size sets it off
+ warichu: { fontSize: em(0.5), color: col('ink'), open: '〔', close: '〕' },#Números na coluna em vez de notas
O estilo numérico da GB/T 7714 com colchetes de canto imprime 〔1〕 em pé na coluna, e a lista não muda.
- style: 'china-national-standard-gb-t-7714-2015-note',
- notes: 'warichu', // 夾注; 'footnote' sets the same notes at the foot of the page
+ style: 'china-national-standard-gb-t-7714-2015-numeric',
+ marker: 'corner', // 〔1〕, upright in vertical textErros comuns
Erro comum
Marque o documento como zh-Hans ou zh-Hant, não com LANG
As edições de uma receita são en e es, mas uma amostra chinesa é chinesa nas duas: `locale: LANG` a marcaria como inglês ou espanhol, hifenizaria as suas palavras latinas, chamaria as suas figuras de Figure ou Figura e daria ao PDF o idioma errado. Escreva você mesmo a marcação: 'zh-Hans' (convenções da China continental: quebra de linha GB, pontuação Kaiming) ou 'zh-Hant' (Taiwan: pontuação de largura inteira centralizada); 'zh-HK' para Hong Kong. Um 'zh' sozinho é lido como chinês simplificado continental. Uma amostra japonesa leva 'ja' (armadilha ja-locale-tag). Quebra de linha em chinês →
Erro comum
Fontes chinesas são carregadas em fatias, pelo bloco cjk
O Fontsource serve uma família chinesa, japonesa ou coreana em cerca de cem arquivos por peso, cada um com um intervalo de caracteres. loadFonts baixa só o arquivo latin, então na tela os caracteres chineses vêm de uma fonte do sistema e são medidos errado, e o fontsourceProvider entrega ao PDF esse arquivo latin, que os imprime como caixas vazias. Inclua o bloco cjk do kit, chame loadCjkFonts(FONTS, markdown) depois de loadFonts (uma vez por voz, com o texto que ela compõe, quando o livro usa várias fontes CJK) e passe a renderToPdf fontProvider: cjkPdfProvider: os dois pegam os arquivos que contêm os caracteres do texto. Fontes chinesas, japonesas e coreanas →
Erro comum
Componha a pontuação de cada região com uma fonte dessa região
As larguras da pontuação levam o branco de cada sinal para o lado em que a região do documento o coloca, não para o lado em que a fonte o desenha: com zh-Hans, uma vírgula Kaiming mantém a metade esquerda da sua caixa, onde uma fonte simplificada desenha o glifo, e cede a metade direita. Uma fonte tradicional centraliza ,。 na caixa, então a metade que sai leva parte do glifo e a vírgula fica colada no caractere seguinte. LXGW WenKai TC, a única Kai de texto do Fontsource, faz isso em texto simplificado. Componha o texto, as notas e as citações continentais em Noto Serif SC ou Noto Sans SC, deixe as fontes TC para zh-Hant e use uma fonte de pincel simplificada (Ma Shan Zheng) só em linhas de destaque, onde o texto de design não aplica larguras de pontuação. Ela também desenha formas herdadas que nem o padrão continental nem o de Taiwan imprimem, 為 com o topo 爫 de 爲 e 令 (em 冷, 領) com o pé de 卩: confira os caracteres que uma página compõe com ela. Largura da pontuação chinesa →
Erro comum
Um livro encadernado pela direita mostra as páginas duplas com showBook
Em um livro encadernado pela direita (texto árabe, hebraico ou persa, chinês vertical, ou page.binding 'right') a página 1 continua sendo a ímpar, mas fica à esquerda da lombada, e os pares se leem [3 | 2]. showPages dispõe todos os livros como se fossem encadernados pela esquerda; showBook, do bloco book (o bloco cjk traz a mesma função), lê doc.binding e espelha os pares. capture.hero continua nomeando uma página dupla em ordem de leitura, [par, ímpar]: [2, 3]. Livros encadernados pela direita →
Erro comum
Em uma grade de caracteres, desative o balanceamento de colunas
O balanceamento de colunas preenche uma página que termina curta (como quando um título mantido junto ao seu texto deixa linhas em branco no pé) acrescentando linhas da grade acima dos títulos e compondo um parágrafo com uma linha mais frouxa. Uma linha chinesa mais frouxa espaça os seus caracteres (0,13 eme em uma página GB/T 9704, muito acima de balancing.maxTracking), então os caracteres saem das colunas da grade e os títulos saem das suas linhas. Uma página contada em células deve terminar curta: defina headings.balancing: { enabled: false }. Grade de caracteres →
Erro comum
Qualquer objeto headings desativa a quebra de página do H1
Por padrão, um H1 salta para uma página ímpar (always-odd), mas passar qualquer objeto headings redefine esse padrão, então os capítulos ficam emendados e span: 'page' não faz nada. Declare de novo headings.levels[0].breakBefore: { enabled: true, parity } em toda configuração. Capítulos que abrem em página ímpar →
Erro comum
Coloque entre aspas cada valor do frontmatter
O YAML lê title: 1984 como número e uma data como objeto Date, e valores que não são strings saem vazios nos placeholders e deixam o PDF sem título. Coloque cada valor entre aspas: title: "1984". Metadados do documento →
Erro comum
Carregue todas as fontes antes do layout
O motor de layout mede o texto com as fontes que o navegador carregou e guarda as larguras em cache, então uma fonte que chega depois da primeira composição deixa quebras de linha erradas e um PDF que não corresponde mais à tela. Carregue antes todos os pesos e estilos e chame clearMeasurementCache() antes de recompor quando alguma chegar atrasada. Fontes antes da diagramação →
- Um estilo de notas escreve a segunda citação de uma obra de forma abreviada, e o estilo de notas da GB/T 7714 a reduz a 同1, “como na nota 1”. Um warichu não tem número, então o leitor não acha a nota 1. Este capítulo cita cada obra uma única vez; num texto que volte a citar uma obra, escreva a segunda citação à mão ou use o estilo numérico da segunda variação.
- O estilo escreve pontuação ocidental nas entradas chinesas,
宿白. 唐宋時期的雕版印刷[M]. 北京: 文物出版社, 1999., como a norma as imprime. Numa coluna vertical, esses sinais mantêm a forma ocidental: giram de lado e ocupam menos de uma casa, ao contrário do 。 e da , do texto. - Uma fileira de warichu tem metade do corpo do texto: com o texto em 10,5 pt, cada fileira fica com 5,25 pt, o menor tipo da página. Componha o texto em 五號 ou maior, ou aumente
cjk.warichu.fontSize; as duas fileiras sempre dividem a largura de uma coluna.
Créditos
- Receita
- Ignacio Ferro
- Texto
- Texto original, CC BY 4.0
- Fontes
- Noto Serif SC (SIL OFL 1.1) · Noto Sans SC (SIL OFL 1.1) · LXGW WenKai TC (SIL OFL 1.1)
- Código
- MIT, como o Postext
Editar este texto ↗ (abre em uma nova aba)Pasta da receita no GitHub ↗ (abre em uma nova aba)


