Zum Hauptinhalt springen
Rezept Nummer 90

Rezepte · Kapitel 5 · Buchaufbau

Ein chinesischer Aufsatz, zitiert nach GB/T 7714

Ein zweispaltiger chinesischer Zeitschriftenaufsatz: [@key]-Zitate erscheinen hochgestellt als [1] und [2–4], die Literatur trägt [M], [J], [D] und [EB/OL].

Auf dieser Seite
Ausgabe
Canvas · PDF
Stufe
Mittel
Postext
Getestet mit Postext 1.31.0
Erfordert ≥ 1.25.0 · postext-pdf ≥ 1.25.0
Lizenz
Aktualisiert am 10. Okt. 2026
Code MIT · Text CC BY 4.0

S. 41 · 1 von 2

  • Englisches Beispiel: noch keine deutsche Ausgabe
  • Endformat 184 × 260 mm
  • 2 Spalten, 6,3 mm Spaltenabstand
  • Noto Serif SC 9/15
  • LXGW WenKai TC
  • Noto Sans SC
  • 2 Seiten
  • Stufe
  • Postext 1.30.0
  • In 125 ms gesetzt
  • 147 Zeilen Code

Kurz gesagt

Ein kurzer Forschungsaufsatz aus einer chinesischen Zeitschrift. Der Autor gibt jeder Quelle ein Kürzel; Postext nummeriert die Quellen nach der Reihenfolge der Zitate und druckt die Literatur, wie chinesische Zeitschriften es verlangen.

Was Sie setzen

Zwei Seiten eines Forschungsaufsatzes in einer erfundenen Zeitschrift für Verlagswissenschaft, 示例出版研究 („Example Publishing Research“), gesetzt, wie eine chinesische Hochschulzeitschrift ihre Aufsätze setzt. Die Seite hat das Format 16开, 184 × 260 mm, mit zwei Spalten zu 23 Zeichen in 小五 auf einem Raster von 15 pt. Ein Titelblock läuft über die ganze Seitenbreite: der Name der Zeitschrift auf einem indigoblauen Band, der Titel in fetter Hei, die Autoren, ihre Institutionen und eine Zusammenfassung mit Schlüsselwörtern. Die Zitate folgen GB/T 7714—2015, der nationalen Norm für Literaturangaben, in ihrem numerischen System (顺序编码制): Jedes Werk erhält eine Nummer in der Reihenfolge seiner ersten Zitierung, die Nummer erscheint hochgestellt in eckigen Klammern, und drei oder mehr aufeinanderfolgende Nummern werden zu einem Bereich. Das Literaturverzeichnis gibt jedem Eintrag sein Kürzel für den Dokumenttyp und schreibt 等 nach drei Autoren eines chinesischen Werks und „et al.“ nach denen eines westlichen. Ein Kapitel einer Dissertation, zitiert nach APA 7 verwendet dasselbe Zitier-Markup in einem Autor-Jahr-Stil.

Dieses Rezept beantwortet

  • Wie zitiere ich nach dem chinesischen Standard GB/T 7714, auch in vertikalem Text?
  • Wie zitiere ich Werke und erstelle das Literaturverzeichnis nach APA, IEEE oder einem anderen Zitierstil?

Die kurze Antwort

script.js · Zeilen 31–43im vollständigen Code
// Register the engine once, before the first build. The GB/T 7714 numeric style writes
// superscript [n] in the order works are first cited, joins three or more in a row into a
// range and sets the list with its type codes: [M] book, [J] article, [D] thesis, [C]
// conference paper, [EB/OL] web page. Its CSL file holds a second layout for works whose
// language is English, with "et al." for 等: a Western entry takes "et al." and a Chinese
// one keeps 等, each chosen by the entry's `language`.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
  style: 'china-national-standard-gb-t-7714-2015-numeric',
  locale: 'zh-CN', // 等, 卷, 版 and the other terms of the list
  bibliography: { fontSize: em(7.5 / BODY), lineHeight: pt(12), entrySpacing: pt(1.5),
    labelWidth: em(1.7) }, // turnovers under the text, not past it: [1] and a space
};

Zutaten

Schrift
Noto Serif SC, Noto Sans SC, LXGW WenKai TC (SIL OFL 1.1)
Dateien
Keine: Jedes Bild wird im Code gezeichnet

Zubereitung

#1 · Der Stil und sein Layout für westliche Werke

Der Code steht oben in der kurzen Antwort. Die Zitier-Engine wird einmal registriert, vor dem ersten Build, und postext-citeproc liefert die drei Stile von GB/T 7714 mit: numerisch, Autor-Jahr und Fußnoten (Zitate). Im Text ist ein Zitat ein Schlüssel in eckigen Klammern. [@tinker1963] druckt ein hochgestelltes [1], und [@rayner2016; @dyson2001; @lin2023] druckt [2–4]. Zwei Nummern hintereinander bleiben eine Liste, [5,6]. Ein erneut zitiertes Werk behält seine erste Nummer: [4] im dritten Absatz und [1] und [2] weiter hinten im Aufsatz.

Die Norm verlangt „et al.“ nach den ersten drei Autoren eines westlichen Werks und 等 nach denen eines chinesischen. Die mitgelieferte CSL-Datei hat dafür zwei Layouts, eines davon für Werke auf Englisch, und jeder Eintrag nimmt sein Layout aus seinem eigenen Feld language: Aus Rayner und vier Mitautoren wird „RAYNER K, SCHOTTER E R, MASSON M E J, et al.“, aus 林岚 und drei Mitautoren „林岚, 周明远, 陈思齐, 等“. locale: 'zh-CN' schreibt die übrigen Wörter der Liste auf Chinesisch.

#2 · Die Literatur in einem CSL-YAML-Block

Die Werke stehen in einem Block :::references{format=csl-yaml} am Ende des Markdown, in dem Format, das Zotero als CSL YAML exportiert (Literaturangaben). Der CSL-Typ bestimmt das Kürzel: book druckt [M], article-journal [J], thesis [D], paper-conference [C] nach seinem // und dem Tagungsband, und webpage [EB/OL] mit dem Zugriffsdatum in eckigen Klammern. Ein Zeitschriftenaufsatz mit DOI wird zu [J/OL] und behält seine DOI, wie die Norm es verlangt. Chinesische Namen stehen in literal, damit der Stil sie nicht in Familien- und Vornamen zerlegt. :::bibliography{title=""} setzt die Liste unter die nicht nummerierte Überschrift 参考文献, und labelWidth setzt die Folgezeilen unter den Text des Eintrags, nicht darüber hinaus.

Seite 2: westliche Einträge in Versalien mit et al., chinesische Einträge mit 等, jeder mit seinem Typkürzel.

#3 · Der Titelblock auf einem Band

script.js · Zeilen 47–67im vollständigen Code
const BAND = 30; // mm from the trim's top
const at = (id, edge, y) => ({ anchor: { to: id ? `#${id}` : 'container', edge },
  offset: { y: mm(y) }, size: { width: mm(AREA) } });
const line = (id, content, family, size, color, placement, extra) => ({ kind: 'text', id,
  content, fontFamily: family, fontSize: pt(size), color: col(color), align: 'center',
  overflow: 'wrap', placement, ...extra });
const onBand = (x, y) => ({ anchor: { to: 'page', edge: 'top-left' },
  offset: { x: mm(x), y: mm(y) }, size: { width: mm(AREA) } });
const masthead = { enabled: true, minHeight: pt(9 * LEAD), slot: { elements: [
  { kind: 'box', id: 'band', style: { backgroundColor: col('indigo') },
    placement: { anchor: { to: 'page', edge: 'top-left' },
      size: { width: 'fill', height: mm(BAND) } } },
  line('journal', '示例出版研究', HEI, 15, 'paper', onBand(15.8, 13),
    { fontWeight: 700, align: 'left', letterSpacing: pt(3) }),
  line('issue', '第44卷 第3期 2026年9月', HEI, 8, 'mist', onBand(15.8, 16),
    { align: 'right' }),
  line('title', '{titleText}', HEI, 17, 'ink', at('', 'top-left', 14),
    { fontWeight: 700, lineHeight: 1.45 }),
  line('authors', '{attr.authors}', KAI, 12, 'ink', at('title', 'below', 4)),
  line('affiliations', '{attr.affiliations}', SONG, 7.5, 'muted', at('authors', 'below', 2)),
] } };

Der Titelblock ist die Gestaltung der H1, ein Überschriftenformat über beide Spalten. Das Band ist ein am Seitenrand verankerter Kasten, und der Name der Zeitschrift steht weiß darauf. Titel, Autoren und Institutionen kommen aus der Überschrift und ihren Attributen, jedes unter dem vorigen platziert (#title, below), sodass ein dreizeiliger Titel den Rest nach unten schiebt (Seitenbreite und erweiterte Gestaltung).

#4 · Die Zusammenfassung und ihre Bezeichnungen

script.js · Zeilen 71–78im vollständigen Code
// In 宋, not 楷: the one Kai on Fontsource is a Taiwan face (gotcha: cjk-face-region).
const calloutStyles = [{ id: 'abstract', span: 'page', backgroundEnabled: false,
  padding: { top: pt(0), bottom: pt(0), left: em(2), right: em(2) },
  marginTop: pt(0), marginBottom: pt(LEAD),
  body: { fontFamily: SONG, fontSize: pt(BODY), lineHeight: pt(LEAD), firstLineIndent: pt(0),
    textAlign: 'justify' } }];
const chipStyles = [{ id: 'label', fontFamily: HEI, bold: true, color: col('indigo'),
  backgroundEnabled: false, borderWidth: pt(0), paddingX: em(0), gap: em(0.5) }];

Die Zusammenfassung ist ein Kasten über die ganze Seitenbreite, beidseitig um zwei Zeichen eingezogen. Ihre Bezeichnungen sind Chips: :chip[摘 要:]{style="label"} setzt das Wort in fetter Hei, im Indigo der Zeitschrift, ohne Kasten (Chip-Formate). Chinesische Zeitschriften setzen die Zusammenfassung oft in 楷体; diese bleibt in der Song, aus dem Grund, der unter „Häufige Fehler“ steht.

script.js · Zeilen 16–22im vollständigen Code
const palette = {
  ink: '#1a1a1a', indigo: '#24427a', mist: '#b8c6e0', rule: '#9aa3b5', muted: '#5f6470',
  paper: '#ffffff',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.indigo })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));

Das ganze Rezept

Sandbox
// ═══ Postext Cookbook · Nº 090 · A Chinese paper cited to GB/T 7714 ═════════════════
// https://postext.dev/en/cookbook/gbt7714-chinese-paper
// 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.25.0
import {
  buildDocument, withLoadedFonts, renderPageToCanvas, registerCitationEngine,
} 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 paper is Chinese in both editions
const RECIPE = 'gbt7714-chinese-paper';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: black text, one indigo for the journal's name, its rules and its labels
const palette = {
  ink: '#1a1a1a', indigo: '#24427a', mist: '#b8c6e0', rule: '#9aa3b5', muted: '#5f6470',
  paper: '#ffffff',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.indigo })
  .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] = [9, 15]; // pt: 小五 on a 15 pt line, the grid both columns share
const [CHARS, LINES] = [23, 40]; // characters to a column's line, lines to a column
const PT = 25.4 / 72; // mm in a point
const AREA = (2 * CHARS + 2) * BODY * PT; // mm: two columns and a 2-em gutter, 152.4

// #region answer: GB/T 7714 numbered: [1] in citation order, [2–4], 等 or et al. by language
// Register the engine once, before the first build. The GB/T 7714 numeric style writes
// superscript [n] in the order works are first cited, joins three or more in a row into a
// range and sets the list with its type codes: [M] book, [J] article, [D] thesis, [C]
// conference paper, [EB/OL] web page. Its CSL file holds a second layout for works whose
// language is English, with "et al." for 等: a Western entry takes "et al." and a Chinese
// one keeps 等, each chosen by the entry's `language`.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
  style: 'china-national-standard-gb-t-7714-2015-numeric',
  locale: 'zh-CN', // 等, 卷, 版 and the other terms of the list
  bibliography: { fontSize: em(7.5 / BODY), lineHeight: pt(12), entrySpacing: pt(1.5),
    labelWidth: em(1.7) }, // turnovers under the text, not past it: [1] and a space
};
// #endregion

// #region masthead: the journal's name on an indigo band, then title, authors, affiliations
const BAND = 30; // mm from the trim's top
const at = (id, edge, y) => ({ anchor: { to: id ? `#${id}` : 'container', edge },
  offset: { y: mm(y) }, size: { width: mm(AREA) } });
const line = (id, content, family, size, color, placement, extra) => ({ kind: 'text', id,
  content, fontFamily: family, fontSize: pt(size), color: col(color), align: 'center',
  overflow: 'wrap', placement, ...extra });
const onBand = (x, y) => ({ anchor: { to: 'page', edge: 'top-left' },
  offset: { x: mm(x), y: mm(y) }, size: { width: mm(AREA) } });
const masthead = { enabled: true, minHeight: pt(9 * LEAD), slot: { elements: [
  { kind: 'box', id: 'band', style: { backgroundColor: col('indigo') },
    placement: { anchor: { to: 'page', edge: 'top-left' },
      size: { width: 'fill', height: mm(BAND) } } },
  line('journal', '示例出版研究', HEI, 15, 'paper', onBand(15.8, 13),
    { fontWeight: 700, align: 'left', letterSpacing: pt(3) }),
  line('issue', '第44卷 第3期 2026年9月', HEI, 8, 'mist', onBand(15.8, 16),
    { align: 'right' }),
  line('title', '{titleText}', HEI, 17, 'ink', at('', 'top-left', 14),
    { fontWeight: 700, lineHeight: 1.45 }),
  line('authors', '{attr.authors}', KAI, 12, 'ink', at('title', 'below', 4)),
  line('affiliations', '{attr.affiliations}', SONG, 7.5, 'muted', at('authors', 'below', 2)),
] } };
// #endregion

// #region abstract: across both columns, its labels in 黑 the colour of the journal
// In 宋, not 楷: the one Kai on Fontsource is a Taiwan face (gotcha: cjk-face-region).
const calloutStyles = [{ id: 'abstract', span: 'page', backgroundEnabled: false,
  padding: { top: pt(0), bottom: pt(0), left: em(2), right: em(2) },
  marginTop: pt(0), marginBottom: pt(LEAD),
  body: { fontFamily: SONG, fontSize: pt(BODY), lineHeight: pt(LEAD), firstLineIndent: pt(0),
    textAlign: 'justify' } }];
const chipStyles = [{ id: 'label', fontFamily: HEI, bold: true, color: col('indigo'),
  backgroundEnabled: false, borderWidth: pt(0), paddingX: em(0), gap: em(0.5) }];
// #endregion

// Running heads on the body pages, the folio at the outer corner, over a hairline.
const head = (id, content, parity, edge, x, extra) => ({ kind: 'text', id, content, parity,
  pages: 'body', fontFamily: HEI, fontSize: pt(7.5), color: col('muted'), overflow: 'clip',
  align: edge, placement: { anchor: { to: 'page', edge: `top-${edge}` },
    offset: { x: mm(x), y: mm(12) } }, ...extra });
const folio = { fontWeight: 700, color: col('indigo') };
const header = { elements: [
  head('v-folio', '{pageNumber}', 'even', 'left', 15.8, folio),
  head('v-head', '示例出版研究 2026年第3期', 'even', 'left', 24),
  head('r-head', '赵一鸣,等:横排中文正文的行长与行距', 'odd', 'right', -24),
  head('r-folio', '{pageNumber}', 'odd', 'right', -15.8, folio),
  { kind: 'rule', id: 'head-rule', pages: 'body', thickness: pt(0.5), color: col('rule'),
    placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: mm(15.8), y: mm(16.5) },
      size: { width: mm(AREA) } } },
] };

const config = () => ({
  locale: 'zh-Hans', // written out, never LANG (gotcha: cjk-locale-tag)
  colorPalette,
  citations,
  page: { width: mm(184), height: mm(260), dpi: 150, pageNumbering: { startAt: 41 },
    // 16开; with the grid on the margins are minimums, grown to centre the 23 × 40 area
    margins: { top: mm(22), bottom: mm(20), left: mm(15), right: mm(15), mirror: true } },
  layout: { layoutType: 'double', gutterWidth: pt(2 * BODY) },
  cjk: { grid: { enabled: true, charsPerLine: CHARS, linesPerPage: LINES } },
  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: HEI, fontWeight: 700, color: col('ink'),
    // Off by default on a character grid. On, a short column takes whole grid lines above
    // its heads, and the two columns of the last page end level.
    balancing: { enabled: true },
    levels: [
      { level: 1, breakBefore: { enabled: true, parity: 'any' } }, // the next page, either side
      { level: 2, numberingTemplate: '{2}', numberSeparator: ' ', fontSize: pt(10.5),
        lineHeight: pt(2 * LEAD), marginTop: pt(0), marginBottom: pt(0) }, // 1 实验方法
      { level: 3, numberingTemplate: '{2}.{3}', numberSeparator: ' ', fontSize: pt(BODY),
        lineHeight: pt(LEAD), marginTop: pt(0), marginBottom: pt(0) }, // 1.1 被试
    ] },
  headingStyles: [
    { id: 'article', numbered: false, span: 'page', advancedDesign: masthead },
    { id: 'intro', numbered: false }, // 引言 goes before section 1, unnumbered
    { id: 'references', numbered: false },
  ],
  calloutStyles,
  chipStyles,
  paragraphStyles: [{ id: 'colophon', fontFamily: SONG, fontSize: pt(6.5), lineHeight: pt(9),
    color: col('muted'), firstLineIndent: pt(0), textAlign: 'left', marginTop: pt(LEAD) }],
  header,
  footer: { elements: [] },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
Markdown-Beispiel · 140 Zeilen · content.en.mdtitle: "横排中文正文的行长与行距" --- # 横排中文正文的行长与行距:\\一项纸本与屏幕的对照阅读实验 {style="article" authors="赵一鸣¹ 孙雨桐²" affiliations="1. 示例大学新闻与传播学院,示例市 100000;2. 示例出版社排版中心,示例市 100000"} :::callout{type="abstract"} :chip[摘 要:]{style="label"}行长和行距是横排中文正文最基本的两个版式参数,现行出版物大多凭经验确定。本文以48名大学生为被试,比较每行20、28、36字三种行长与1.5倍、1.8倍两种行距在纸本和屏幕上的阅读速度与理解成绩。结果表明:行长为28字时阅读速度最快;行长增至36字,纸本上的理解成绩无显著变化,屏幕上则下降约6%;1.8倍行距的优势在长行条件下比在短行条件下明显。据此建议单栏横排书刊正文以每行26~30字为宜,行长超过32字时应相应加大行距。 :chip[关键词:]{style="label"}行长;行距;中文排版;阅读速度;屏幕阅读 :chip[文献标志码:]{style="label"}A  :chip[文章编号:]{style="label"}1000-0000(2026)03-0041-04 ::: ## 引言 {style="intro"} 行长与行距对阅读的影响,西文排版界讨论已久。Tinker在20世纪中叶以印刷品为材料做了大量易读性实验,认为最适宜的行长取决于字号和行距,不能脱离二者单独规定[@tinker1963]。近二十年来,眼动记录和屏幕阅读进入这一领域,研究者发现阅读速度与理解成绩并不总是同步变化,只用速度评价版式容易得出片面的结论[@rayner2016; @dyson2001; @lin2023]。 中文的情况与西文不同。汉字等宽,行长可以直接用字数计量,出版社也习惯以“每行字数×每页行数”规定版心,32开图书的正文多为每行26~28字[@zhou2019; @chen2021]。W3C的《中文排版需求》整理了中文版面的基本要素和术语[@clreq],但行长、行距取什么值,仍由各出版社自行决定。 这些数值大多来自排版经验,以汉字为材料、同时比较纸本与屏幕的实证研究还很少。林岚等[@lin2023]测量了屏幕上三种行长的阅读速度,没有考察行距;王晓和李文[@wang2022]讨论了屏幕阅读中的行距,只用了一种行长。本研究把行长和行距放在同一个实验中,在纸本和屏幕上分别测量阅读速度和理解成绩,为书刊正文的版式设计提供依据。 ## 实验方法 ### 被试 示例大学本科生48名,其中男生21名、女生27名,年龄18~23岁,母语均为汉语,视力或矫正视力正常,此前均未参加过阅读实验。 ### 材料与版式 从近年出版的科普读物中选取说明文24篇,每篇约600字,难度经预实验平衡。版式按3(行长:每行20、28、36字)×2(行距:1.5倍、1.8倍)设计,字号统一为五号(10.5 pt),字体为宋体,两端对齐。纸本材料用A4纸单面印刷;屏幕材料在27英寸显示器上呈现,调整显示比例,使字的视角大小与纸本一致。 ### 程序与测量 每名被试在纸本和屏幕上各读12篇,媒介的先后顺序在被试间平衡,版式条件在篇目间轮换。每读完一篇,回答5道理解题。阅读速度以每分钟字数计,理解成绩以答对题数的百分比计。屏幕条件下同时记录眼动,采样率为1000 Hz。 ## 结果 ### 阅读速度 行长为28字时,纸本和屏幕上的阅读速度均为最快,平均每分钟分别读512字和486字。行长20字时,换行次数增多,速度下降4%左右;行长36字时,速度下降7%左右,眼动记录显示换行后的回视次数明显增加。行太短和太长都会降低速度,这与Tinker对西文的观察方向一致[@tinker1963]。 ### 理解成绩 纸本上,三种行长的理解成绩差异不显著。屏幕上,行长36字时的理解成绩比28字时低约6%。行距的作用主要出现在长行条件下:行长36字时,1.8倍行距的理解成绩比1.5倍行距高约4%;行长20字时,两种行距几乎没有差别。 ## 讨论 长行降低了屏幕阅读的理解成绩,原因可能在换行时的视线定位。行越长,眼睛从行尾回到下一行行首的距离越大,落点越容易偏离;加大行距,相邻两行分得更开,这类错误随之减少。这一解释与眼动研究对换行和回视的描述相符[@rayner2016],也说明行长和行距需要一并考虑。 本研究的材料只有说明文,被试也限于大学生;文学作品和其他年龄段的读者是否表现出同样的规律,还需要进一步检验。屏幕条件只用了一种显示器,没有考察手机等小屏幕设备,那里的行长通常不到20字。 ## 结论 单栏横排的中文书刊正文,每行26~30字是较稳妥的选择。行长超过32字时,行距宜加大到1.8倍左右。面向屏幕的版式比纸本更需要控制行长,不宜把纸本的版心原样搬到屏幕上。 ## 参考文献 {style="references"} :::bibliography{title=""} :::paragraphs{style="colophon"} A specimen article written for the Postext Cookbook. The journal, its authors, their experiment and the Chinese works [4]–[6] and [8] are fictitious; works [1]–[3] and [7] are real. ::: :::references{format=csl-yaml} - id: tinker1963 type: book language: en author: [{family: Tinker, given: Miles A.}] title: Legibility of print publisher: Iowa State University Press publisher-place: Ames issued: 1963 - id: rayner2016 type: article-journal language: en author: [{family: Rayner, given: Keith}, {family: Schotter, given: Elizabeth R.}, {family: Masson, given: Michael E. J.}, {family: Potter, given: Mary C.}, {family: Treiman, given: Rebecca}] title: "So much to read, so little time: how do we read, and can speed reading help?" container-title: Psychological Science in the Public Interest volume: 17 issue: 1 page: 4-34 issued: 2016 DOI: 10.1177/1529100615623267 - id: dyson2001 type: article-journal language: en author: [{family: Dyson, given: Mary C.}, {family: Haselgrove, given: Mark}] title: The influence of reading speed and line length on the effectiveness of reading from screen container-title: International Journal of Human-Computer Studies volume: 54 issue: 4 page: 585-612 issued: 2001 DOI: 10.1006/ijhc.2001.0458 - id: lin2023 type: article-journal language: zh-CN author: [{literal: 林岚}, {literal: 周明远}, {literal: 陈思齐}, {literal: 王晓}] title: 屏幕上横排中文的行长与阅读速度 container-title: 示例出版研究 volume: 41 issue: 2 page: 15-24 issued: 2023 - id: zhou2019 type: book language: zh-CN author: [{literal: 周明远}] title: 汉字排版概论 publisher: 示例出版社 publisher-place: 示例市 issued: 2019 - id: chen2021 type: thesis genre: 硕士学位论文 language: zh-CN author: [{literal: 陈思齐}] title: 中文书刊版心设计研究 publisher: 示例大学 publisher-place: 示例市 issued: 2021 - id: wang2022 type: paper-conference language: zh-CN author: [{literal: 王晓}, {literal: 李文}] title: 屏幕阅读中的行距选择 container-title: 第五届数字出版学术研讨会论文集 editor: [{literal: 示例出版学会}] publisher: 示例出版社 publisher-place: 示例市 page: 88-95 issued: 2022 - id: clreq type: webpage language: zh-CN author: [{literal: W3C}] title: 中文排版需求 URL: https://www.w3.org/TR/clreq/ accessed: 2026-09-30 :::
`; // content.<lang>.md: the same Chinese text in both // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { // every face the pages use, loaded before the build 'Noto Serif SC': ['400'], // SONG: the text, the references, the affiliations 'Noto Sans SC': ['400', '700'], // HEI: running heads; the journal, title, heads, labels 'LXGW WenKai TC': ['400'], // KAI: the authors' names }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── // Each voice loads the files of what it sets, and the Song the words the style adds. const all = (re) => (markdown.match(re) ?? []).join(''); const labels = all(/:chip\[[^\]]*\]/g); const heads = `${all(/^#+ .*$/gm)}示例出版研究第卷期年月赵一鸣等横排中文正文的行长与行距`; await loadFonts(FONTS, markdown); await loadCjkFonts({ [SONG]: ['400'] }, `${markdown}等版卷期页`); await loadCjkFonts({ [HEI]: ['400', '700'] }, `${heads}${labels}0123456789`); await loadCjkFonts({ [KAI]: ['400'] }, all(/authors="[^"]*"/g)); const doc = await withLoadedFonts(() => buildDocument({ markdown }, config()), { ...kitFonts(FONTS), text: markdown }); showPages(doc, { title: t({ en: 'A Chinese paper cited to GB/T 7714', es: 'Un artículo chino citado según la GB/T 7714' }) }); offerPdf(() => renderToPdf(doc, { fontProvider: cjkPdfProvider }), `${RECIPE}.pdf`);
Kit · core, fonts, viewer, pdf, cjk: in jedem Rezept gleich · 458 Zeilen// ─── 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 ───────────────────────────────────────────────────────────────────────

Das zusammengesetzte script.js läuft unverändert: Fügen Sie es in das Modulskript einer beliebigen Seite ein, oder öffnen Sie das Rezept auf CodePen. Ordner des Rezepts auf GitHub ↗ (öffnet in neuem Tab)

Varianten

#Eckige Klammern statt hochgestellter Nummern

Manche Zeitschriften drucken die Zitatnummern auf der Zeile, in Textgröße. marker ändert, wie die Nummer gekennzeichnet wird, und lässt den Stil unverändert: 'brackets' ergibt [1] auf der Zeile, 'corner' ergibt 〔1〕.

   locale: 'zh-CN', // 等, 卷, 版 and the other terms of the list
+  marker: 'brackets',

#Chinesische Werke zuerst

Gemischte Bibliografien werden manchmal mit den chinesischen Werken vor den westlichen sortiert. In einem numerischen Stil laufen die Nummern dann nicht mehr der Reihe nach durch die Liste, deshalb passt das besser zum Autor-Jahr-Stil.

-  style: 'china-national-standard-gb-t-7714-2015-numeric',
+  style: 'china-national-standard-gb-t-7714-2015-author-date',
   locale: 'zh-CN', // 等, 卷, 版 and the other terms of the list
-  bibliography: { fontSize: em(7.5 / BODY), lineHeight: pt(12), entrySpacing: pt(1.5),
+  bibliography: { groupByLanguage: true, fontSize: em(7.5 / BODY), lineHeight: pt(12),

Häufige Fehler

Häufiger Fehler

Zeichnen Sie das Dokument mit zh-Hans oder zh-Hant aus, nicht mit LANG

Die Ausgaben eines Rezepts sind en und es, ein chinesisches Beispiel ist aber in beiden Chinesisch: `locale: LANG` würde es als Englisch oder Spanisch auszeichnen, seine lateinischen Wörter trennen, seine Abbildungen mit Figure oder Figura beschriften und dem PDF die falsche Sprache geben. Seit postext 1.30 ist die Auszeichnung auch die Sprache, in der der Text geformt wird (gemessen, gezeichnet und eingebettet). Unter einer englischen oder spanischen Auszeichnung setzt eine chinesische Schrift daher ihre Ziffern und die mit lateinischem Text geteilte Interpunktion in anderen Formen, und die Zeilen brechen an anderer Stelle um. Schreiben Sie die Auszeichnung selbst: 'zh-Hans' (Festlandkonventionen: GB-Zeilenumbruch, Kaiming-Interpunktion) oder 'zh-Hant' (Taiwan: zentrierte Interpunktion voller Breite); 'zh-HK' für Hongkong. Ein bloßes 'zh' gilt als vereinfacht, Festland. Ein japanisches Beispiel erhält 'ja' (Häufiger Fehler ja-locale-tag). Chinesischer Zeilenumbruch →

Häufiger Fehler

Chinesische Schnitte werden in Teilstücken geladen, über den Block cjk

Fontsource liefert eine chinesische, japanische oder koreanische Familie als etwa hundert Dateien pro Schriftstärke aus, jede für einen Zeichenbereich. loadFonts holt nur die Datei latin, am Bildschirm stammen die Han-Zeichen also aus einer Systemschrift und werden falsch gemessen, und fontsourceProvider gibt dem PDF diese latin-Datei, die sie als leere Kästchen druckt. Führen Sie den Kit-Block cjk auf, rufen Sie loadCjkFonts(FONTS, markdown) nach loadFonts auf (einmal pro Stimme, mit dem Text, den sie setzt, wenn das Buch mehrere CJK-Schriften verwendet), und geben Sie renderToPdf fontProvider: cjkPdfProvider: Beide nehmen die Dateien, die die Zeichen des Textes enthalten. Chinesische, japanische und koreanische Schriften →

  • Ein erzählendes Zitat, @zhou2019 ohne Klammern, nennt die Autoren und fügt die Nummer an, doch ein Schlüssel läuft durch chinesische Schriftzeichen weiter, sodass @zhou2019认为 als ein Schlüssel gelesen wird, den es nicht gibt. Schreiben Sie den Namen in den Text und das Zitat dahinter, 周明远[@zhou2019]认为, wie es chinesische Aufsätze ohnehin tun.
  • Der mitgelieferte numerische Stil druckt keine Fundstellen: [@zhou2019, 页 45] ergibt [5] ohne die Seite. GB/T 7714 setzt die Seite hinter die Klammer in die Hochstellung, [5]45; bis der Stil das tut, schreiben Sie die Seite in den Satz.
  • LXGW WenKai TC, die einzige Kai auf Fontsource, ist eine Schrift aus Taiwan: Sie zeichnet ,und 。 in die Mitte der Zelle, und unter zh-Hans beschneidet die Engine die rechte Hälfte der Zelle, was in die Glyphe schneidet. Die Zusammenfassung steht in der Noto Serif SC, und die Kai bleibt den Namen der Autoren vorbehalten, die keine Satzzeichen haben.
  • Auf einem Zeichenraster ist der Spaltenausgleich standardmäßig aus: Jede Zeile bleibt auf dem Raster, und eine Spalte, die die nächste Überschrift nicht aufnehmen kann, endet kurz. Bleibt er aus, endete die zweite Spalte von Seite 1 drei Zeilen zu kurz (die Überschrift 2 结果 braucht ihre eigenen zwei Zeilen und zwei Zeilen Text darunter, sie eröffnet also Seite 2), und die letzte Seite füllte ihre erste Spalte und ließe die zweite zur Hälfte leer. Das Rezept setzt headings.balancing.enabled: true: Eine kurze Spalte nimmt ganze Rasterzeilen über ihren Überschriften auf, nie indem sie Zeichen aus ihren Zellen rückt, und die beiden Spalten der letzten Seite enden auf gleicher Höhe.

Nachweise

Text
Eigener Text, CC BY 4.0
Schriften
Noto Serif SC (SIL OFL 1.1) · Noto Sans SC (SIL OFL 1.1) · LXGW WenKai TC (SIL OFL 1.1)
SandboxPDF