成品一览
一篇关于屏幕阅读与纸面阅读的博士论文的最后七页,用B5页面黑白排版。第6章、附录A、术语表、参考文献和索引都在同样一条62 mm高的黑色色带下开始,标题在色带上反白。章在色带中显示数字,附录显示字母;后置部分的各节带眉题BACK MATTER和一条简短的斜体说明。本章排为两端对齐的单栏。术语表和索引改为两栏齐左,参考文献回到单栏,每个条目的转行都缩进。索引由正文中的标记生成:引擎把术语按首字母归类排序,找到每个标记所在的页,并把连续的页合并成171–73这样的页码范围。粗体页码指向术语表中的释义。
这道食谱解答
- 怎样排参考文献或术语表(悬挂缩进、较小字号)?
- 怎样做一份页码随正文移动自动更新的书末索引?
- 怎样让标题、粗体字和项目符号不再显示成蓝色?
- 怎样给标题编号(1、1.1、1.1.1),并给每一级设置不同样式?
- 怎样设置书眉:左页放书名,右页放章名,页码在外侧?
- 怎样强制分页或分栏,并让每章都从右页开始?
简短回答
// '# Glossary {style="glossary"}' in the Markdown picks a style. Each style starts a page
// of either parity, the appendix a recto (a style that sets no break inherits its level's
// 'odd': gotcha style-inherits-break), stays out of the chapter count (numbered: false, so
// its band has no numeral) and brings its own running heads; the glossary and the index
// set their pages in two columns until the next '#'. config() takes both lists below.
const twoColumns = { layoutType: 'double', gutterWidth: mm(6) };
const backMatter = (id, extra) => ({ id, numbered: false, breakBefore: { enabled: true,
parity: 'any' }, advancedDesign: opener('Back matter'), header: sectionHeads, ...extra });
const headingStyles = () => [
backMatter('appendix', { breakBefore: { enabled: true, parity: 'odd' }, // {letter="A"}
header: appendixHeads, advancedDesign: opener('Appendix', '{attr.letter}') }),
backMatter('glossary', { layout: twoColumns }),
backMatter('references'),
backMatter('index', { layout: twoColumns }),
];
// One paragraph per entry, in :::paragraphs{style="…"}: the turnover lines hang, so the
// first word of every entry stands clear at the left. Ragged, as APA asks of references,
// and so never hyphenated (gotcha: ragged-no-hyphenation). The index has its own settings.
const entries = (id, size, lead, hang, extra) => ({ id, fontSize: pt(size),
lineHeight: pt(lead), textAlign: 'left', hangingIndent: em(hang), ...extra });
const paragraphStyles = () => [
entries('term', 9.3, 12.4, 1), // the glossary: a bold term, then its definition
entries('reference', 9.3, 12.4, 1.5, { spaceBetween: pt(2.4) }),
];
用料
做法
#1 · 给后置部分的每一部分一种标题样式
代码见上文的简短回答。# Glossary {style="glossary" note="…"}开启一节,一直延续到下一个一级标题;这一节的页面使用该样式的版式、书眉和章首设计(标题样式)。所以术语表和索引改为两栏,而参考文献的样式没有设置版式,就退回到文档的单栏。numbered: false让这些标题不计入章数(删掉它,术语表的色带会印出8)。每种样式都指定了自己的分页方式,因为没有指定的样式会继承章的'odd',留下空白左页。术语表和参考文献的条目是:::paragraphs{style="…"}块中的段落,字号9.3 pt、行距12.4 pt,正文则是11 pt、行距14.6 pt;转行在术语表中悬挂1 em,在参考文献中悬挂1.5 em(段落样式)。
#2 · 所有章首页共用一条色带
const SINK = 8; // lines reserved, 41.2 mm: 3.2 mm more than the band, and text on the grid
// Design text sets each baseline 0.8 of its line under the line's top, and a line is 1.2 × the
// size unless lineHeight says otherwise. In mm, a line's part above its baseline and below it:
const PT = 25.4 / 72;
const above = (size, lineHeight = 1.2) => 0.8 * size * lineHeight * PT;
const below = (size, lineHeight = 1.2) => 0.2 * size * lineHeight * PT;
const KICKER = 4.3, TITLE = BAND - TOP - 9.5; // mm under the text block's top: two baselines
// A bottom-aligned box that ends below() under a baseline sets its last line on it. The
// numeral's line is 0.72 of its size: a line taller than its box would hang from its top.
const text = (id, content, family, size, lineHeight, baseline, edge, w, extra) => ({
kind: 'text', id, content, fontFamily: family, fontSize: pt(size), lineHeight,
color: col('paper'), overflow: 'wrap', align: edge.endsWith('right') ? 'right' : 'left',
verticalAlign: 'bottom', ...extra, placement: { anchor: { to: 'container', edge },
size: { width: mm(w), height: mm(baseline + below(size, lineHeight)) } } });
// The mark: '{number}', empty on an unnumbered heading, or the appendix's '{attr.letter}'.
const opener = (label, mark = '{number}') => ({ enabled: true, minHeight: pt(SINK * LEAD),
slot: { elements: [
{ kind: 'box', id: 'band', style: { backgroundColor: col('band') }, placement: {
anchor: { to: 'page', edge: 'top-left' }, size: { width: 'fill', height: mm(BAND) } } },
text('label', label, LABEL, 8, 1.2, KICKER, 'top-left', 80,
{ fontWeight: 700, letterSpacing: pt(1.6), textTransform: 'uppercase' }),
text('title', '{titleText}', DISPLAY, 34, 1.04, TITLE, 'top-left', 84),
text('mark', mark, DISPLAY, 118, 0.72, TITLE, 'top-right', 34),
text('note', '{attr.note}', TEXT, 8.6, 1.3, TITLE, 'top-right', 44, { italic: true }),
] } });
标记是{number},在不编号的标题上为空,所以一套设计同时服务于章、附录和后置部分:附录把{attr.letter}作为标记传入,后置部分的各节在本该放数字的位置印出{attr.note}。minHeight预留八行14.6 pt,即从版心顶端起41.2 mm,比色带多出3.2 mm,并让正文保持在网格上。设计文本把基线放在行高的0.8处,所以底边位于某条基线below()的底对齐框,其最后一行就落在那条基线上。标题、118 pt的数字和说明的最后一行都立在标题的基线上,距裁切线52.5 mm。数字的行高取字号的0.72,因为在1.4.1中,高于所在框的行会忽略verticalAlign: 'bottom'。
#3 · 手动给附录编字母
// In 1.4.1 a heading style cannot change the numbering: the appendix is unnumbered, and its
// letter feeds the band (see answer), the running head and a table type that counts A.1.
const appendixHeads = heads('Appendix {attr.letter}. {chapterTitle}');
const appendixTables = { ...defaultResourceTypes(LANG).find((type) => type.id === 'table'),
id: 'table-a', numberingTemplate: 'A.{n}' }; // a copy of 'table'
在postext 1.4.1中,标题样式不能改变所在级别的编号,所以附录不编号,由# Interview guide {style="appendix" letter="A"}带上字母。这个属性供给色带和附录的书眉,附录自己的资源类型把其中的表编为A.1、A.2,依此类推;如果用默认类型,这张表会成为表6.2,因为不编号的标题让章计数器停在6。本章自己的标题按各级模板计数:'{1}.{2}'印出6.1,continuation.headings.h1: 5让这一章成为第六章。
#4 · 把书眉放在外侧
const HEAD = 17.5, GAP = 9; // mm: the heads' baseline under the trim; the folio to the words
// Each text is placed by its top, above() over HEAD: the folio and the capitals share a baseline.
const head = (id, content, parity, edge, x, size = 7.5, extra) => ({ kind: 'text', id, content,
parity, pages: 'body', fontFamily: LABEL, fontSize: pt(size), fontWeight: 700,
letterSpacing: pt(1.3), textTransform: 'uppercase', color: col('ink'), ...extra, placement: {
anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(HEAD - above(size)) } } });
const folio = { fontFamily: TEXT, fontWeight: 400, letterSpacing: pt(0) };
const heads = (recto) => ({ elements: [
head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, 9.5, folio),
head('verso', '{title}', 'even', 'top-left', OUTER + GAP),
head('recto', recto, 'odd', 'top-right', -(OUTER + GAP)),
head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, 9.5, folio),
// The header's container spans the text block, so one rule serves both pages.
{ kind: 'rule', id: 'hairline', pages: 'body', direction: 'horizontal', thickness: pt(0.5),
color: col('rule'), placement: { anchor: { to: 'container', edge: 'top-left' },
offset: { y: mm(HEAD + 2) }, size: { width: 'fill' } } },
] });
const chapterHeads = heads('Chapter {chapterNumber}. {chapterTitle}');
const sectionHeads = heads('{chapterTitle}'); // 'Glossary', 'References', 'Index'
// Openers drop the folio to the foot, centred under the text block, its baseline 12 mm below.
const footer = { elements: [{ kind: 'text', id: 'drop-folio', content: '{pageNumber}',
pages: 'opener', ...folio, fontSize: pt(9.5), color: col('ink'), align: 'center',
placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(12 - above(9.5)) } } }] };
四个文本元素锚定在页面上,并用parity筛选,让页码在两页上都位于外侧;每个元素按顶边定位,放在距裁切线17.5 mm的基线的above()处,所以9.5 pt的页码和7.5 pt的大写字母立在同一行上。左页印论文标题({title},来自frontmatter),右页印该节标题({chapterTitle}),例如第177页上的INDEX。pages: 'body'让它们不出现在章首页上;章首页只有页脚的页码,在版心下居中。本章的右页会印Chapter 6. Conclusion,附录的右页会印Appendix A. Interview guide,不过在这个示例中只有索引延续到了右页。
#5 · 在正文讨论术语的地方做标记
// Glossary definitions carry 'main' (bold numbers); runs of pages join as 171–72 (Chicago).
// The heads stand on the entries' 11.6 pt pitch, 7.5 pt of space above them: 19 pt from the
// last entry of a letter to the next letter's baseline, 11.6 pt from a letter to its first entry.
const index = { fontFamily: TEXT, fontSize: pt(9), lineHeight: pt(11.6), color: col('ink'),
indent: em(1), turnoverIndent: em(2), rangeFormat: 'chicago',
groups: { fontFamily: DISPLAY, fontSize: pt(13), fontWeight: 400, color: col('ink'),
marginTop: pt(7.5) } };
:index[tablet]印出这个词,并以同一个词归入索引;Rayner:index{term="Rayner, Keith"}什么也不印,把Rayner所在的页归入全名之下;term="interviews!timing of"生成一个子条目。术语表中的术语带main,所以它们的页码排为粗体,正如索引色带中的说明所写。# Index {style="index"}下的:::index把条目排在该样式的两栏中,buildDocument会反复重新排版文档,直到页码不再变化。条目字号9 pt、行距11.6 pt,转行悬挂2 em,子条目缩进1 em;字母用展示字体排,字号13 pt。标记在表格单元格中不起作用,所以表6.1的各行不会增加页码:look-backs列出的是报告它们的那一段,而不是那张表。
#6 · 所有默认颜色都用墨色
const palette = { ink: '#000000', band: '#000000', rule: '#000000', paper: '#ffffff' };
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
// Bold, italic and list markers default to 'main-color': pointed at the ink, they print black.
const colorPalette = Object.entries({ ...palette, 'main-color': palette.ink })
.map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
引擎对粗体、斜体和列表标记的默认颜色取自调色板条目main-color,所以把它指向墨色,这些就会印成黑色而不是蓝色(调色板)。在1.4.1中,调色板管不到bodyText.referenceColor,所以配置里要再写一遍:没有这一行,正文中的Table 6.1会印成#295AA3蓝色。referenceBold: false把引用排为正体,与周围的作者–年份引文一致。
完整食谱
// ═══ Postext Cookbook · Nº 031 · Thesis back matter: appendix, glossary and index ═══ // https://postext.dev/en/cookbook/thesis-back-matter // Code: MIT · Text: original (CC BY 4.0) · Pictures: none // Fonts: Libertinus Serif, Serif Display and Sans (SIL OFL 1.1) · Needs postext ≥ 1.7.0 import { buildDocument, renderPageToCanvas, clearMeasurementCache, defaultResourceTypes, } from 'https://esm.sh/postext'; import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf'; const LANG = 'en'; // @lang: the language of the sample document ('en') const RECIPE = 'thesis-back-matter'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: one ink; every colour is black or white, each under its own name const palette = { ink: '#000000', band: '#000000', rule: '#000000', paper: '#ffffff' }; const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id }); // Bold, italic and list markers default to 'main-color': pointed at the ink, they print black. const colorPalette = Object.entries({ ...palette, 'main-color': palette.ink }) .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })); // #endregion const TEXT = 'Libertinus Serif', DISPLAY = 'Libertinus Serif Display', LABEL = 'Libertinus Sans'; const TOP = 24, INNER = 25, OUTER = 31; // mm: a 120 mm measure, about 70 characters at 11 pt const LEAD = 14.6; // pt: the body's leading, the grid every page is set on const BAND = 62; // mm from the trim's top: the black band at the head of every opener // #region answer: back matter as unnumbered heading styles, entries in hanging indents // '# Glossary {style="glossary"}' in the Markdown picks a style. Each style starts a page // of either parity, the appendix a recto (a style that sets no break inherits its level's // 'odd': gotcha style-inherits-break), stays out of the chapter count (numbered: false, so // its band has no numeral) and brings its own running heads; the glossary and the index // set their pages in two columns until the next '#'. config() takes both lists below. const twoColumns = { layoutType: 'double', gutterWidth: mm(6) }; const backMatter = (id, extra) => ({ id, numbered: false, breakBefore: { enabled: true, parity: 'any' }, advancedDesign: opener('Back matter'), header: sectionHeads, ...extra }); const headingStyles = () => [ backMatter('appendix', { breakBefore: { enabled: true, parity: 'odd' }, // {letter="A"} header: appendixHeads, advancedDesign: opener('Appendix', '{attr.letter}') }), backMatter('glossary', { layout: twoColumns }), backMatter('references'), backMatter('index', { layout: twoColumns }), ]; // One paragraph per entry, in :::paragraphs{style="…"}: the turnover lines hang, so the // first word of every entry stands clear at the left. Ragged, as APA asks of references, // and so never hyphenated (gotcha: ragged-no-hyphenation). The index has its own settings. const entries = (id, size, lead, hang, extra) => ({ id, fontSize: pt(size), lineHeight: pt(lead), textAlign: 'left', hangingIndent: em(hang), ...extra }); const paragraphStyles = () => [ entries('term', 9.3, 12.4, 1), // the glossary: a bold term, then its definition entries('reference', 9.3, 12.4, 1.5, { spaceBetween: pt(2.4) }), ]; // #endregion // #region opener: a black band across the head of the page, the title reversed out of it const SINK = 8; // lines reserved, 41.2 mm: 3.2 mm more than the band, and text on the grid // Design text sets each baseline 0.8 of its line under the line's top, and a line is 1.2 × the // size unless lineHeight says otherwise. In mm, a line's part above its baseline and below it: const PT = 25.4 / 72; const above = (size, lineHeight = 1.2) => 0.8 * size * lineHeight * PT; const below = (size, lineHeight = 1.2) => 0.2 * size * lineHeight * PT; const KICKER = 4.3, TITLE = BAND - TOP - 9.5; // mm under the text block's top: two baselines // A bottom-aligned box that ends below() under a baseline sets its last line on it. The // numeral's line is 0.72 of its size: a line taller than its box would hang from its top. const text = (id, content, family, size, lineHeight, baseline, edge, w, extra) => ({ kind: 'text', id, content, fontFamily: family, fontSize: pt(size), lineHeight, color: col('paper'), overflow: 'wrap', align: edge.endsWith('right') ? 'right' : 'left', verticalAlign: 'bottom', ...extra, placement: { anchor: { to: 'container', edge }, size: { width: mm(w), height: mm(baseline + below(size, lineHeight)) } } }); // The mark: '{number}', empty on an unnumbered heading, or the appendix's '{attr.letter}'. const opener = (label, mark = '{number}') => ({ enabled: true, minHeight: pt(SINK * LEAD), slot: { elements: [ { kind: 'box', id: 'band', style: { backgroundColor: col('band') }, placement: { anchor: { to: 'page', edge: 'top-left' }, size: { width: 'fill', height: mm(BAND) } } }, text('label', label, LABEL, 8, 1.2, KICKER, 'top-left', 80, { fontWeight: 700, letterSpacing: pt(1.6), textTransform: 'uppercase' }), text('title', '{titleText}', DISPLAY, 34, 1.04, TITLE, 'top-left', 84), text('mark', mark, DISPLAY, 118, 0.72, TITLE, 'top-right', 34), text('note', '{attr.note}', TEXT, 8.6, 1.3, TITLE, 'top-right', 44, { italic: true }), ] } }); // #endregion // #region running-heads: the thesis on the verso, the section on the recto, a hairline under const HEAD = 17.5, GAP = 9; // mm: the heads' baseline under the trim; the folio to the words // Each text is placed by its top, above() over HEAD: the folio and the capitals share a baseline. const head = (id, content, parity, edge, x, size = 7.5, extra) => ({ kind: 'text', id, content, parity, pages: 'body', fontFamily: LABEL, fontSize: pt(size), fontWeight: 700, letterSpacing: pt(1.3), textTransform: 'uppercase', color: col('ink'), ...extra, placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(HEAD - above(size)) } } }); const folio = { fontFamily: TEXT, fontWeight: 400, letterSpacing: pt(0) }; const heads = (recto) => ({ elements: [ head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, 9.5, folio), head('verso', '{title}', 'even', 'top-left', OUTER + GAP), head('recto', recto, 'odd', 'top-right', -(OUTER + GAP)), head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, 9.5, folio), // The header's container spans the text block, so one rule serves both pages. { kind: 'rule', id: 'hairline', pages: 'body', direction: 'horizontal', thickness: pt(0.5), color: col('rule'), placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: mm(HEAD + 2) }, size: { width: 'fill' } } }, ] }); const chapterHeads = heads('Chapter {chapterNumber}. {chapterTitle}'); const sectionHeads = heads('{chapterTitle}'); // 'Glossary', 'References', 'Index' // Openers drop the folio to the foot, centred under the text block, its baseline 12 mm below. const footer = { elements: [{ kind: 'text', id: 'drop-folio', content: '{pageNumber}', pages: 'opener', ...folio, fontSize: pt(9.5), color: col('ink'), align: 'center', placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(12 - above(9.5)) } } }] }; // #endregion // #region appendix: the letter comes from the heading, '# Interview guide {letter="A"}' // In 1.4.1 a heading style cannot change the numbering: the appendix is unnumbered, and its // letter feeds the band (see answer), the running head and a table type that counts A.1. const appendixHeads = heads('Appendix {attr.letter}. {chapterTitle}'); const appendixTables = { ...defaultResourceTypes(LANG).find((type) => type.id === 'table'), id: 'table-a', numberingTemplate: 'A.{n}' }; // a copy of 'table' // #endregion // #region index: the pages of the :index marks, sorted under letters in the display face // Glossary definitions carry 'main' (bold numbers); runs of pages join as 171–72 (Chicago). // The heads stand on the entries' 11.6 pt pitch, 7.5 pt of space above them: 19 pt from the // last entry of a letter to the next letter's baseline, 11.6 pt from a letter to its first entry. const index = { fontFamily: TEXT, fontSize: pt(9), lineHeight: pt(11.6), color: col('ink'), indent: em(1), turnoverIndent: em(2), rangeFormat: 'chicago', groups: { fontFamily: DISPLAY, fontSize: pt(13), fontWeight: 400, color: col('ink'), marginTop: pt(7.5) } }; // #endregion const config = () => ({ // a factory, never a shared object (gotcha: config-cache-identity) colorPalette, header: chapterHeads, footer, layout: { layoutType: 'single' }, page: { sizePreset: 'custom', width: mm(176), height: mm(250), dpi: 150, // B5 margins: { top: mm(TOP), bottom: mm(24), left: mm(INNER), right: mm(OUTER), mirror: true } }, bodyText: { fontFamily: TEXT, fontSize: pt(11), lineHeight: pt(LEAD), color: col('ink'), // 'Table 6.1' in roman and in ink, outside the palette's reach (gotcha: palette-skips-designs) referenceColor: col('ink'), referenceBold: false, firstLineIndent: mm(4.5), indentAfterHeading: false, minWordSpacing: 0.8, maxWordSpacing: 1.8 }, // Exact heading margins (snapToGrid: false), no lines added above them; the chapter's heads // measure whole grid lines. headings: { fontFamily: DISPLAY, fontWeight: 400, color: col('ink'), snapToGrid: false, balancing: { maxLinesPerHeading: 0 }, levels: [ // The H1 break restated (gotcha: headings-drop-h1-break). span: 'page' (the styles inherit // it) sets the band above the columns: inside a column, its top would be clipped. { level: 1, numberingTemplate: '{1}', span: 'page', marginBottom: pt(0), advancedDesign: opener('Chapter'), breakBefore: { enabled: true, parity: 'odd' } }, { level: 2, numberingTemplate: '{1}.{2}', fontSize: pt(14), lineHeight: pt(LEAD), marginTop: pt(LEAD * 1.5), marginBottom: pt(LEAD / 2) }, // three lines in all ] }, headingStyles: headingStyles(), paragraphStyles: paragraphStyles(), index, orderedLists: { marginTop: pt(LEAD / 2), marginBottom: pt(LEAD / 2) }, unorderedLists: { bulletChar: '–' }, resourceTypes: [...defaultResourceTypes(LANG), appendixTables], // tables 6.1… and A.1… // Captions in the text face, as APA sets a table's number and title. captionStyle: { fontSize: pt(9), position: 'above', gap: pt(4), note: { fontSize: pt(8) } }, // Rules only and a bold header: filled header cells show seams between the columns. tableStyle: { rules: 'horizontal', borderColor: col('rule'), borderWidth: pt(0.5), headerBackgroundEnabled: false, headerFontSize: pt(9.5), bodyFontSize: pt(9.5), cellPadding: mm(1) }, calloutStyles: [{ id: 'colophon', span: 'page', marginTop: pt(LEAD), backgroundEnabled: false, stripe: { enabled: true, side: 'top', width: pt(0.5), color: col('rule') }, padding: { top: mm(2.5), right: mm(0), bottom: mm(0), left: mm(0) }, body: { fontSize: pt(8), lineHeight: pt(10.5), firstLineIndent: pt(0), textAlign: 'left' } }], }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Markdown样例 · 126行 · content.en.md
title: "Reading on Screens and Paper" subtitle: "A Mixed-Methods Study of Comprehension, Confidence and Navigation" author: "Ines Varley" --- # Conclusion This thesis set out to test whether it matters if a long text:index{term="texts, length of"} is read on paper or on a screen. Chapters 3 to 5 reported a within-subjects:index{term="within-subjects design"} experiment with forty-eight :index[undergraduates] and :index[interviews] with sixteen of them, combined in the :index[convergent design] described by Creswell:index{term="Creswell, John W."} and Plano Clark:index{term="Plano Clark, Vicki L."} (2018). This chapter brings the two strands together and sets out what they mean for :index[teaching] and for the :index[design of reading software]. ## What the study found :ref{id="findings" style="full"} summarises the results.:index{term="comprehension"} On :index[literal]{term="comprehension!literal"} questions, answerable from a single sentence, the medium made no difference. On :index[inferential]{term="comprehension!inferential"} questions, which required connecting ideas across paragraphs, paper readers scored higher. The difference points the same way as the :index[meta-analyses] of Delgado:index{term="Delgado, Pablo"} et al. (2018) and Clinton:index{term="Clinton, Virginia"} (2019), which found the :index[paper advantage] in :index[expository]{term="expository text"} rather than :index[narrative]{term="narrative text"} texts. Calibration:index{term="calibration"} showed the larger difference. Screen readers:index{term="calibration!on screen"} predicted:index{term="confidence!judgements of"} higher scores than paper readers and obtained lower ones, so the gap between :index[confidence] and :index[accuracy]:index{term="calibration!bias in"} was nearly three times as wide. The result repeats the :index[overconfidence] that Ackerman:index{term="Ackerman, Rakefet"} and Goldsmith:index{term="Goldsmith, Morris"} (2011) found in students who read on screen and set their own study time.:index{term="self-regulated study"} Such readers stop once they judge a text understood, so overconfidence cuts their study short. Paper readers also turned back:index{term="look-backs"} almost twice as often as screen readers scrolled back:index{term="scrolling"}, most often just before an inferential question.:index{term="comprehension!inferential"} In the interviews:index{term="interviews"}, eleven of the sixteen :index[participants] remembered:index{term="memory"} where on a page:index{term="spatial memory"} an idea had been (“top left, next to the diagram”), and three gave up looking for a passage on the :index[tablet] because “it could have been anywhere”. Liu:index{term="Liu, Ziming"} (2005) described a drift towards :index[browsing] and :index[keyword spotting] on screen; these readers went through the whole text but had fewer :index[landmarks] to return to. ## Implications for teaching and design For short texts and factual questions, screens serve as well as paper.:index{term="teaching"} For long expository:index{term="expository text"} texts that students must understand:index{term="comprehension"} rather than search, paper remains the safer choice.:index{term="paper advantage"} Where it is not available, students should test their understanding instead of trusting their sense of it: in the :index[pilot sessions], a short :index[self-test] after reading halved the overconfidence:index{term="overconfidence"} on screen.:index{term="calibration!on screen"} Readers also used the fixed position of text on a page as a map,:index{term="landmarks"} one of the uses of paper that Sellen:index{term="Sellen, Abigail J."} and Harper:index{term="Harper, Richard H. R."} (2002) observed in offices.:index{term="offices, paper in"} Reading applications:index{term="design of reading software"} that keep a stable page and show the reader’s place in the whole text may restore some of that map. ## Limitations and further work The participants:index{term="participants"} were students at one university:index{term="university, single"} who read English fluently,:index{term="limitations"} and the medium matters more for some readers, texts and tasks than it does for others (Singer:index{term="Singer, Lauren M."} & Alexander:index{term="Alexander, Patricia A."}, 2017). The texts were expository and about 1,800 words long,:index{term="texts, length of"} and the screen condition used a single tablet.:index{term="tablet"} A :index[replication] with a larger sample, several devices and the eye-movement:index{term="eye movements"} recording reviewed by Rayner:index{term="Rayner, Keith"} (1998) would show where on the page the two media part company. Huey:index{term="Huey, Edmund Burke"} (1908) thought that a complete analysis of what we do when we read would be almost the acme of a psychologist’s achievements. The experiments reported here add a small part to that analysis; the replication proposed above could measure how far readers rely on the position of a passage on the page when they look back. # Interview guide {style="appendix" letter="A"} The interviews:index{term="interviews"} took place within a week of each participant’s:index{term="participants"} second session. They were audio-recorded,:index{term="interviews!recording of"} transcribed:index{term="transcription"} in full and analysed thematically:index{term="thematic analysis"} following Braun:index{term="Braun, Virginia"} and Clarke:index{term="Clarke, Victoria"} (2006); :ref{id="session-plan" style="full"} gives their timing:index{term="interviews!timing of"}: a free recall:index{term="recall"} of the two study texts, the questions below and a short debriefing.:index{term="debriefing"} The questions were asked in this order,:index{term="interviews!questions asked"} and a prompt:index{term="interviews!prompts in"} only when the participant had not already covered its point. 1. Tell me about the last long text:index{term="texts, length of"} you read for a course.:index{term="courses, reading for"} - Where did you read it, and on paper or on a screen? 2. Which of the texts in this study do you remember:index{term="memory"} best, and why? 3. When you wanted to check an earlier passage, what did you do?:index{term="look-backs"} - How did you know where to look? 4. How sure were you of your answers?:index{term="confidence"} What made you more or less sure? 5. Did reading on the tablet:index{term="tablet"} feel different from reading on paper? 6. Some students say they read more carefully on paper. Do you? 7. What would the ideal way to read a long text for study be like?:index{term="design of reading software"} # Glossary {style="glossary" note="Words in italics are defined under entries of their own."} :::paragraphs{style="term"} **calibration**:index{term="calibration" main} The agreement between a reader’s confidence in having understood a text and the accuracy of that understanding, measured here as the difference between predicted and actual scores. **comprehension, inferential**:index{term="comprehension!inferential" main} Understanding that requires the reader to connect information from different parts of a text or to add knowledge the text does not state. **comprehension, literal**:index{term="comprehension!literal" main} Understanding of what a single sentence or passage states directly. **confidence judgement**:index{term="confidence!judgements of" main} A reader’s estimate, made after reading and before seeing the questions, of how many answers will be correct. **convergent design**:index{term="convergent design" main} A mixed-methods design in which quantitative and qualitative data are collected in the same period, analysed separately and then compared. **expository text**:index{term="expository text" main} A text written to explain or inform, such as a textbook chapter or a report, as opposed to a narrative text. **fixation**:index{term="fixation" main} A pause of the eyes, typically about a quarter of a second, during which the reader takes in text; fixations alternate with *saccades*. **look-back**:index{term="look-backs" main} Any return to an earlier part of a text during reading: turning back a page, scrolling up or following a link to a previous section. **metacomprehension**:index{term="metacomprehension" main} A reader’s knowledge and monitoring of their own understanding of a text; *calibration* is one of its measures. **navigation**:index{term="navigation" main} The movements a reader makes through a text as a whole, as distinct from the movements of the eyes along a line. **overconfidence**:index{term="overconfidence" main} Positive *calibration* bias: predicting a higher score than the one actually obtained. **saccade**:index{term="saccade" main} A rapid movement of the eyes from one *fixation* to the next, during which little or no text is taken in. **screen inferiority effect**:index{term="screen inferiority effect" main} The finding that comprehension of the same text is lower on screen than on paper, most consistently for *expository texts* read under time pressure. **self-regulated study**:index{term="self-regulated study" main} Reading in which the reader, not the experimenter, decides how long to spend on a text. **spatial memory for text**:index{term="spatial memory" main} Memory of where on a page or in a document a piece of information appeared, used as a cue for *look-backs*. **thematic analysis**:index{term="thematic analysis" main} A method for identifying, analysing and reporting patterns of meaning across qualitative data such as interview transcripts. **within-subjects design**:index{term="within-subjects design" main} An experimental design in which every participant takes part in every condition, here reading on both paper and screen. ::: # References {style="references" note="Every work cited in the thesis, set in APA style (7th edition)."} :::paragraphs{style="reference"} Ackerman, R., & Goldsmith, M. (2011). Metacognitive regulation of text learning: On screen versus on paper. *Journal of Experimental Psychology: Applied, 17*(1), 18–32. Baron, N. S. (2015). *Words onscreen: The fate of reading in a digital world.* Oxford University Press. Braun, V., & Clarke, V. (2006). Using thematic analysis in psychology. *Qualitative Research in Psychology, 3*(2), 77–101. Clinton, V. (2019). Reading from paper compared to screens: A systematic review and meta-analysis. *Journal of Research in Reading, 42*(2), 288–325. Creswell, J. W., & Plano Clark, V. L. (2018). *Designing and conducting mixed methods research* (3rd ed.). SAGE. Delgado, P., Vargas, C., Ackerman, R., & Salmerón, L. (2018). Don’t throw away your printed books: A meta-analysis on the effects of reading media on reading comprehension. *Educational Research Review, 25*, 23–38. Dillon, A. (1992). Reading from paper versus screens: A critical review of the empirical literature. *Ergonomics, 35*(10), 1297–1326. Huey, E. B. (1908). *The psychology and pedagogy of reading.* Macmillan. Liu, Z. (2005). Reading behavior in the digital environment: Changes in reading behavior over the past ten years. *Journal of Documentation, 61*(6), 700–712. Mangen, A., Walgermo, B. R., & Brønnick, K. (2013). Reading linear texts on paper versus computer screen: Effects on reading comprehension. *International Journal of Educational Research, 58*, 61–68. Noyes, J. M., & Garland, K. J. (2008). Computer- vs. paper-based tasks: Are they equivalent? *Ergonomics, 51*(9), 1352–1375. Paterson, D. G., & Tinker, M. A. (1940). *How to make type readable.* Harper & Brothers. Rayner, K. (1998). Eye movements in reading and information processing: 20 years of research. *Psychological Bulletin, 124*(3), 372–422. Sellen, A. J., & Harper, R. H. R. (2002). *The myth of the paperless office.* MIT Press. Singer, L. M., & Alexander, P. A. (2017). Reading on paper and digitally: What the past decades of empirical research reveal. *Review of Educational Research, 87*(6), 1007–1041. Tinker, M. A. (1963). *Legibility of print.* Iowa State University Press. Wolf, M. (2018). *Reader, come home: The reading brain in a digital world.* Harper. ::: # Index {style="index" note="Bold numbers refer to the definitions in the glossary."} :::index :::callout{type="colophon"} Set in Libertinus Serif, Libertinus Serif Display and Libertinus Sans (SIL Open Font License). Text: original, CC BY 4.0. The thesis, its author, its participants and its results are fictional; the works in the references are real. :::`; // content.<lang>.md, inlined by the Cookbook // A table from rows of 'cell|cell|cell'; aligns has a letter a column, l or r. const table = (id, typeId, caption, note, widths, aligns, rows) => ({ id, typeId, kind: 'table', caption, note, createdAt: 0, updatedAt: 0, table: { model: { headerRowCount: 1, columnWidths: widths, rows: rows.map((row, r) => row.split('|').map((content, c) => ({ content, isHeader: r === 0, align: aligns[c] === 'r' ? 'right' : 'left' }))) } } }); const resources = [ table('findings', 'table', 'Main results by medium', 'Means for 48 participants. Bias is the predicted minus the actual score.', [5, 1.4, 1.4], 'lrr', ['Measure|Paper|Screen', 'Literal comprehension (of 10)|7.8|7.7', 'Inferential comprehension (of 10)|6.4|5.6', 'Predicted score (%)|75|78', 'Actual score (%)|71|66.5', 'Calibration bias (points)|+4.0|+11.5', 'Look-backs per text|5.8|3.1']), table('session-plan', 'table-a', 'Timing of an interview session', undefined, [1, 6, 1.4], 'llr', ['Part|Content|Minutes', '1|Welcome, consent and a check of the recorder|3', '2|Free recall of the two study texts|5', '3|Questions 1–4: reading habits, look-backs and confidence|12', '4|Questions 5–7: the two media and an ideal design|12', '5|Debrief|3']), ]; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { 'Libertinus Serif': ['400', '400i', '700'], 'Libertinus Serif Display': ['400'], 'Libertinus Sans': ['700'] }; // every face the pages use, loaded first (gotcha: fonts-first) // ─── 4 · Build & show ─────────────────────────────────────────────────────── // The thesis's sixth and last chapter opens on page 171, a recto. const continuation = { pageIndexOffset: 170, pageNumbering: { startAt: 171 }, headings: { h1: 5 } }; await loadFonts(FONTS, markdown); // The index is laid out again until its page numbers settle, inside this one call. const doc = await buildWithFonts( () => buildDocument({ markdown, resources, continuation }, config()), markdown); showPages(doc, { title: 'Reading on Screens and Paper: the back matter' }); offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider }), `${RECIPE}.pdf`);工具包 · core, fonts, viewer, pdf:每道食谱都相同 · 275行
// ─── 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 v1 ── the same in every recipe · postext.dev/cookbook ──────── // Postext measures text with the faces the browser has loaded, and caches the // widths, so every face must be ready before the first build. Faces come from // Fontsource: the same static files the PDF embeds, so screen and PDF agree. /** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. 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', }; const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin']; const jobs = []; let added = 0; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const meta = optional ? await fontsourceMeta(family) : null; for (const spec of new Set(specs)) { const weight = parseInt(spec, 10); const style = spec.endsWith('i') ? 'italic' : 'normal'; if (hasFace(family, weight, style)) continue; 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` (a buildDocument or buildBundle call) and checks the faces * the pages use. A regular face missing from FONTS is loaded with a warning; * bold and italic variants are loaded when the family ships them. Then the * measurement caches are cleared and the build runs 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; its * bold, italic and bold-italic variants are listed whether or not used. */ 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' }; } /** True when a loaded FontFace covers exactly this family, weight and style * (document.fonts.check() is also true 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; } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), 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 ─────── /** Shows the pages as facing spreads on a dark desk: the first page is a * recto on its own, then verso | recto pairs, as in a bound book. Pages * are painted when they scroll near the screen. */ 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 v1 ── the same in every recipe that exports a PDF ────────────── /** postext-pdf embeds TrueType bytes. Fetch the Fontsource file the screen * used, snapping to a weight the family ships and falling back to upright * when it has no italic: the PDF asks for every face a block could use. */ async function fontsourceProvider(family, weight, style) { 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 res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${w}-${s}.woff2`); if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} (${res.status})`); return decompressWoff2(new Uint8Array(await res.arrayBuffer())); } /** A "Build the PDF" button in the bar. Once built: "Open the PDF" (a new * tab, since CodePen's preview frame cannot show 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 ───────────────────────────────────────────────────────────────────────
组合好的script.js可以直接运行:把它粘贴到任何页面的模块脚本中,或在CodePen上打开这道食谱。 GitHub上的食谱文件夹 ↗
变化
#让每一节都从右页开始
送交图书馆装订的论文常让每一节从右页开始。这样术语表、参考文献和索引会移到第175、177和179页,每一节前面都有一个空白左页,索引中的粗体页码指向第175页。
-const backMatter = (id, extra) => ({ id, numbered: false, breakBefore: { enabled: true,
- parity: 'any' }, advancedDesign: opener('Back matter'), header: sectionHeads, ...extra });
+const backMatter = (id, extra) => ({ id, numbered: false, breakBefore: { enabled: true,
+ parity: 'odd' }, advancedDesign: opener('Back matter'), header: sectionHeads, ...extra });#把论文的前面部分也排出来
在第1页之前、用罗马数字计页的前置部分,见前置部分用罗马数字页码,然后从第1页开始。
常见问题
易错点
标题样式会继承其级别的分页设置
headingStyles中的条目没有写出的字段都取自它所属的标题级别,breakBefore也不例外。在:::pagebreak之后以H1设置样式的目录页或版权页会继承parity 'odd',结果落在一张空白页之后。给这类样式设置breakBefore: { enabled: false }。 标题样式 →
易错点
传入任何headings对象都会关掉H1换页
默认情况下,H1换页到右页(always-odd),但只要传入headings对象,这个默认值就会被重置,于是各章接排,span: 'page'也不起作用。在每份配置中重新写明headings.levels[0].breakBefore: { enabled: true, parity }。 从右页开始的章 →
易错点
替换调色板时,设计元素和引用颜色不会跟着变
postext 1.4.1把colorPalette读入文字样式(正文、标题、列表、题注、表格、框),但不读入页眉、页脚、章首页和篇章页的元素,也不读入bodyText.referenceColor:它们保留写在paletteId旁边的十六进制颜色。替换调色板时(例如做深色屏幕版或换色),在构建前根据colorPalette重写每一个关联的颜色。 语义调色板 →
易错点
PDF会请求每个字族的所有字重和样式
renderToPdf会向字体提供函数请求任何块可能用到的每个字族的粗体、斜体和粗斜体,哪怕从来没有印出来,只要有一次请求被拒绝,导出就会中止。提供函数必须就近匹配该字族实际提供的字重,没有斜体时退回正体。 嵌入PDF的字体 →
易错点
frontmatter的每个值都加引号
YAML会把title: 1984读成数字,把日期读成Date对象;非字符串的值在占位符中打印为空,PDF也会没有标题。每个值都加引号:title: "1984"。 文档元数据 →
易错点
配置按对象身份缓存:每次新建一个对象
引擎按对象身份缓存解析后的配置,所以就地修改配置再构建,会复用旧的结果。每次构建都新建一个对象,这也是食谱的配置写成工厂函数config()的原因。 在Canvas上绘制页面 →
易错点
排版前加载所有字体
排版用浏览器已加载的字体测量文字,并缓存宽度,所以首次构建之后才到的字体会造成断行错误,PDF也不再与屏幕一致。先加载所有字重和样式;有字体迟到时,重新构建前调用clearMeasurementCache()。 排版前加载字体 →
致谢
- 文本
- 原创文字, CC BY 4.0
- 字体
- Libertinus Serif (SIL OFL 1.1) · Libertinus Serif Display (SIL OFL 1.1) · Libertinus Sans (SIL OFL 1.1)


