跳到主要内容
食谱编号180

排版食谱 · 第2章 · 字体与文字

学习读本里按颜色分类的高亮

一本历史读本,关键词用三种颜色高亮:术语绿色、日期琥珀色、人名地名蓝色,三种样式都在 markStyles 里定义并链接到调色板。

  • 英文样例:尚无中文版本
  • 成品尺寸170 × 240 mm
  • 1栏
  • Source Serif 4 10.5/14.5
  • Source Sans 3
  • 4页
  • 难度
  • Postext 1.65.0
  • 排版用时127 ms
  • 164行代码

简单来说

一篇讲印刷术历史的学校读物。关键术语标成绿色,年代标成琥珀色,人物和地点标成蓝色,第一页的图例说明每种颜色的含义。

成品一览

学校历史读本《Readings in Early Modern History》中某单元的第二篇读物:四页,讲1450年到1550年间印刷术如何从美因茨传遍欧洲,页面 170 × 240 mm。编者像一个认真的学生那样给正文划了重点,而且整本书用同一套颜色:关键术语铺绿色,年代铺琥珀色,人物和地点铺浅蓝色。标题下的图例直接用这三种颜色说明代码,末页的术语表每个词条都以绿色术语开头。高亮只是铺在普通文字下面的色带,所以行的断行、断词和两端对齐都和没有色带时完全一样。

这道食谱解答

  • 怎样用各有含义的颜色(术语、日期、人名)给学习材料的关键词划重点?

简短回答

script.js · 第37–52行在完整代码中
// :mark[Mainz]{style="name"} in the text picks a style by id. The band is painted under
// words that stay ordinary text, so the lines break, hyphenate and justify as if the
// mark were not there, and a mark that runs over a line end gets a band on each line.
const band = (id, name) => ({
  id, name,
  background: col(id), // pale enough for ink at full contrast on top
  color: col('ink'), // the words keep the colour of the text
  // borderRadius: 0 is the default (square ends, as a highlighter pen leaves them)
  paddingX: em(0.12), // the band reaches a little past the first and the last letter
  paddingTop: em(0.04), paddingBottom: em(0.1), // and down past the descenders
});
const markStyles = [
  band('term', t({ en: 'Key term', es: 'Término clave' })),
  band('date', t({ en: 'Date', es: 'Fecha' })),
  band('name', t({ en: 'Name', es: 'Nombre' })),
];

用料

类型
Source Serif 4, Source Sans 3(SIL OFL 1.1)
素材
  • press-1400.jpg
  • The printing workshop on the opening page, a painting (Generated With Diffusion Models, 原创)

做法

#1 · 每类词一个样式

代码就是上面的简短回答。markStyles 声明三个有名字的样式,正文里每处高亮选一个::mark[Johann Fust]{style="name"}、:mark[1455]{style="date"}、:mark[colophon]{style="term"}(高亮标记)。每个样式表示一种含义(name 是编辑时看到的名字),于是 Markdown 记下的是一个词为什么被标出,而不是它用什么颜色。改一个样式的颜色,全书所有日期都跟着变。两端是方的,postext 1.52 起默认如此(高亮样式):荧光笔画出来的两端是方的,圆角色带看起来像标签。

#2 · 调色板里的三种浅色

script.js · 第14–29行在完整代码中
const palette = {
  ink: '#1f1c19', // text: a warm near-black
  accent: '#8a3220', // kickers, rules, numbers: an iron-gall red
  tint: '#f5f0e7', // the key's background
  rule: '#d3cabc', // hairlines
  muted: '#6b645b', // running heads, colophon
  paper: '#ffffff',
  term: '#cce8c8', // key terms: a pale green
  date: '#fbe0a1', // dates: amber
  name: '#cfe0f4', // people and places: a pale blue
};
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' } })),
  { id: 'main-color', name: 'accent (defaults)', value: { hex: palette.accent, model: 'hex' } },
];

三条色带和正文墨色、强调色一样是调色板条目,在 Sandbox 里打开这本书的人改一个色样就能换掉整套颜色。它们很浅,亮度接近方框的底色,上面的文字对比度保持在 12.6:1 以上;三种颜色在同一行里相邻时也分得清,比如第3页的“In 1517 Martin Luther”。高亮不占地方:paddingX 向短语旁边的空格伸出 0.12 em,paddingBottom 把色带延到下伸部以下,两者都画在原本就有的空白里。

#3 · 开篇:标题上方的画

script.js · 第56–81行在完整代码中
const opener = {
  enabled: true,
  minHeight: mm(122), // the floor of the opener: the key starts on the same line
  marginBottom: pt(0),
  slot: {
    elements: [
      { kind: 'image', id: 'press', resourceId: 'press', fit: 'cover',
        placement: { anchor: { to: 'page', edge: 'top-left' },
          size: { width: mm(170), height: mm(96) } } },
      { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: SANS,
        fontSize: pt(8.5), fontWeight: 700, letterSpacing: pt(1.6),
        textTransform: 'uppercase', color: col('accent'), align: 'left',
        placement: { anchor: { to: 'container', edge: 'top-left' },
          offset: { x: mm(0), y: mm(82) } } },
      { kind: 'text', id: 'title', content: '{titleText}', fontFamily: SERIF,
        fontSize: pt(26), lineHeight: 1.08, fontWeight: 700, color: col('ink'), align: 'left',
        placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(2) },
          size: { width: mm(126), height: 'auto' } } },
      { kind: 'text', id: 'standfirst', content: '{attr.standfirst}', fontFamily: SERIF,
        italic: true, fontSize: pt(10.5), lineHeight: LEAD / 10.5, color: col('ink'),
        align: 'left',
        placement: { anchor: { to: '#title', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
          size: { width: mm(118), height: 'auto' } } },
    ],
  },
};

一级标题印的是一个设计。画锚定在页面上,所以从上边和左右两边出血,170 × 96 mm,用 fit: 'cover'。栏目标签和导语来自标题行上的属性(标题属性),图例则是一个普通的 :::callout,列表项分别以三种高亮开头。minHeight 决定开篇的深度:不管标题多长,图例都从同一行开始。

#4 · 按奇偶页排的书眉

script.js · 第85–106行在完整代码中
const head = (id, content, parity, edge, x, extra = {}) => ({
  kind: 'text', id, content, parity, pages: 'body',
  fontFamily: SANS, fontSize: pt(7.5), fontWeight: 600, letterSpacing: pt(1.2),
  textTransform: 'uppercase', color: col('muted'),
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(13) } },
  ...extra,
});
const folio = { fontWeight: 700, color: col('ink') };
const header = {
  elements: [
    head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
    head('verso-title', '{title}', 'even', 'top-left', OUTER + 9),
    head('recto-title', '{chapterTitle}', 'odd', 'top-right', -(OUTER + 9)),
    head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
  ],
};
const footer = {
  elements: [head('drop-folio', '{pageNumber}', 'all', 'top', 0, {
    ...folio, pages: 'opener',
    placement: { anchor: { to: 'container', edge: 'top' }, offset: { x: mm(0), y: mm(9) } },
  })],
};

左页印丛书名,右页印本篇标题,两者都用 parity 筛选,并用 pages: 'body' 避开开篇页(文本元素)。开篇页的页码放在页脚。

完整食谱

沙盒
// ═══ Postext Cookbook · Nº 180 · Colour-coded highlights in a study reader ════════
// https://postext.dev/en/cookbook/colour-coded-study-highlights
// Code: MIT · Text: original (CC BY 4.0) · Picture: painting made for the recipe
// Fonts: Source Serif 4, Source Sans 3 (SIL OFL 1.1) · Needs postext ≥ 1.65.0
import {
  buildDocumentWithFonts, renderPageToCanvas, registerResourceImage, defaultResourceTypes,
} from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'colour-coded-study-highlights';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: semantic colours, three of them for the highlighter
const palette = {
  ink: '#1f1c19', // text: a warm near-black
  accent: '#8a3220', // kickers, rules, numbers: an iron-gall red
  tint: '#f5f0e7', // the key's background
  rule: '#d3cabc', // hairlines
  muted: '#6b645b', // running heads, colophon
  paper: '#ffffff',
  term: '#cce8c8', // key terms: a pale green
  date: '#fbe0a1', // dates: amber
  name: '#cfe0f4', // people and places: a pale blue
};
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' } })),
  { id: 'main-color', name: 'accent (defaults)', value: { hex: palette.accent, model: 'hex' } },
];
// #endregion
const OUTER = 20; // outer margin in mm: the running heads align to it
const LEAD = 14.5; // body leading in pt
const SANS = 'Source Sans 3';
const SERIF = 'Source Serif 4';

// #region answer: three highlight styles, one per kind of word, linked to the palette
// :mark[Mainz]{style="name"} in the text picks a style by id. The band is painted under
// words that stay ordinary text, so the lines break, hyphenate and justify as if the
// mark were not there, and a mark that runs over a line end gets a band on each line.
const band = (id, name) => ({
  id, name,
  background: col(id), // pale enough for ink at full contrast on top
  color: col('ink'), // the words keep the colour of the text
  // borderRadius: 0 is the default (square ends, as a highlighter pen leaves them)
  paddingX: em(0.12), // the band reaches a little past the first and the last letter
  paddingTop: em(0.04), paddingBottom: em(0.1), // and down past the descenders
});
const markStyles = [

组合好的script.js可以直接运行:把它粘贴到任何页面的模块脚本中,或在CodePen上打开这道食谱。 GitHub上的食谱文件夹 ↗ (在新标签页中打开)

变化

#让色带两端变圆

圆角取字号的五分之一,就像记号笔画出的柔和两端。

-  // borderRadius: 0 is the default (square ends, as a highlighter pen leaves them)
+  borderRadius: em(0.2), // soft ends, as a felt-tip pen leaves them

#每行一条完整色带

设置 decorationBreak: 'clone' 后,跨行的高亮在每一段的两端都有留白,比如第3页的“pamphlets”在“pam-”后断开的地方。

   paddingX: em(0.12), // the band reaches a little past the first and the last letter
+  decorationBreak: 'clone', // each line piece gets a whole band

常见问题

  • 只靠颜色,黑白复印后就看不出来了,而很多读物正是这样复印后发到课堂上的。术语表另外把术语设为粗体;如果读者会复印这些页面,就给三类词中的一类再加一个自己的标志。
  • markStyles 里没有声明的样式 id 会退回第一个样式,页面上不会报错(Sandbox 会提示)。id 要短,并检查正文里的拼写。

致谢

文本
  • The reading, its key, the review questions and the colophon · Ignacio Ferro · CC BY 4.0
图片
  • The printing workshop on the opening page, a painting · Generated With Diffusion Models · 原创
字体
Source Serif 4 (SIL OFL 1.1) · Source Sans 3 (SIL OFL 1.1)
沙盒