成品一览
这是Smyth海军上将《Sailor’s Word-Book》(1867)新版中的四页,收A到C三个字母,页面尺寸为150 × 200 mm。词条排成两栏两端对齐的8 pt Alegreya,栏间有一条细线;每个词头为藏青色粗体,每条的转行悬挂缩进1 em。一个字母在栏内开始,用一个四行高的藏青色首字母加一道黄铜色细线标出,词条接着排下去,不另起一页。每一页的书口都印着完整的书边索引,共22个浅色标签。当前字母的标签为藏青色,比其他标签宽3.2 mm,每换一个字母就下移一格。西班牙文版保留Smyth的英文词头,翻译了释义。
这道食谱解答
- 怎样做书边索引:切口上的标签随着字母或章逐页下移?
- 怎样排参考文献或术语表(悬挂缩进、较小字号)?
- 怎样给正文或框里的关键词(粗体或斜体)着色?
- 怎样设置书眉:左页放书名,右页放章名,页码在外侧?
- 怎样在章首页和空白页上隐藏书眉,或者把空白左页涂成篇的颜色?
简短回答
// 22 tabs share the 44 lines of the text block, two lines each; letters with few words
// share a tab, as in most thumb-indexed dictionaries.
const TABS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'QR', 'S', 'T', 'UV', 'W', 'XYZ'];
const STEP = 2 * LEAD * PT; // mm: 7.34
const EDGE = { odd: 'top-right', even: 'top-left' }; // the fore-edge: right on a recto
// A cell of the index, `width` mm inside the trim and 3 mm past it, so that only its
// inner corners show their rounding; its label is centred on the part inside the trim.
const cell = (id, label, i, parity, width, fill, ink, size) => {
const y = mm(TOP + i * STEP + 0.3); // a 0.6 mm gap between neighbours
const at = (x, w) => ({ anchor: { to: 'page', edge: EDGE[parity] }, offset: { x: mm(x), y },
size: { width: mm(w), height: mm(STEP - 0.6) } });
return [
{ kind: 'box', id, parity, style: { backgroundColor: col(fill), borderRadius: mm(1.2) },
placement: at(parity === 'odd' ? 3 : -3, width + 3) },
{ kind: 'text', id: `${id}-label`, parity, content: label, fontFamily: LABEL, fontWeight: 700,
fontSize: pt(size), lineHeight: 1, color: col(ink), align: 'center',
verticalAlign: 'middle', placement: at(0, width) },
];
};
const sides = (make) => ['odd', 'even'].flatMap(make);
// Every page prints the whole index, pale; the section's own letter stands out of it.
const ladder = sides((p) => TABS.flatMap((label, i) =>
cell(`index-${i}-${p}`, label, i, p, 5.8, 'tint', 'slate', 7.5)));
const letterStyles = () => TABS.flatMap((label, i) => [...label].map((letter) => ({
id: letter,
// '# B {style="B"}' opens the section of B: its pages take this header, and a page
// where A ends and B begins takes B's (gotcha: section-last-wins).
header: { elements: [...runningHeads, ...ladder,
...sides((p) => cell(`tab-${p}`, label, i, p, 9, 'navy', 'paper', 8.5))] },
})));
用料
做法
#1 · 给每个字母一种带自己标签的标题样式
代码就是上面的简短回答。TABS列出22个标签,少见的字母合用一个(QR、UV、XYZ)。每个标签高两行正文(7.34 mm),加起来正好填满版心的44行。循环为每个字母生成一种标题样式,其header在书眉和浅色索引之外,加上该字母的藏青色标签,位置比上一个低一格(标题样式)。每个单元格写两次,每种parity一次,并且锚定到页面外侧边缘,而不是页眉自身的容器。那个容器是上页边距区域,宽度与版心相同,从裁切边延伸到正文,而标签需要伸到书口,并且纵贯全部44行。一页采用最后一个在该页开始的部分的页眉,所以第2页从A开始,却显示B的标签。
#2 · 第一页不印书眉
const HEAD = 11; // mm from the trim's top to the heads' top
const head = (id, content, parity, edge, x, extra) => ({ kind: 'text', id, content, parity,
pages: 'body', fontFamily: LABEL, fontSize: pt(8), fontWeight: 500, letterSpacing: pt(1),
color: col('muted'),
placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(HEAD) } }, ...extra });
const folio = { fontWeight: 700, color: col('ink'), letterSpacing: pt(0) };
const runningHeads = [
head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
head('verso-title', '{title}', 'even', 'top-left', OUTER + 8),
head('recto-letter', t({ en: 'Letter {chapterTitle}', es: 'Letra {chapterTitle}' }), 'odd',
'top-right', -(OUTER + 8)),
head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
];
// The first page drops its folio to the foot, under the text block.
const footer = { elements: [head('drop-folio', '{pageNumber}', 'all', 'top', 0, {
...folio, pages: 'opener',
placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(6) } } })] };
书眉是锚定在页面上的文本元素,用parity和pages: 'body'筛选(文本元素)。第1页的书名横跨两栏,使第1页成为章首页。书眉会跳过它,它的页码来自页脚:一个设为pages: 'opener'的元素,印在版心下方6 mm处。在右页上,{chapterTitle}印出该页或此前最后一个一级标题的字母,即Letter C。从postext 1.5.0起,{firstMark.<key>}和{lastMark.<key>}可以印出一页的第一个和最后一个词头,也就是大多数词典都有的书眉词;本食谱是用1.4.1排的,保留了字母。
#3 · 只给词头上色
// Bold and italic default to the engine's blue: the body sets them to ink, and only the
// entry style prints its bold, the headwords, in navy.
const BODY = 8; // pt: a reference size, about 50 characters to the 60 mm column
const bodyText = { fontFamily: TEXT, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'),
boldColor: col('ink'), italicColor: col('ink'), firstLineIndent: mm(0),
minWordSpacing: 0.7, maxWordSpacing: 1.7,
maxRuntTracking: 0 }; // gotcha: runt-tracking-unpainted
const paragraphStyles = [
{ id: 'entry', hangingIndent: em(1), boldColor: col('navy') }, // :::paragraphs{style="entry"}
{ id: 'colophon', fontSize: pt(7), color: col('muted'), textAlign: 'center',
marginTop: pt(LEAD) },
];
每个词条是:::paragraphs{style="entry"}块中的一个段落,转行悬挂缩进1 em(段落样式)。正文的boldColor让第1页上的**Note.**保持墨色,词条样式的boldColor把词头印成藏青色。段落样式没有italicColor,所以斜体的子词条保持正文的墨色。
#4 · 在栏内放一个四行下沉的首字母
// Its cap line meets the first body line's and it stands on the fourth baseline. Cap
// heights measured on the glyph H: 0.652 em in Alegreya SC 900, 0.646 em in Alegreya.
const DROP = 4; // body lines the letter spans
const LETTER = ((DROP - 1) * LEAD + 0.646 * BODY) / 0.652; // pt: 55.8
// A design line sets its baseline 0.8 of its height below its top; a body line does too.
const letterHead = { enabled: true, slot: { elements: [
{ kind: 'text', id: 'letter', content: '{titleText}', fontFamily: DISPLAY, fontWeight: 900,
fontSize: pt(LETTER), lineHeight: 1, color: col('navy'), align: 'left',
placement: { anchor: { to: 'container', edge: 'top-left' },
offset: { y: pt((DROP - 0.2) * LEAD - 0.8 * LETTER) } } },
{ kind: 'rule', id: 'hairline', direction: 'horizontal', thickness: pt(0.6), color: col('brass'),
placement: { anchor: { to: 'container', edge: 'top-left' },
offset: { y: pt((DROP + 0.55) * LEAD) }, size: { width: 'fill' } } },
] } };
LETTER为55.8 pt:在这个字号下,Alegreya SC 900的大写字母(高0.652 em)立在第四条基线上,顶端正好到达第一行正文的大写字母高度线。字母和黄铜色细线在第五行之内结束,标题块向下对齐到下一条网格线,所以每个字母标题占五行,下面的词条仍在网格上。在第3页上,第二栏顶部的C与第一栏的前四行平齐。一级标题设置breakBefore: { enabled: false },各字母样式继承这一设置。在1.4.1中,只要有headings对象,一级标题前就已经不分页;把这个值明确写出来,即使以后的版本恢复了分页,字母也会继续接排。
#5 · 让书名横跨两栏
const centred = (id, content, family, size, lineHeight, y, width, extra) => ({ kind: 'text', id,
content, fontFamily: family, fontSize: pt(size), lineHeight, color: col('navy'), align: 'center',
overflow: 'wrap', ...extra, placement: { anchor: { to: 'container', edge: 'top' },
offset: { y: mm(y) }, size: { width: width ? mm(width) : 'fill' } } });
const rule = (id, y, thickness) => ({ kind: 'rule', id, direction: 'horizontal',
thickness: pt(thickness), color: col('navy'), placement: { anchor: { to: 'container',
edge: 'top-left' }, offset: { y: mm(y) }, size: { width: 'fill' } } });
const titleBand = { enabled: true, slot: { elements: [
// The column rule starts at the top of the text block, under the band too: a field of
// paper down to the thin rule covers it, so it hangs from the double rule.
{ kind: 'box', id: 'field', style: { backgroundColor: col('paper') }, placement: {
anchor: { to: 'container', edge: 'top-left' }, size: { width: 'fill', height: mm(45) } } },
{ kind: 'image', id: 'anchor', resourceId: 'anchor', placement: {
anchor: { to: 'container', edge: 'top' }, size: { width: 'auto', height: mm(18) } } },
centred('name', '{titleText}', DISPLAY, 22, 1.1, 20, 0, { fontWeight: 900 }),
centred('subtitle', '{attr.subtitle}', TEXT, 9, 1.25, 29.5, 92, { italic: true,
color: col('ink') }),
centred('byline', '{attr.byline}', LABEL, 7, 1.2, 38.8, 0, { fontWeight: 500,
letterSpacing: pt(1.2), color: col('muted') }),
rule('thick', 43.6, 1.2),
rule('thin', 45, 0.4),
] } };
// '# The Sailor’s Word-Book {style="title" subtitle="…" byline="…"}' opens page 1. The band's
// {titleText} joins the hidden heading's wrapped lines with a space, and a page-span heading
// wraps at the column width: at the default H1 size the band printed 'Word- Book'. At body
// size the hidden title fits one line of the 60 mm column.
const titleStyle = { id: 'title', span: 'page', advancedDesign: titleBand, fontSize: pt(BODY) };
书名样式横跨整页,是一条色带,里面有锚、书名、从标题属性读取的副标题和作者署名,以及一道双线(通栏与高级版面设计)。细线结束在版心顶部下方45.1 mm处,所以色带对齐到13个网格行,即47.7 mm。在通栏标题下,栏间线仍从版心顶部开始,会穿过书名。一个纸色的框把色带一直盖到细线,栏间线只在它下方显示。{titleText}用空格连接隐藏标题折行后的各行,而1.4.1按栏宽为通栏标题折行。按默认的H1字号,色带上印出的是Word- Book,所以书名样式把隐藏标题设为正文字号,这样书名能在60 mm的栏宽里排成一行。
完整食谱
// ═══ Postext Cookbook · Nº 046 · Dictionary with a moving thumb index ═══════════════ // https://postext.dev/en/cookbook/dictionary-thumb-index // Code: MIT · Text: W. H. Smyth, 1867 (PD); Spanish translation CC BY 4.0 · Pictures: code // Fonts: Alegreya, Alegreya SC, Alegreya Sans SC (SIL OFL 1.1) · Needs postext ≥ 1.4.1 import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerResourceImage, } from 'https://esm.sh/postext'; const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'dictionary-thumb-index'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: ink and navy on a warm paper, brass for the rules and the rope const palette = { ink: '#1b1f24', // text navy: '#1f3a5f', // headwords, letters and tabs brass: '#a4834f', // hairlines and the rope of the vignette rule: '#cfccc3', // the column rule tint: '#e4e9f0', // the pale cells of the index slate: '#56657b', // their letters muted: '#62666b', // running heads paper: '#fbfaf6', // the page, and the letter reversed out of each tab }; const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id }); const colorPalette = Object.entries(palette) .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })); // #endregion const TEXT = 'Alegreya', DISPLAY = 'Alegreya SC', LABEL = 'Alegreya Sans SC'; const PT = 25.4 / 72; // mm in a point const LEAD = 10.4; // pt: the body's leading, the grid every column is set on const LINES = 44; // lines in a full column const TOP = 18, INNER = 12, OUTER = 14; // mm; the foot margin makes the block whole lines const BOTTOM = 200 - TOP - LINES * LEAD * PT; // #region answer: one heading style per letter, each with its tab one step down the fore-edge // 22 tabs share the 44 lines of the text block, two lines each; letters with few words // share a tab, as in most thumb-indexed dictionaries. const TABS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'QR', 'S', 'T', 'UV', 'W', 'XYZ']; const STEP = 2 * LEAD * PT; // mm: 7.34 const EDGE = { odd: 'top-right', even: 'top-left' }; // the fore-edge: right on a recto // A cell of the index, `width` mm inside the trim and 3 mm past it, so that only its // inner corners show their rounding; its label is centred on the part inside the trim. const cell = (id, label, i, parity, width, fill, ink, size) => { const y = mm(TOP + i * STEP + 0.3); // a 0.6 mm gap between neighbours const at = (x, w) => ({ anchor: { to: 'page', edge: EDGE[parity] }, offset: { x: mm(x), y }, size: { width: mm(w), height: mm(STEP - 0.6) } }); return [ { kind: 'box', id, parity, style: { backgroundColor: col(fill), borderRadius: mm(1.2) }, placement: at(parity === 'odd' ? 3 : -3, width + 3) }, { kind: 'text', id: `${id}-label`, parity, content: label, fontFamily: LABEL, fontWeight: 700, fontSize: pt(size), lineHeight: 1, color: col(ink), align: 'center', verticalAlign: 'middle', placement: at(0, width) }, ]; }; const sides = (make) => ['odd', 'even'].flatMap(make); // Every page prints the whole index, pale; the section's own letter stands out of it. const ladder = sides((p) => TABS.flatMap((label, i) => cell(`index-${i}-${p}`, label, i, p, 5.8, 'tint', 'slate', 7.5))); const letterStyles = () => TABS.flatMap((label, i) => [...label].map((letter) => ({ id: letter, // '# B {style="B"}' opens the section of B: its pages take this header, and a page // where A ends and B begins takes B's (gotcha: section-last-wins). header: { elements: [...runningHeads, ...ladder, ...sides((p) => cell(`tab-${p}`, label, i, p, 9, 'navy', 'paper', 8.5))] }, }))); // #endregion // #region running-heads: the book on the verso, the letter on the recto, folios outside const HEAD = 11; // mm from the trim's top to the heads' top const head = (id, content, parity, edge, x, extra) => ({ kind: 'text', id, content, parity, pages: 'body', fontFamily: LABEL, fontSize: pt(8), fontWeight: 500, letterSpacing: pt(1), color: col('muted'), placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(HEAD) } }, ...extra }); const folio = { fontWeight: 700, color: col('ink'), letterSpacing: pt(0) }; const runningHeads = [ head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio), head('verso-title', '{title}', 'even', 'top-left', OUTER + 8), head('recto-letter', t({ en: 'Letter {chapterTitle}', es: 'Letra {chapterTitle}' }), 'odd', 'top-right', -(OUTER + 8)), head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio), ]; // The first page drops its folio to the foot, under the text block. const footer = { elements: [head('drop-folio', '{pageNumber}', 'all', 'top', 0, { ...folio, pages: 'opener', placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(6) } } })] }; // #endregion // #region entries: one paragraph per entry, the headword bold in navy, the turnovers hanging // Bold and italic default to the engine's blue: the body sets them to ink, and only the // entry style prints its bold, the headwords, in navy. const BODY = 8; // pt: a reference size, about 50 characters to the 60 mm column const bodyText = { fontFamily: TEXT, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), firstLineIndent: mm(0), minWordSpacing: 0.7, maxWordSpacing: 1.7, maxRuntTracking: 0 }; // gotcha: runt-tracking-unpainted const paragraphStyles = [ { id: 'entry', hangingIndent: em(1), boldColor: col('navy') }, // :::paragraphs{style="entry"} { id: 'colophon', fontSize: pt(7), color: col('muted'), textAlign: 'center', marginTop: pt(LEAD) }, ]; // #endregion // #region letter-heads: a four-line initial over a brass hairline, in the column, running on // Its cap line meets the first body line's and it stands on the fourth baseline. Cap // heights measured on the glyph H: 0.652 em in Alegreya SC 900, 0.646 em in Alegreya. const DROP = 4; // body lines the letter spans const LETTER = ((DROP - 1) * LEAD + 0.646 * BODY) / 0.652; // pt: 55.8 // A design line sets its baseline 0.8 of its height below its top; a body line does too. const letterHead = { enabled: true, slot: { elements: [ { kind: 'text', id: 'letter', content: '{titleText}', fontFamily: DISPLAY, fontWeight: 900, fontSize: pt(LETTER), lineHeight: 1, color: col('navy'), align: 'left', placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: pt((DROP - 0.2) * LEAD - 0.8 * LETTER) } } }, { kind: 'rule', id: 'hairline', direction: 'horizontal', thickness: pt(0.6), color: col('brass'), placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: pt((DROP + 0.55) * LEAD) }, size: { width: 'fill' } } }, ] } }; // #endregion // #region title: the book's name across both columns, under a fouled anchor const centred = (id, content, family, size, lineHeight, y, width, extra) => ({ kind: 'text', id, content, fontFamily: family, fontSize: pt(size), lineHeight, color: col('navy'), align: 'center', overflow: 'wrap', ...extra, placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(y) }, size: { width: width ? mm(width) : 'fill' } } }); const rule = (id, y, thickness) => ({ kind: 'rule', id, direction: 'horizontal', thickness: pt(thickness), color: col('navy'), placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: mm(y) }, size: { width: 'fill' } } }); const titleBand = { enabled: true, slot: { elements: [ // The column rule starts at the top of the text block, under the band too: a field of // paper down to the thin rule covers it, so it hangs from the double rule. { kind: 'box', id: 'field', style: { backgroundColor: col('paper') }, placement: { anchor: { to: 'container', edge: 'top-left' }, size: { width: 'fill', height: mm(45) } } }, { kind: 'image', id: 'anchor', resourceId: 'anchor', placement: { anchor: { to: 'container', edge: 'top' }, size: { width: 'auto', height: mm(18) } } }, centred('name', '{titleText}', DISPLAY, 22, 1.1, 20, 0, { fontWeight: 900 }), centred('subtitle', '{attr.subtitle}', TEXT, 9, 1.25, 29.5, 92, { italic: true, color: col('ink') }), centred('byline', '{attr.byline}', LABEL, 7, 1.2, 38.8, 0, { fontWeight: 500, letterSpacing: pt(1.2), color: col('muted') }), rule('thick', 43.6, 1.2), rule('thin', 45, 0.4), ] } }; // '# The Sailor’s Word-Book {style="title" subtitle="…" byline="…"}' opens page 1. The band's // {titleText} joins the hidden heading's wrapped lines with a space, and a page-span heading // wraps at the column width: at the default H1 size the band printed 'Word- Book'. At body // size the hidden title fits one line of the 60 mm column. const titleStyle = { id: 'title', span: 'page', advancedDesign: titleBand, fontSize: pt(BODY) }; // #endregion const config = () => ({ // a factory, never a shared object (gotcha: config-cache-identity) locale: t({ en: 'en-us', es: 'es' }), // exact codes (gotcha: hyphenation-locales) colorPalette, footer, header: { elements: [] }, // every page takes the header of its letter's style page: { sizePreset: 'custom', width: mm(150), height: mm(200), dpi: 150, backgroundColor: col('paper'), margins: { top: mm(TOP), bottom: mm(BOTTOM), left: mm(INNER), right: mm(OUTER), mirror: true } }, layout: { gutterWidth: mm(4), // two columns is the default layout columnRule: { enabled: true, color: col('rule'), lineWidth: pt(0.4) } }, bodyText, paragraphStyles, // The designs paint every heading; the hidden ones are still measured, and in the face // FONTS loads, or the kit would fetch Open Sans 700 for text nobody sees. headings: { fontFamily: DISPLAY, fontWeight: 900, levels: [ // Letters run on in the column. Any headings object already drops the H1 break // (gotcha: headings-drop-h1-break); stating it keeps them running on if that default // returns, and the letter styles inherit it (gotcha: style-inherits-break). { level: 1, breakBefore: { enabled: false }, marginTop: pt(LEAD), marginBottom: pt(0), advancedDesign: letterHead }, ] }, headingStyles: [ titleStyle, ...letterStyles(), ], }); // #region art: a fouled anchor, the Admiralty's badge, for the title band // Drawn in the page's navy and brass; the rope's lay is a dashed navy stroke over it. const anchorSvg = () => { const { navy, brass } = palette; const rope = 'M66.5 19 C88 26 86 44 60 52 C34 60 32 76 60 82 C88 88 86 104 60 110 ' + 'C44 114 38 122 42 136'; const fluke = (s) => `<path transform="translate(60 0) scale(${s} 1)" fill="${navy}" d="M-44 92 L-50 78 L-36 70 L-34 88 Z"/>`; return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 150" width="120" height="150"> <circle cx="60" cy="13" r="8.5" fill="none" stroke="${navy}" stroke-width="4"/> <path d="M56.5 21 L63.5 21 L64.5 124 L55.5 124 Z" fill="${navy}"/> <rect x="20" y="27" width="80" height="6.5" rx="3.2" fill="${navy}"/> <circle cx="19" cy="30.2" r="5" fill="${navy}"/> <circle cx="101" cy="30.2" r="5" fill="${navy}"/> <path d="M16 84 Q20 132 60 134 Q100 132 104 84" fill="none" stroke="${navy}" stroke-width="7" stroke-linecap="round"/> ${fluke(1)}${fluke(-1)} <path d="M60 126 L66 134 L60 142 L54 134 Z" fill="${navy}"/> <path d="${rope}" fill="none" stroke="${brass}" stroke-width="4.2" stroke-linecap="round"/> <path d="${rope}" fill="none" stroke="${navy}" stroke-opacity="0.5" stroke-width="4.2" stroke-dasharray="1 1.8"/> </svg>`; }; // #endregion // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Markdown样例 · 118行 · content.en.md
title: "The Sailor’s Word-Book" author: "W. H. Smyth" --- # The Sailor’s Word-Book {style="title" subtitle="An alphabetical digest of nautical terms, including some more especially military and scientific, but useful to seamen" byline="Admiral W. H. Smyth · London, 1867"} **Note.** Admiral William Henry Smyth died on 8 September 1865 and left this word-book in manuscript. Sir Edward Belcher, a vice-admiral, revised it for the press, and Blackie and Son published it in London in 1867. This selection prints entries from the letters A, B and C in Smyth’s wording, some of them shortened, among them words now in everyday English: A1, *taken aback*, *aloof*, the *bitter end*, a wide *berth*, *clear the decks*, *cut and run*. Cross-references follow *see*; the entries they name are not in this selection. # A {style="A"} :::paragraphs{style="entry"} **A.** The highest class of the excellence of merchant ships on Lloyd’s books, subdivided into A1 and A2, after which they descend by the vowels: A1 being the very best of the first class. Formerly a river-built (Thames) ship took the first rate for 12 years, a Bristol one for 11, and those of the northern ports 10. Some of the out-port built ships keep their rating 6 to 8 years, and inferior ones only 4. But improvements in ship-building, and the large introduction of iron, are now claiming longer life. **Aback.** The situation of a ship’s sails when the wind bears against their front surfaces. They are *laid aback*, when this is purposely effected to deaden her way by rounding in the weather-braces; and *taken aback*, when brought to by an unexpected change of wind, or by inattention in the helmsman. – *All aback forward*, the notice given from the forecastle, when the head-sails are pressed aback by a sudden change in the wind. (*See* Work aback.) – *Taken aback*, a colloquialism for being suddenly surprised or found out. **Abeam.** In a line at right angles to the vessel’s length; opposite the centre of a ship’s side. **About.** Circularly; the situation of a ship after she has gone round, and trimmed sails on the opposite tack. – *Ready about!* and *About-ship!* are orders to the ship’s company to prepare for tacking by being at their stations. **Adrift.** Floating at random; the state of a boat or vessel broken from her moorings, and driven to and fro without control by the winds and waves. Cast loose; cut adrift. **Afloat.** Borne up and supported by the water; buoyed clear of the ground; also used for being on board ship. **Ahead.** A term especially referable to any object farther onward, or immediately before the ship, or in the course steered, and therefore opposed to *astern*. – *Ahead of the reckoning*, is sailing beyond the estimated position of the ship. – *Ahead* is also used for progress; as, *cannot get ahead*, and is generally applied to forward, in advance. **A-hull.** A ship under bare poles and her helm a-lee, driving from wind and sea, stern foremost. Also a ship deserted, and exposed to the tempestuous winds. **A-lee.** The contrary of *a-weather*: the position of the helm when its tiller is borne over to the lee-side of the ship, in order to go about or put her head to windward. – *Hard a-lee!* or *luff a-lee!* is said to the steersman to put the helm down. – *Helm’s a-lee!* the word of command given on putting the helm down, and causing the head-sails to shake in the wind. **Aloof.** The old word for “keep your luff,” in the act of sailing to the wind. (*See* Luff.) – *Keep aloof*, at a distance. **Anchor.** A large and heavy instrument in use from the earliest times for holding and retaining ships, which it executes with admirable force. With few exceptions it consists of a long iron shank, having at one end a ring, to which the cable is attached, and the other branching out into two arms, with flukes or palms at their bill or extremity. A stock of timber or iron is fixed at right angles to the arms, and serves to guide the flukes perpendicularly to the surface of the ground. According to their various form and size, anchors obtain the epithets of the *sheet*, *best bower*, *small bower*, *spare*, *stream*, *kedge*, and *grapling* (which see under their respective heads). **Apple-pie order.** A strange but not uncommon term for a ship in excellent condition and well looked to. Neat and orderly. Absurdly said to be a corruption of *du pol au pied*. **Ashore.** Aground, on land. – To *go ashore*, to disembark from a boat. Opposed to *aboard*. **Astern.** Any distance behind a vessel; in the after-part of the ship; in the direction of the stern, and therefore the opposite of *ahead*. – *To drop astern*, is to be left behind – when abaft a right angle to the keel at the main-mast, she drops astern. **Athwart.** The transverse direction; anything extending or across the line of a ship’s course. – *Athwart hawse*, a vessel, boat, or floating lumber accidentally drifted across the stem of a ship, the transverse position of the drift being understood. – *Athwart the fore-foot*, just before the stem; ships fire a shot in this direction to arrest a stranger, and make her bring-to. – *Athwart ships*, in the direction of the beam; from side to side: in opposition to *fore-and-aft*. **Avast.** The order to stop, hold, cease, or stay, in any operation: its derivation from the Italian *basta* is more plausible than *have fast*. **Awning.** A cover or canvas canopy suspended by a crow-foot and spread over a ship, boat, or other vessel, to protect the decks and crew from the sun and weather. (*See* Euphroe.) Also that part of the poop-deck which is continued forward beyond the bulk-head of the cabin. **Azure.** The deep blue colour of the sky, when perfectly cloudless. ::: # B {style="B"} :::paragraphs{style="entry"} **Ballast.** As a verb, signifies to steady; as a substantive, a comprehensive mind. A man is said to “lose his ballast” when his judgment fails him, or he becomes top-heavy from conceit. **Beam-ends.** A ship is said to be on her beam-ends when she has heeled over so much on one side that her beams approach to a vertical position; hence also a person lying down is metaphorically said to be on his beam-ends. **Berth.** The station in which a ship rides at anchor, either alone, or in a fleet; as, she lies in a good berth, *i.e.* in good anchoring ground, well sheltered from the wind and sea, and at a proper distance from the shore and other vessels. – *Snug berth*, a place, situation, or establishment. A sleeping berth. – *To berth a vessel*, is to fix upon, and put her into the place she is to occupy. – *To berth a ship’s company*, to allot to each man the space in which his hammock is to be hung, giving the customary 14 inches in width. – *To give a berth*, to keep clear of, as to give a point of land a wide berth, is to keep at a due distance from it. **Bitter-end.** That part of the cable which is abaft the bitts, and therefore within board when the ship rides at anchor. They say, “Bend to the bitter-end” when they would have that end bent to the anchor, and when a chain or rope is paid out to the bitter-end, no more remains to be let go. The bitter-end is the clinching end – sometimes that end is bent to the anchor, because it has never been used, and is more trustworthy. The first 40 fathoms of a cable of 115 fathoms is generally worn out when the inner end is comparatively new. **Booby.** A well-known tropical sea-bird, *Sula fusca*, of the family *Pelecanidae*. It is fond of resting out of the water at night, even preferring an unstable perch on the yard of a ship. The name is derived from the way in which it allows itself to be caught immediately after settling. The direction in which they fly as evening comes on often shows where land may be found. **Brace of shakes.** A moment: taken from the flapping of a sail. I will be with you before it shakes thrice. **Bring up with a round turn.** Suddenly arresting a running rope by taking a round turn round a bollard, bitt-head, or cleat. Said of doing a thing effectually though abruptly. It is used to bring one up to his senses by a severe rating. **Broach-to, to.** To fly up into the wind. It generally happens when a ship is carrying a press of canvas with the wind on the quarter, and a good deal of after-sail set. The masts are endangered by the course being so altered, as to bring it more in opposition to, and thereby increasing the pressure of the wind. In extreme cases the sails are caught flat aback, when the masts would be likely to give way, or the ship might go down stern foremost. **Broadside.** The whole array, or the simultaneous discharge of the artillery on one side of a ship of war above and below. It also implies the whole of that side of a ship above the water which is situate between the bow and quarter, and is in a position nearly perpendicular to the horizon. Also, a name given to the old folio sheets whereon ballads and proclamations were printed of old (broad-sheet). **By the board.** Over the ship’s side. When a mast is carried away near the deck it is said to go by the board. ::: # C {style="C"} :::paragraphs{style="entry"} **Cabin.** A room or compartment partitioned off in a ship, where the officers and passengers reside. In a man-of-war, the principal cabin, in which the captain or admiral lives, is the upper after-part of the vessel. **Cabin-boy.** A boy whose duty is to attend and serve the officers and passengers in the cabin. **Cable’s length.** A measure of about 100 fathoms, by which the distances of ships in a fleet are frequently estimated. This term is frequently misunderstood. In all marine charts a cable is deemed 607.56 feet, or one-tenth of a sea mile. In rope-making the cable varies from 100 to 115 fathoms; cablet, 120 fathoms; hawser-laid, 130 fathoms, as determined by the admiralty in 1830. **Caboose.** The cook-room or kitchen of merchantmen on deck; a diminutive substitute for the galley of a man-of-war. It is generally furnished with cast-iron apparatus for cooking. **Careen, to.** A ship is said to careen when she inclines to one side, or lies over when sailing on a wind; off her keel or carina. **Cat o’ nine tails.** An instrument of punishment used on board ships in the navy; it is commonly of nine pieces of line or cord, about half a yard long, fixed upon a piece of thick rope for a handle, and having three knots on each, at small intervals, nearest one end; with this the seamen who transgress are flogged upon the bare back. **Caulk, to.** (*See* Caulking.) To lie down on deck and sleep, with clothes on. **Chart.** A hydrographical map, or a projection of some part of the earth’s superficies *in plano*, for the use of navigators, further distinguished as plane-charts, Mercator’s charts, globular charts, and the bottle or current chart, to aid in the investigation of surface currents (all which see). A selenographic chart represents the moon, especially as seen by the aid of photography and Mr. De la Rue’s arrangement. **Cheer, to.** To salute a ship *en passant*, by the people all coming on deck and huzzahing three times; it also implies to encourage or animate. (*See also* Hearty and Man ship!) **Chock-a-block, or chock and block.** Is the same with *block-a-block* and *two-blocks* (which see). When the lower block of a tackle is run close up to the upper one, so that you can hoist no higher, the blocks being together. **Clear, to.** Has several significations, particularly to escape from, to unload, to empty, to prepare, &c., as: – *To clear for action.* To prepare for action. – *To clear away* for this or that, is to get obstructions out of the way. – *To clear the decks.* To remove lumber, put things in their places, and coil down the ropes. Also, to take the things off a table after a meal. – *To clear goods.* To pay the custom-house dues and duties. – *To clear the land.* To escape from the land. – *To clear a lighter, or the hold.* To empty either. **Coil.** A certain quantity of rope laid up in ring fashion. The manner in which all ropes are disposed of on board ship for convenience of stowage. They are laid up round, one fake over another, or by concentric turns, termed *Flemish coil*, forming but one tier, and lying flat on the deck, the end being in the middle of it, as a snake or worm coils itself. **Convoy.** A fleet of merchant ships similarly bound, protected by an armed force. Also, the ship or ships appointed to conduct and defend them on their passage. Also, a guard of troops to escort a supply of stores to a detached force. **Cross-trees.** Certain timbers supported by the cheeks and trestle-trees at the upper ends of the lower and top masts, athwart which they are laid to sustain the frame of the tops on the one, and to extend the top-gallant shrouds on the other. **Cuddy.** A sort of cabin or cook-room, generally in the fore-part, but sometimes near the stern of lighters and barges of burden. In the oceanic traders it is a cabin abaft, under the round-house or poop-deck, for the commander and his passengers. Also, the little cabin of a boat. **Cut and run, to.** To cut the cable for an escape. Also, to move off quickly; to quit occupation; to be gone. ::: :::paragraphs{style="colophon"} Type: Alegreya, Alegreya SC and Alegreya Sans SC (SIL OFL) Text: W. H. Smyth, 1867 (Project Gutenberg eBook 26000) Note and anchor: Postext Cookbook (CC BY 4.0) :::`; // content.<lang>.md, inlined by the Cookbook // No paragraph cites the anchor, so it is never placed as a figure: only the title band's // image element draws it. const resources = [{ id: 'anchor', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0, altText: t({ en: 'An anchor with a rope wound round its shank.', es: 'Un ancla con un cabo enrollado en la caña.' }), svg: { fileId: 'anchor.svg', width: 120, height: 150 } }]; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { Alegreya: ['400', '400i', '700'], 'Alegreya SC': ['900'], 'Alegreya Sans SC': ['500', '700'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); await loadSvg('anchor.svg', anchorSvg()); const doc = await buildWithFonts(() => buildDocument({ markdown, resources }, config()), markdown); showPages(doc, { title: t({ en: 'The Sailor’s Word-Book', es: 'Vocabulario del marinero' }) });工具包 · core, fonts, viewer, images:每道食谱都相同 · 270行
// ─── 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 · images v1 ── recipes with pictures · postext.dev/cookbook ────────── /** Registers a photo or PNG for the canvas and keeps its bytes for the PDF. * fetch → ImageBitmap never taints the canvas (a plain cross-origin <img> would). */ async function loadImage(fileId, url) { const res = await fetch(url); if (!res.ok) throw new Error(`Image not found (${res.status}): ${url}`); const bytes = new Uint8Array(await res.arrayBuffer()); registerResourceImage(fileId, await createImageBitmap(new Blob([bytes]))); (loadImage.bytes ??= new Map()).set(fileId, bytes); } /** Registers SVG markup (drawn in code, or fetched) as a vector image. */ async function loadSvg(fileId, svg) { const img = new Image(); img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; await img.decode(); registerResourceImage(fileId, img); (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg)); } /** renderToPdf({ resourceBytes: imageBytes }) */ function imageBytes(fileId) { return loadImage.bytes?.get(fileId); } /** renderToHtml({ resourceImageUrl: imageUrl }) */ function imageUrl(fileId) { const bytes = imageBytes(fileId); if (!bytes) return undefined; imageUrl.urls ??= new Map(); if (!imageUrl.urls.has(fileId)) { const type = /\.svg$/i.test(fileId) ? 'image/svg+xml' : /\.png$/i.test(fileId) ? 'image/png' : 'image/jpeg'; imageUrl.urls.set(fileId, URL.createObjectURL(new Blob([bytes], { type }))); } return imageUrl.urls.get(fileId); } // ─── /Kit ───────────────────────────────────────────────────────────────────────
组合好的script.js可以直接运行:把它粘贴到任何页面的模块脚本中,或在CodePen上打开这道食谱。 GitHub上的食谱文件夹 ↗
变化
#只印当前标签
印刷的书边索引通常只显示当前字母的标签;从页眉中去掉ladder,藏青色标签仍会从A到C逐级下移。
- header: { elements: [...runningHeads, ...ladder,
+ header: { elements: [...runningHeads,常见问题
易错点
标题样式会继承其级别的分页设置
headingStyles中的条目没有写出的字段都取自它所属的标题级别,breakBefore也不例外。在:::pagebreak之后以H1设置样式的目录页或版权页会继承parity 'odd',结果落在一张空白页之后。给这类样式设置breakBefore: { enabled: false }。 标题样式 →
易错点
传入任何headings对象都会关掉H1换页
默认情况下,H1换页到右页(always-odd),但只要传入headings对象,这个默认值就会被重置,于是各章接排,span: 'page'也不起作用。在每份配置中重新写明headings.levels[0].breakBefore: { enabled: true, parity }。 从右页开始的章 →
易错点
相对容器的负偏移什么也渲染不出来
自动宽度的设计文本被限制在容器内,所以相对容器的负偏移会把它推出去,结果什么也不渲染。把这类元素用明确的mm偏移锚定到页面或出血上,或者给它们一个固定宽度。 锚定设计元素 →
易错点
替换调色板时,设计元素和引用颜色不会跟着变
postext 1.4.1把colorPalette读入文字样式(正文、标题、列表、题注、表格、框),但不读入页眉、页脚、章首页和篇章页的元素,也不读入bodyText.referenceColor:它们保留写在paletteId旁边的十六进制颜色。替换调色板时(例如做深色屏幕版或换色),在构建前根据colorPalette重写每一个关联的颜色。 语义调色板 →
易错点
不换行空格仍然会断行
在postext 1.4.1中,断行器把U+00A0当作普通空格,所以0.08 %、2.006 s或Section 2可能被拆到两行。把两部分连写(0.08%),或者改写句子。 转义与字面字符 →
易错点
消除孤字时收紧的字距可能根本不绘制
在postext 1.4.1中,段落以孤字结尾时,排版会把它排短一行:先收紧词间距,再用最多maxRuntTracking个千分之一em的负字距。Canvas和PDF渲染器只绘制大于零的字距,所以收紧了字距的段落印出来时并没有收紧:两端对齐的行从词间空格中扣掉了这部分差额,显得拥挤,末行还可能超出行长,在栏边被裁掉。设置bodyText.maxRuntTracking: 0,保留词间距的修正,再改写重新出现的孤字。 段末孤行、段首孤行与孤字 →
易错点
frontmatter的每个值都加引号
YAML会把title: 1984读成数字,把日期读成Date对象;非字符串的值在占位符中打印为空,PDF也会没有标题。每个值都加引号:title: "1984"。 文档元数据 →
- 在postext 1.4.1中,通栏标题下的栏间线从版心顶部开始,即使色带的高度是整数个网格行(本例正是如此)。用一个页面底色的框,画在版面设计的其他元素之下,就能把它盖住。
- 60 mm的栏约容纳50个字符,而Smyth的英文不能改写。在截图中,218行两端对齐的英文里有18行(8 %)的词间距被拉到正常宽度的1.7倍以上,西班牙文版不到2 %,因为译文是按栏宽调整过的。在把排得最差的五个词条(Aground、Aloft、Belay、Cockpit、Crow’s nest)换成别的之前,英文版的这个比例是17 %。要么挑选排得好的词条,要么加宽栏。
致谢
- 文本
- The Sailor’s Word-Book (London: Blackie and Son, 1867), entries from A, B and C · W. H. Smyth; Sir Edward Belcher · 公有领域
- Spanish translation, editor’s note and colophon · Postext Cookbook · 原创
- 字体
- Alegreya (SIL OFL 1.1) · Alegreya SC (SIL OFL 1.1) · Alegreya Sans SC (SIL OFL 1.1)


