# 周报中的报告：最早的两篇艾滋病报告

> 把1981年美国疾控中心的两篇报告重排成一份美国信纸开本的周报：报头、带行内标签的病例、Reported by与编者按的段落样式、编号参考文献。

- HTML版本: https://postext.dev/zh/cookbook/outbreak-report-bulletin
- 食谱 No. 143 · 完整出版物 · 难度 2 (中级) · 输出: Canvas, PDF
- 体裁: 论文与学术出版物, 报告
- 需要 postext ≥ 1.19.0, postext-pdf ≥ 1.19.0 · 已用 1.19.0, postext-pdf 1.19.0 测试，日期 2026-10-06
- 页面: [1](https://postext.dev/cookbook/outbreak-report-bulletin/en/p01.webp?v=6679adff), [2](https://postext.dev/cookbook/outbreak-report-bulletin/en/p02.webp?v=6679adff), [3](https://postext.dev/cookbook/outbreak-report-bulletin/en/p03.webp?v=6679adff), [4](https://postext.dev/cookbook/outbreak-report-bulletin/en/p04.webp?v=6679adff), [5](https://postext.dev/cookbook/outbreak-report-bulletin/en/p05.webp?v=6679adff), [6](https://postext.dev/cookbook/outbreak-report-bulletin/en/p06.webp?v=6679adff)
- PDF: https://postext.dev/cookbook/outbreak-report-bulletin/en/outbreak-report-bulletin.pdf?v=6679adff
- 在沙盒中打开: https://postext.dev/zh/sandbox#recipe=outbreak-report-bulletin&lang=en (.postext: https://postext.dev/cookbook/outbreak-report-bulletin/en/outbreak-report-bulletin.postext)
- 最后更新: 2026-10-06
- 其他语言: [en](https://postext.dev/en/cookbook/outbreak-report-bulletin.md), [es](https://postext.dev/es/cookbook/outbreak-report-bulletin.md), [ca](https://postext.dev/ca/cookbook/outbreak-report-bulletin.md), [ja](https://postext.dev/ja/cookbook/outbreak-report-bulletin.md), [ar](https://postext.dev/ar/cookbook/outbreak-report-bulletin.md)

## 简单来说

一份1981年的公共卫生周报，用信纸开本重新排版。报告里每一种段落（病例、报告医生名单、编者的评论）都有自己的样子，参考文献自动编号。

## 成品一览

六页美国信纸开本的流行病学周报，重印美国疾病控制中心（CDC）首次描述后来被称为艾滋病的两篇报告：洛杉矶的五例*Pneumocystis*肺炎（1981年6月5日），以及随后的卡波西肉瘤病例（1981年7月3日）。报头是原创设计，借鉴那个年代政府公报的风格：一条蓝色色带，压缩无衬线体的刊名，下面是两栏Plex Serif正文。报告里的每一种声音都有自己的样式：病例在粗体编号标签之下，*Reported by*一行用小号斜体，还有*Editorial Note*（编者按）、括号中的引用序号和编号的文献表。正文、参考文献和表格都按CDC当年的印本；时间线和第2页的病例表由编者添加。

**这份食谱回答的问题:**

- 怎样排周报中的报告，包括病例、编者按和编号参考文献？
- 怎样按APA、IEEE或其他引用样式引用文献并生成参考文献表？
- 怎样做一个有表头行、合并单元格、栏宽和逐格对齐的表？
- 怎样用Postext重新排版arXiv或PubMed Central上的开放获取论文，并保留引用、图和许可声明？

## 简短回答

```js
// script.js, 行 83–103
// :::paragraphs{style="…"} wraps the blocks of each voice; **Patient 1:** is a run-in label.
const voice = (id, family, size, lead, extra) => ({ id, fontFamily: family, fontSize: pt(size),
  lineHeight: pt(lead), color: col('ink'), boldColor: col('ink'), ...extra });
const aside = { textAlign: 'left', firstLineIndent: pt(0), snapToGrid: false };
const paragraphStyles = [
  // Case histories and the Editorial Note: the body texture, bold labels in the accent.
  voice('case', TEXT, 9.5, LEAD, { boldColor: col('accent'), boldFontWeight: 600 }),
  voice('editorial', TEXT, 9.5, LEAD, { boldColor: col('accent'), boldFontWeight: 600 }),
  // "Reported by …": the authors' line, italic small type in the condensed sans, off the grid.
  voice('reported', LABEL, 7.8, 10.2, { ...aside, italic: true, color: col('muted'),
    hyphenation: false, marginTop: pt(4), marginBottom: pt(6) }),
  // A line the reprint adds, in the label face and the accent: never taken for 1981 text.
  voice('editor', LABEL, 8, 10.6, { ...aside, italic: true, color: col('accent'),
    marginTop: pt(3), marginBottom: pt(5) }),
  voice('colophon', LABEL, 7.4, 9.6, { ...aside, color: col('muted'), marginTop: pt(LEAD) }),
];
// MMWR cited (1) in citation order, and each report (its H1 is a chapter) counted from 1.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = { style: 'american-medical-association', marker: 'parentheses',
  numbering: 'chapter', bibliography: { scope: 'chapter', fontSize: em(0.86),
    lineHeight: pt(10.6), entrySpacing: pt(1.5), labelAlign: 'right' } };
```

## 用料

**讲解**

- [段落样式](https://postext.dev/zh/docs/configuration.md#段落样式): 为与正文区分的连续段落设置的命名样式：诗歌、题词、献辞、签名、小字。
- [设计过的章首页](https://postext.dev/zh/docs/configuration.md#通栏与高级设计): 用文字、线条、框和图片自由组合绘制的标题，在正文上方留出所需的高度。
- [由参考文献数据生成的文献表](https://postext.dev/zh/docs/document-format.md#引用与参考文献): 根据文档中的参考文献数据（front matter或BibTeX块）生成的所引文献列表，放在:::bibliography处或最后一章之后。

**还用到**

- [按引用样式排的引用](https://postext.dev/zh/docs/document-format.md#引用与参考文献)
- [脚注](https://postext.dev/zh/docs/document-format.md#脚注)
- [表格样式](https://postext.dev/zh/docs/configuration.md#表格样式)
- [自定义资源类型](https://postext.dev/zh/docs/configuration.md#资源类型)
- [题注样式](https://postext.dev/zh/docs/configuration.md#题注样式)
- [图的放置](https://postext.dev/zh/docs/document-format.md#位置)
- [书眉与页码](https://postext.dev/zh/docs/configuration.md#页眉与页脚)
- [按页面角色显示书眉](https://postext.dev/zh/docs/configuration.md#文本元素)
- [标题样式](https://postext.dev/zh/docs/configuration.md#标题样式)
- [标题属性](https://postext.dev/zh/docs/document-format.md#标题属性)
- [单栏或双栏](https://postext.dev/zh/docs/configuration.md#版式类型)
- [成品尺寸](https://postext.dev/zh/docs/configuration.md#页面尺寸预设)
- [语义调色板](https://postext.dev/zh/docs/configuration.md#调色板)
- [导出PDF](https://postext.dev/zh/docs/configuration.md#生成pdf)
- [决定图位置的引用](https://postext.dev/zh/docs/document-format.md#行内引用主要写法)
- [图就放在这里](https://postext.dev/zh/docs/document-format.md#块级嵌入可选显式的行内放置)
- [用你的语言显示“图”和“表”](https://postext.dev/zh/docs/configuration.md#资源类型)
- [嵌入PDF的字体](https://postext.dev/zh/docs/configuration.md#为什么需要字体提供函数)
- [作为资源的图和表](https://postext.dev/zh/docs/document-format.md#资源)
- [分节版式](https://postext.dev/zh/docs/configuration.md#标题样式)
- [由数据生成的表](https://postext.dev/zh/docs/document-format.md#块级嵌入可选显式的行内放置)
- [命名表格样式](https://postext.dev/zh/docs/configuration.md#命名表格样式)

**配置一览**

- [`bodyText`](https://postext.dev/zh/docs/configuration.md#正文), [`captionStyle`](https://postext.dev/zh/docs/configuration.md#题注样式), [`citations`](https://postext.dev/zh/docs/configuration.md#引用), [`colorPalette`](https://postext.dev/zh/docs/configuration.md#调色板), [`footer`](https://postext.dev/zh/docs/configuration.md#页眉与页脚), `footnotes`, [`header`](https://postext.dev/zh/docs/configuration.md#页眉与页脚), [`headingStyles`](https://postext.dev/zh/docs/configuration.md#标题样式), [`headings`](https://postext.dev/zh/docs/configuration.md#标题), [`layout`](https://postext.dev/zh/docs/configuration.md#版式), [`locale`](https://postext.dev/zh/docs/configuration.md#断词), [`page`](https://postext.dev/zh/docs/configuration.md#页面), [`paragraphStyles`](https://postext.dev/zh/docs/configuration.md#段落样式), [`resourceTypes`](https://postext.dev/zh/docs/configuration.md#资源类型), [`tableStyle`](https://postext.dev/zh/docs/configuration.md#表格样式), [`tableStyles`](https://postext.dev/zh/docs/configuration.md#命名表格样式)

**API**

- [`LOCALES`](https://postext.dev/zh/docs/document-format.md#引用与参考文献), [`STYLES`](https://postext.dev/zh/docs/document-format.md#引用与参考文献), [`buildDocument`](https://postext.dev/zh/docs/configuration.md#构建文档), [`clearMeasurementCache`](https://postext.dev/zh/docs/configuration.md#测量缓存), [`createCiteprocEngine`](https://postext.dev/zh/docs/document-format.md#引用与参考文献), [`decompressWoff2`](https://postext.dev/zh/docs/configuration.md#浏览器字体提供函数fontsource--woff2), [`defaultResourceTypes`](https://postext.dev/zh/docs/configuration.md#资源类型), [`mergeCells`](https://postext.dev/zh/docs/document-format.md#块级嵌入可选显式的行内放置), [`parseTSV`](https://postext.dev/zh/docs/document-format.md#块级嵌入可选显式的行内放置), [`registerCitationEngine`](https://postext.dev/zh/docs/document-format.md#引用与参考文献), [`registerResourceImage`](https://postext.dev/zh/docs/architecture.md#api接口), [`renderPageToCanvas`](https://postext.dev/zh/docs/configuration.md#把页面渲染为位图), [`renderToPdf`](https://postext.dev/zh/docs/configuration.md#生成pdf)

**字体**

- IBM Plex Serif (OFL-1.1), Libre Franklin (OFL-1.1), IBM Plex Sans Condensed (OFL-1.1)

## 做法

### 1 · 每种声音一个段落样式

代码就是上面的[简短回答](#简短回答)。周报中的报告有几种语域：病例、报告人名单和编者的评论。用`:::paragraphs{style="…"}`把每一段包起来，就能在不改动文字的情况下给它一种外观：*Reported by*一行缩到7.8 pt的压缩斜体，脱离基线网格；病例则保持正文的质感，`**Patient 1:**`标签用强调色。引用写作`[@walzer1974]`；`american-medical-association`加上`marker: 'parentheses'`把它们印成(1)，并按引用顺序排列文献表，与周报当年一致；`numbering: 'chapter'`让每篇报告的引用从1开始编号，`scope: 'chapter'`在每篇报告下放一份使用同样编号的文献表。

### 2 · 开启第一篇报告的报头

```js
// script.js, 行 36–66
// Kicker and dateline are attributes of each report's H1; {titleText} read with inlineMarks
// keeps the italic genus. The July report opens where June ends, under a double rule.
const reportHead = (y, size) => [
  text('kicker', '{attr.kicker}', LABEL, 8.5, { ...caps(8.5), color: col('accent'),
    placement: at('container', 'top-left', 0, y, MEASURE) }),
  text('title', '{titleText}', DISPLAY, size, { fontWeight: 800, lineHeight: 1.08,
    inlineMarks: true, placement: at('#kicker', 'below', 0, 2.2, MEASURE) }),
  text('dateline', '{attr.dateline}', LABEL, 9, { fontStyle: 'italic', color: col('muted'),
    placement: at('#title', 'below', 0, 2, MEASURE) }),
];
const masthead = { enabled: true, minHeight: mm(72), slot: { elements: [
  { kind: 'box', id: 'band', style: { backgroundColor: col('accent') }, placement: {
    anchor: { to: 'page', edge: 'top-left' }, size: { width: 'fill', height: mm(49) } } },
  text('series', 'A reprint series in public health history', LABEL, 8,
    { ...caps(8), color: col('paper'), placement: at('page', 'top-left', SIDE, 11, 120) }),
  text('number', 'Reprint No. 1', LABEL, 8, { ...caps(8), color: col('paper'), align: 'right',
    placement: at('page', 'top-right', -SIDE, 11, 50) }),
  text('wordmark', 'Surveillance Notes', DISPLAY, 50, { fontWeight: 800, color: col('paper'),
    letterSpacing: pt(-1), lineHeight: 1, placement: at('page', 'top-left', SIDE - 1, 18) }),
  text('issue', 'Two reports of June 5 and July 3, 1981: the first published accounts of '
    + 'what became known as AIDS, as the Centers for Disease Control printed them', TEXT, 9.5,
  { fontStyle: 'italic', lineHeight: 1.3, placement: at('container', 'top-left', 0, 29, 92) }),
  text('contents', '**1** *Pneumocystis* Pneumonia — Los Angeles\n**3** Kaposi’s Sarcoma '
    + 'and *Pneumocystis* Pneumonia Among Homosexual Men — New York City and California\n'
    + '**6** Notifiable diseases, week ending June 27, 1981',
  LABEL, 8, { inlineMarks: true, lineHeight: 1.3, boldColor: col('accent'),
    placement: at('container', 'top-right', 0, 29, 76), align: 'left' }),
  rule('rule-a', 46.5, 2), rule('rule-b', 48, 0.5), ...reportHead(52, 23),
] } };
const opener = { enabled: true, minHeight: mm(26), slot: { elements: [rule('rule-a', 0, 2),
  rule('rule-b', 1.5, 0.5), ...reportHead(5, 18)] } };
```

报头是第一篇报告标题样式的设计，所以只在第1页出现一次，书眉跳过这一页（`pages: 'body'`）。眉题和日期行来自每篇报告H1的属性，标题则是`{titleText}`：文本元素设了`inlineMarks: true`，就保留标题中属名的斜体。7月的报告在一条双线下用同样的三个元素开头。它的标题样式以`spanBreak: false`横跨整页，所以标题从6月报告结束的地方开始：上方各栏底部齐平，下方正文在两栏中继续，就像周报把一篇报告接在另一篇之后那样。

### 3 · 由制表符分隔文本生成的三张表

```js
// script.js, 行 437–470
const NUMBER = /^[\d,.()% –]*$/; // a column of counts aligns right, its heading centred
const table = (tsv, headerRows, columnWidths) => {
  const model = parseTSV(tsv, { headerRows });
  const numeric = model.rows[0].map((_, c) => model.rows.slice(headerRows)
    .every((row) => NUMBER.test(row[c].content)));
  return { ...model, columnWidths, rows: model.rows.map((cells, r) => cells.map((cell, c) =>
    (numeric[c] ? { ...cell, align: r < headerRows ? 'center' : 'right' } : cell))) };
};
// Two header rows, as the weekly printed them: "25th week ending" spans two columns.
const notifiable = [[0, 0, 1, 0], [0, 1, 0, 2], [0, 3, 1, 3], [0, 4, 0, 6]]
  .reduce((m, [r1, c1, r2, c2]) => mergeCells(m, { start: { row: r1, col: c1 },
    end: { row: r2, col: c2 } }), table(NOTIFIABLE, 2, [3.6, 1, 1, 1, 1.1, 1.1, 1.1]));
const added = 'Compiled for this reprint from the case reports; not part of the 1981 report.';
const res = (id, typeId, kind, placement, more) => ({ id, typeId, kind, placement,
  createdAt: 0, updatedAt: 0, ...more });
const resources = [
  res('fig-timeline', 'figure', 'svg', { position: 'bottom', span: 'page' }, {
    svg: { fileId: 'timeline.svg', width: 1780, height: 490 },
    caption: 'The five patients, September 1980 to May 1981, by month. The illness before '
      + 'the pneumonia is counted back from the month of diagnosis, as each report gives it.',
    note: `Drawn by the editors. ${added}`,
    altText: 'A timeline of five rows, one per patient: Pneumocystis pneumonia diagnosed '
      + 'between February and April 1981, two deaths in March and May.' }),
  res('tbl-cases', 'table', 'table', { position: 'top', span: 'page' }, {
    caption: 'The five case reports at a glance.', note: added,
    table: { model: table(CASES, 1, [0.7, 0.45, 1, 2.2, 2.4, 1.5]) } }),
  res('tbl-complaints', 'table', 'table', { position: 'bottom' }, {
    caption: 'Presenting complaints in 20 patients with Kaposi’s sarcoma',
    table: { model: table(COMPLAINTS, 1, [2, 1.15]) } }),
  res('tbl-notifiable', 'table', 'table', { position: 'here' }, {
    caption: 'Summary — cases of specified notifiable diseases, United States',
    note: 'Cumulative totals include revised and delayed reports through previous weeks.',
    table: { model: notifiable, styleId: 'weekly' } }),
];
```

每张表都是一个内容槽，里面是制表符分隔的文本，也就是电子表格复制出来的格式，`parseTSV`把它变成表格模型。数字列右对齐，列标题居中；四次`mergeCells`调用构成法定报告传染病汇总表的两行表头。表格类型在每个H1处重新计数（`resetOn: 'h1'`），所以每篇报告都有自己的表1，和1981年一样。

### 4 · 一种蓝色，处处关联

```js
// script.js, 行 15–19
const palette = { ink: '#1c1f24', accent: '#1f3f74', tint: '#e6eaf1', rule: '#a7b1c2',
  muted: '#566070', paper: '#ffffff' };
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.accent })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
```

色带、标签、页码、表头和图表都引用同一个调色板条目：把`accent`换成自己周报的颜色，六页一次全部改色。

## 完整食谱

一个文件，由食谱文件夹合成，示例文本和排版食谱的公共工具包都已内联；它自己排出页面。要运行它，把它放进空白页面的`<script type="module">`，或粘贴到新建CodePen的JS面板里（设为模块）。它从esm.sh导入postext，不需要安装或构建。

- 食谱文件夹: https://github.com/drnachio/postext/tree/main/cookbook/outbreak-report-bulletin

### script.js

```js
// ═══ Postext Cookbook · Nº 143 · A weekly bulletin report: the first AIDS reports ═══
// https://postext.dev/en/cookbook/outbreak-report-bulletin
// Code: MIT · Text: CDC, MMWR 1981;30:250–252 and 305–308 (public domain) · Chart: original
// Fonts: IBM Plex Serif, Libre Franklin, Plex Sans Condensed (OFL) · Needs postext ≥ 1.19.0
import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerCitationEngine,
  registerResourceImage, defaultResourceTypes, parseTSV, mergeCells } from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';
import { createCiteprocEngine, STYLES, LOCALES } from 'https://esm.sh/postext-citeproc';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'outbreak-report-bulletin';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: a federal blue on office white, every colour linked by id
const palette = { ink: '#1c1f24', accent: '#1f3f74', tint: '#e6eaf1', rule: '#a7b1c2',
  muted: '#566070', paper: '#ffffff' };
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.accent })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
// #endregion
const [TEXT, DISPLAY, LABEL] = ['IBM Plex Serif', 'Libre Franklin', 'IBM Plex Sans Condensed'];
const [TRIM_W, TRIM_H, TOP, BOTTOM, SIDE, GUTTER] = [215.9, 279.4, 22, 20, 19, 6.35];
const MEASURE = TRIM_W - 2 * SIDE; // US Letter, two columns of 86 mm
const LEAD = 13; // pt: 9.5 pt Plex Serif, as a bulletin sets two columns of Letter

const text = (id, content, family, size, extra) => ({ kind: 'text', id, content, align: 'left',
  fontFamily: family, fontSize: pt(size), color: col('ink'), overflow: 'wrap', ...extra });
const at = (to, edge, x, y, width) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) },
  ...(width && { size: { width: mm(width), height: 'auto' } }) });
const caps = (size, extra) => ({ fontFamily: LABEL, fontSize: pt(size), fontWeight: 600,
  letterSpacing: pt(size * 0.18), textTransform: 'uppercase', ...extra });
const rule = (id, y, thickness) => ({ kind: 'rule', id, color: col('accent'),
  thickness: pt(thickness), placement: at('container', 'top-left', 0, y, MEASURE) });

// #region masthead: the bulletin's band, issue strip and contents over the first report
// Kicker and dateline are attributes of each report's H1; {titleText} read with inlineMarks
// keeps the italic genus. The July report opens where June ends, under a double rule.
const reportHead = (y, size) => [
  text('kicker', '{attr.kicker}', LABEL, 8.5, { ...caps(8.5), color: col('accent'),
    placement: at('container', 'top-left', 0, y, MEASURE) }),
  text('title', '{titleText}', DISPLAY, size, { fontWeight: 800, lineHeight: 1.08,
    inlineMarks: true, placement: at('#kicker', 'below', 0, 2.2, MEASURE) }),
  text('dateline', '{attr.dateline}', LABEL, 9, { fontStyle: 'italic', color: col('muted'),
    placement: at('#title', 'below', 0, 2, MEASURE) }),
];
const masthead = { enabled: true, minHeight: mm(72), slot: { elements: [
  { kind: 'box', id: 'band', style: { backgroundColor: col('accent') }, placement: {
    anchor: { to: 'page', edge: 'top-left' }, size: { width: 'fill', height: mm(49) } } },
  text('series', 'A reprint series in public health history', LABEL, 8,
    { ...caps(8), color: col('paper'), placement: at('page', 'top-left', SIDE, 11, 120) }),
  text('number', 'Reprint No. 1', LABEL, 8, { ...caps(8), color: col('paper'), align: 'right',
    placement: at('page', 'top-right', -SIDE, 11, 50) }),
  text('wordmark', 'Surveillance Notes', DISPLAY, 50, { fontWeight: 800, color: col('paper'),
    letterSpacing: pt(-1), lineHeight: 1, placement: at('page', 'top-left', SIDE - 1, 18) }),
  text('issue', 'Two reports of June 5 and July 3, 1981: the first published accounts of '
    + 'what became known as AIDS, as the Centers for Disease Control printed them', TEXT, 9.5,
  { fontStyle: 'italic', lineHeight: 1.3, placement: at('container', 'top-left', 0, 29, 92) }),
  text('contents', '**1** *Pneumocystis* Pneumonia — Los Angeles\n**3** Kaposi’s Sarcoma '
    + 'and *Pneumocystis* Pneumonia Among Homosexual Men — New York City and California\n'
    + '**6** Notifiable diseases, week ending June 27, 1981',
  LABEL, 8, { inlineMarks: true, lineHeight: 1.3, boldColor: col('accent'),
    placement: at('container', 'top-right', 0, 29, 76), align: 'left' }),
  rule('rule-a', 46.5, 2), rule('rule-b', 48, 0.5), ...reportHead(52, 23),
] } };
const opener = { enabled: true, minHeight: mm(26), slot: { elements: [rule('rule-a', 0, 2),
  rule('rule-b', 1.5, 0.5), ...reportHead(5, 18)] } };
// #endregion

const head = (id, content, parity, edge, x, extra) => text(id, content, LABEL, 8, {
  parity, pages: 'body', fontWeight: 500, letterSpacing: pt(0.3), color: col('muted'),
  overflow: 'clip', placement: at('page', edge, x, 12, 150), ...extra });
const folio = { fontWeight: 700, color: col('accent') };
const header = { elements: [
  head('v-folio', '{pageNumber}', 'even', 'top-left', SIDE, folio),
  head('v-title', 'Surveillance Notes · Reprint No. 1', 'even', 'top-left', SIDE + 8),
  head('r-title', '{chapterTitleAtTop}', 'odd', 'top-right', -SIDE - 8, { align: 'right' }),
  head('r-folio', '{pageNumber}', 'odd', 'top-right', -SIDE, { ...folio, align: 'right' }),
] };
const footer = { elements: [head('drop-folio', '{pageNumber}', 'all', 'bottom', 0,
  { ...folio, align: 'center', pages: 'opener', placement: at('page', 'bottom', 0, -11, 20) })] };

// #region answer: case histories, "Reported by" and the Editorial Note as paragraph styles
// :::paragraphs{style="…"} wraps the blocks of each voice; **Patient 1:** is a run-in label.
const voice = (id, family, size, lead, extra) => ({ id, fontFamily: family, fontSize: pt(size),
  lineHeight: pt(lead), color: col('ink'), boldColor: col('ink'), ...extra });
const aside = { textAlign: 'left', firstLineIndent: pt(0), snapToGrid: false };
const paragraphStyles = [
  // Case histories and the Editorial Note: the body texture, bold labels in the accent.
  voice('case', TEXT, 9.5, LEAD, { boldColor: col('accent'), boldFontWeight: 600 }),
  voice('editorial', TEXT, 9.5, LEAD, { boldColor: col('accent'), boldFontWeight: 600 }),
  // "Reported by …": the authors' line, italic small type in the condensed sans, off the grid.
  voice('reported', LABEL, 7.8, 10.2, { ...aside, italic: true, color: col('muted'),
    hyphenation: false, marginTop: pt(4), marginBottom: pt(6) }),
  // A line the reprint adds, in the label face and the accent: never taken for 1981 text.
  voice('editor', LABEL, 8, 10.6, { ...aside, italic: true, color: col('accent'),
    marginTop: pt(3), marginBottom: pt(5) }),
  voice('colophon', LABEL, 7.4, 9.6, { ...aside, color: col('muted'), marginTop: pt(LEAD) }),
];
// MMWR cited (1) in citation order, and each report (its H1 is a chapter) counted from 1.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = { style: 'american-medical-association', marker: 'parentheses',
  numbering: 'chapter', bibliography: { scope: 'chapter', fontSize: em(0.86),
    lineHeight: pt(10.6), entrySpacing: pt(1.5), labelAlign: 'right' } };
// #endregion

const config = () => ({ // a factory: the engine caches resolved configs per object
  locale: 'en-us', colorPalette, citations, paragraphStyles, header, footer,
  resourceTypes: defaultResourceTypes(LANG).map((type) => ({ ...type, shortLabel: type.name,
    resetOn: 'h1', numberingTemplate: '{n}', // each report counts its tables from 1
    ...(type.id === 'table' && { captionStyle: { position: 'above' } }) })),
  headingStyles: [
    { id: 'lead', span: 'page', advancedDesign: masthead },
    { id: 'report', span: 'page', spanBreak: false, advancedDesign: opener },
    { id: 'back', span: 'page', breakBefore: { enabled: true, parity: 'any' }, // one column
      layout: { layoutType: 'single' }, advancedDesign: opener },
  ],
  page: { sizePreset: 'custom', width: mm(TRIM_W), height: mm(TRIM_H), dpi: 150, margins: {
    top: mm(TOP), bottom: mm(BOTTOM), left: mm(SIDE), right: mm(SIDE), mirror: true } },
  layout: { layoutType: 'double', gutterWidth: mm(GUTTER) },
  bodyText: { fontFamily: TEXT, fontSize: pt(9.5), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    referenceBold: false, firstLineIndent: mm(4), indentAfterHeading: false,
    maxJustifyTracking: 10 }, // up to 1 % tracking for a line spaces alone would leave loose
  headings: { fontFamily: LABEL, color: col('accent'), fontWeight: 600, levels: [
    { level: 1, breakBefore: { enabled: false } }, // gotcha: headings-drop-h1-break
    { level: 3, ...caps(8), lineHeight: pt(LEAD), marginTop: pt(LEAD / 2), marginBottom: pt(0) },
  ] },
  footnotes: { fontSize: pt(7.6), lineHeight: pt(10), color: col('muted'),
    numberFormat: 'symbols' }, // the 1981 asterisk: *, †, ‡ …
  tableStyle: { rules: 'horizontal', borderColor: col('rule'), borderWidth: pt(0.5),
    headerBackground: col('accent'), headerColor: col('paper'), headerBold: true,
    headerFontFamily: LABEL, headerFontSize: pt(7.8), bodyFontFamily: LABEL,
    bodyFontSize: pt(7.8), bodyColor: col('ink'), cellPadding: mm(1.3) },
  tableStyles: [{ id: 'weekly', bodyFontSize: pt(7.4), headerFontSize: pt(7.2), // set solid
    cellPadding: mm(0.75) }],
  captionStyle: { fontFamily: LABEL, fontSize: pt(8.2), color: col('ink'), labelBold: true,
    labelColor: col('accent'), gap: mm(2), note: { fontSize: pt(7), color: col('muted') } },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const june = String.raw`---
title: "Pneumocystis Pneumonia — Los Angeles"
author: "Centers for Disease Control"
---

# *Pneumocystis* Pneumonia — Los Angeles {style="lead" kicker="Epidemiologic Notes and Reports" dateline="June 5, 1981 · Vol. 30, No. 21 · pp. 250–252"}

In the period October 1980-May 1981, 5 young men, all active homosexuals, were treated for biopsy-confirmed *Pneumocystis carinii* pneumonia at 3 different hospitals in Los Angeles, California. Two of the patients died. All 5 patients had laboratory-confirmed previous or current cytomegalovirus (CMV) infection and candidal mucosal infection. Case reports of these patients follow.

:::paragraphs{style="editor"}
Figure 1 and Table 1 were added for this reprint. They restate the dates and findings of the five case reports below and add nothing to them (:ref{id="fig-timeline"}; :ref{id="tbl-cases"}).
:::

:::paragraphs{style="case"}
**Patient 1:** A previously healthy 33-year-old man developed *P. carinii* pneumonia and oral mucosal candidiasis in March 1981 after a 2-month history of fever associated with elevated liver enzymes, leukopenia, and CMV viruria. The serum complement-fixation CMV titer in October 1980 was 256; in May 1981 it was 32.[^paired] The patient’s condition deteriorated despite courses of treatment with trimethoprim-sulfamethoxazole (TMP/SMX), pentamidine, and acyclovir. He died May 3, and postmortem examination showed residual *P. carinii* and CMV pneumonia, but no evidence of neoplasia.

**Patient 2:** A previously healthy 30-year-old man developed *P. carinii* pneumonia in April 1981 after a 5-month history of fever each day and of elevated liver-function tests, CMV viruria, and documented seroconversion to CMV, i.e., an acute-phase titer of 16 and a convalescent-phase titer of 28[^paired] in anticomplement immunofluorescence tests. Other features of his illness included leukopenia and mucosal candidiasis. His pneumonia responded to a course of intravenous TMP/SMX, but, as of the latest reports, he continues to have a fever each day.

**Patient 3:** A 30-year-old man was well until January 1981 when he developed esophageal and oral candidiasis that responded to Amphotericin B treatment. He was hospitalized in February 1981 for *P. carinii* pneumonia that responded to oral TMP/SMX. His esophageal candidiasis recurred after the pneumonia was diagnosed, and he was again given Amphotericin B. The CMV complement-fixation titer in March 1981 was 8. Material from an esophageal biopsy was positive for CMV.

**Patient 4:** A 29-year-old man developed *P. carinii* pneumonia in February 1981. He had had Hodgkins disease 3 years earlier, but had been successfully treated with radiation therapy alone. He did not improve after being given intravenous TMP/SMX and corticosteroids and died in March. Postmortem examination showed no evidence of Hodgkins disease, but *P. carinii* and CMV were found in lung tissue.

**Patient 5:** A previously healthy 36-year-old man with a clinically diagnosed CMV infection in September 1980 was seen in April 1981 because of a 4-month history of fever, dyspnea, and cough. On admission he was found to have *P. carinii* pneumonia, oral candidiasis, and CMV retinitis. A complement-fixation CMV titer in April 1981 was 128. The patient has been treated with 2 short courses of TMP/SMX that have been limited because of a sulfa-induced neutropenia. He is being treated for candidiasis with topical nystatin.
:::

[^paired]: Paired specimens not run in parallel.

The diagnosis of *Pneumocystis* pneumonia was confirmed for all 5 patients antemortem by closed or open lung biopsy. The patients did not know each other and had no known common contacts or knowledge of sexual partners who had had similar illnesses. The 5 did not have comparable histories of sexually transmitted disease. Four had serologic evidence of past hepatitis B infection but had no evidence of current hepatitis B surface antigen. Two of the 5 reported having frequent homosexual contacts with various partners. All 5 reported using inhalant drugs, and 1 reported parenteral drug abuse. Three patients had profoundly depressed numbers of thymus-dependent lymphocyte cells and profoundly depressed *in vitro* proliferative responses to mitogens and antigens. Lymphocyte studies were not performed on the other 2 patients.

:::paragraphs{style="reported"}
Reported by MS Gottlieb, MD, HM Schanker, MD, PT Fan, MD, A Saxon, MD, JD Weisman, DO, Div of Clinical Immunology-Allergy, Dept of Medicine, UCLA School of Medicine; I Pozalski, MD, Cedars-Mt. Sinai Hospital, Los Angeles; Field Services Div, Epidemiology Program Office, CDC.
:::

:::paragraphs{style="editorial"}
**Editorial Note:** *Pneumocystis* pneumonia in the United States is almost exclusively limited to severely immunosuppressed patients [@walzer1974]. The occurrence of pneumocystosis in these 5 previously healthy individuals without a clinically apparent underlying immunodeficiency is unusual. The fact that these patients were all homosexuals suggests an association between some aspect of a homosexual lifestyle or disease acquired through sexual contact and *Pneumocystis* pneumonia in this population. All 5 patients described in this report had laboratory-confirmed CMV disease or virus shedding within 5 months of the diagnosis of *Pneumocystis* pneumonia. CMV infection has been shown to induce transient abnormalities of *in vitro* cellular-immune function in otherwise healthy human hosts [@rinaldo1977; @rinaldo1980]. Although all 3 patients tested had abnormal cellular-immune function, no definitive conclusion regarding the role of CMV infection in these 5 cases can be reached because of the lack of published data on cellular-immune function in healthy homosexual males with and without CMV antibody. In 1 report, 7 (3.6%) of 194 patients with pneumocystosis also had CMV infection; 40 (21%) of the same group had at least 1 other major concurrent infection [@walzer1974]. A high prevalence of CMV infections among homosexual males was recently reported: 179 (94%) of 190 males reported to be exclusively homosexual had serum antibody to CMV, and 14 (7.4%) had CMV viruria; rates for 101 controls of similar age who were reported to be exclusively heterosexual were 54% for seropositivity and zero for viruria [@drew1981]. In another study of 64 males, 4 (6.3%) had positive tests for CMV in semen, but none had CMV recovered from urine. Two of the 4 reported recent homosexual contacts. These findings suggest not only that virus shedding may be more readily detected in seminal fluid than in urine, but also that seminal fluid may be an important vehicle of CMV transmission [@lang1975].

All the above observations suggest the possibility of a cellular-immune dysfunction related to a common exposure that predisposes individuals to opportunistic infections such as pneumocystosis and candidiasis. Although the role of CMV infection in the pathogenesis of pneumocystosis remains unknown, the possibility of *P. carinii* infection must be carefully considered in a differential diagnosis for previously healthy homosexual males with dyspnea and pneumonia.
:::

### References

:::bibliography{title=""}

:::references{format=csl-json}
[
 {
  "id": "walzer1974",
  "type": "article-journal",
  "author": [
   {
    "family": "Walzer",
    "given": "P. D."
   },
   {
    "family": "Perl",
    "given": "D. P."
   },
   {
    "family": "Krogstad",
    "given": "D. J."
   },
   {
    "family": "Rawson",
    "given": "P. G."
   },
   {
    "family": "Schultz",
    "given": "M. G."
   }
  ],
  "title": "<i>Pneumocystis carinii</i> pneumonia in the United States. Epidemiologic, diagnostic, and clinical features",
  "container-title": "Ann Intern Med",
  "issued": {
   "date-parts": [
    [
     1974
    ]
   ]
  },
  "volume": "80",
  "page": "83-93"
 },
 {
  "id": "cdc1981",
  "type": "article-journal",
  "author": [
   {
    "literal": "Centers for Disease Control"
   }
  ],
  "title": "<i>Pneumocystis</i> pneumonia—Los Angeles",
  "container-title": "MMWR",
  "issued": {
   "date-parts": [
    [
     1981
    ]
   ]
  },
  "volume": "30",
  "page": "250"
 }
]
:::

:::references{format=bibtex}
@article{rinaldo1977,
  author = {Rinaldo, Jr., C. R. and Black, P. H. and Hirsch, M. S.},
  title = {Interaction of cytomegalovirus with leukocytes from patients with mononucleosis due to cytomegalovirus},
  journal = {J Infect Dis}, year = 1977, volume = 136, pages = {667--678}}
@article{rinaldo1980,
  author = {Rinaldo, Jr., C. R. and Carney, W. P. and Richter, B. S. and Black, P. H. and Hirsch, M. S.},
  title = {Mechanisms of immunosuppression in cytomegaloviral mononucleosis},
  journal = {J Infect Dis}, year = 1980, volume = 141, pages = {488--495}}
@article{drew1981,
  author = {Drew, W. L. and Mintz, L. and Miner, R. C. and Sands, M. and Ketterer, B.},
  title = {Prevalence of cytomegalovirus infection in homosexual men},
  journal = {J Infect Dis}, year = 1981, volume = 143, pages = {188--192}}
@article{lang1975,
  author = {Lang, D. J. and Kummer, J. F.},
  title = {Cytomegalovirus in semen: observations in selected populations},
  journal = {J Infect Dis}, year = 1975, volume = 132, pages = {472--473}}
:::
`; // MMWR 1981;30(21):250–252
const july = String.raw`# Kaposi’s Sarcoma and *Pneumocystis* Pneumonia Among Homosexual Men — New York City and California {style="report" kicker="Epidemiologic Notes and Reports" dateline="July 3, 1981 · Vol. 30, No. 25 · pp. 305–308"}

During the past 30 months, Kaposi’s sarcoma (KS), an uncommonly reported malignancy in the United States, has been diagnosed in 26 homosexual men (20 in New York City [NYC]; 6 in California). The 26 patients range in age from 26-51 years (mean 39 years). Eight of these patients died (7 in NYC, 1 in California)—all 8 within 24 months after KS was diagnosed. The diagnoses in all 26 cases were based on histopathological examination of skin lesions, lymph nodes, or tumor in other organs. Twenty-five of the 26 patients were white, 1 was black. Presenting complaints from 20 of these patients are shown in :ref{id="tbl-complaints"}.

Skin or mucous membrane lesions, often dark blue to violaceous plaques or nodules, were present in most of the patients on their initial physician visit. However, these lesions were not always present and often were considered benign by the patient and his physician.

A review of the New York University Coordinated Cancer Registry for KS in men under age 50 revealed no cases from 1970-1979 at Bellevue Hospital and 3 cases in this age group at the New York University Hospital from 1961-1979.

Seven KS patients had serious infections diagnosed after their initial physician visit. Six patients had pneumonia (4 biopsy confirmed as due to *Pneumocystis carinii* [PC]), and one had necrotizing toxoplasmosis of the central nervous system. One of the patients with *Pneumocystis* pneumonia also experienced severe, recurrent, herpes simplex infection; extensive candidiasis; and cryptococcal meningitis. The results of tests for cytomegalovirus (CMV) infection were available for 12 patients. All 12 had serological evidence of past or present CMV infection. In 3 patients for whom culture results were available, CMV was isolated from blood, urine and/or lung of all 3. Past infections with amebiasis and hepatitis were commonly reported.

Since the previous report of 5 cases of *Pneumocystis* pneumonia in homosexual men from Los Angeles [@cdc1981], 10 additional cases (4 in Los Angeles and 6 in the San Francisco Bay area) of biopsy-confirmed PC pneumonia have been identified in homosexual men in the state. Two of the 10 patients also have KS. This brings the total number of *Pneumocystis* cases among homosexual men in California to 15 since September 1979. Patients range in age from 25 to 46 years.

:::paragraphs{style="reported"}
Reported by A Friedman-Kien, MD, L Laubenstein, MD, M Marmor, PhD, K Hymes, MD, J Green, MD, A Ragaz, MD, J Gottleib, MD, F Muggia, MD, R Demopoulos, MD, M Weintraub, MD, D Williams, MD, New York University Medical Center, NYC; R Oliveri, MD, J Marmer, MD, NYC; J Wallace, MD, I Halperin, MD, JF Gillooley, MD, St. Vincent’s Hospital and Medical Center, NYC; N Prose, MD, Downstate Medical Center, NYC; E Klein, MD, Roosevelt Hospital, NYC; J Vogel, MD, B Safai, MD, P Myskowski, MD, C Urmacher, MD, B Koziner, MD, L Nisce, MD, M Kris, MD, D Armstrong, MD, J Gold, MD, Sloan-Kettering Memorial Institute, NYC; D Mildran, MD, Beth Israel Hospital, NYC; M Tapper, MD, Lenox Hill Hospital, NYC; JB Weissman, MD, Columbia Presbyterian Hospital, NYC; R Rothenberg, MD, State Epidemiologist, New York State Dept of Health; SM Friedman, MD, Acting Director, Bur of Preventable Diseases, New York City Dept of Health; FP Siegal, MD, Dept of Medicine, Mount Sinai School of Medicine, City College of New York, NYC; J Groundwater, MD, J Gilmore, MD, San Francisco; D Coleman, MD, S Follansbee, MD, J Gullett, MD, SJ Stegman, MD, University of California at San Francisco; C Wofsy, MD, San Francisco General Hospital, San Francisco; D Bush, MD, Franklin Hospital, San Francisco; L Drew, MD, PhD, Mt. Zion Hospital, E Braff, MD, S Dritz, MD, City/County Health Dept, San Francisco; M Klein, MD, Valley Memorial Hospital, Salinas; JK Preiksaitis, MD, Stanford University Medical Center, Palo Alto; MS Gottlieb, MD, University of California at Los Angeles; R Jung, MD, University of Southern California Medical Center, Los Angeles; J Chin, MD, State Epidemiologist, California Dept of Health Services; J Goedert, MD, National Cancer Institute, National Institute of Health; Parasitic Diseases Div, Center for Infectious Diseases, VD Control Division, Center for Prevention Services, Chronic Diseases Div, Center for Environmental Health, CDC.
:::

:::paragraphs{style="editorial"}
**Editorial Note:** KS is a malignant neoplasm manifested primarily by multiple vascular nodules in the skin and other organs. The disease is multifocal, with a course ranging from indolent, with only skin manifestations, to fulminant, with extensive visceral involvement [@safai1981].

Accurate incidence and mortality rates for KS are not available for the United States, but the annual incidence has been estimated between 0.02-0.06 per 100,000; it affects primarily elderly males [@oettle1962; @rothman1962]. In a series of 92 patients treated between 1949 and 1975 at the Memorial Sloan-Kettering Cancer Institute in NYC, 76% were male, and the mean age was 63 years (range 23-90 years) at the time of diagnosis [@safai1980].

The disease in elderly men is usually manifested by skin lesions and a chronic clinical course (mean survival time is 8-13 years) [@safai1981]. Two exceptions to this epidemiologic pattern have been noted previously. The first occurs in an endemic belt across equatorial Africa, where KS commonly affects children and young adults and accounts for up to 9% of all cancers [@oettle1962]. Secondly, the disease appears to have a higher incidence in renal transplant recipients [@harwood1979; @stribling1978; @myers1974; @penn1979] and in others receiving immunosuppressive therapy [@gange1978; @klepp1978; @hoshaw1980].

The occurrence of this number of KS cases during a 30-month period among young, homosexual men is considered highly unusual. No previous association between KS and sexual preference has been reported. The fulminant clinical course reported in many of these patients also differs from that classically described for elderly persons.

The histopathologic diagnosis of KS may be difficult for 2 reasons. Changes in some lesions may be interpreted as nonspecific, and other cutaneous and soft tissue sarcomas, such as angiosarcoma of the skin, may be confused with KS [@girard1970; @rosai1976].

That 10 new cases of *Pneumocystis* pneumonia have been identified in homosexual men suggests that the 5 previously reported cases were not an isolated phenomenon [@cdc1981]. In addition, CDC has a report of 4 homosexual men in NYC who developed severe, progressive, perianal herpes simplex infections and had evidence of cellular immunodeficiencies. Three died, 1 with systemic CMV infection. The fourth patient is currently undergoing therapy. It is not clear if or how the clustering of KS, pneumocystis, and other serious diseases in homosexual men is related. What is known is that the patients with *Pneumocystis* pneumonia described in the previous report showed evidence of impaired cellular immunity and previous or current CMV infection [@cdc1981]. Furthermore, serologic evidence of past CMV infection and active shedding of CMV have been shown to be much more common among homosexual men than heterosexual men attending a sexually transmitted disease clinic [@drew1981]. A specific serologic association with CMV infection has been demonstrated among American and European patients with KS [@giraldo1975; @giraldo1978] and herpes-type virus particles have been demonstrated in tissue culture cell lines from African cases of KS [@giraldo1972]. It has been hypothesized that activation of oncogenic virus during periods of immunosuppression may result in the development of KS [@kapadia1977]. Although immunosuppression often results in CMV infection, it is not yet clear whether CMV infection precedes or follows the above-mentioned disorders.

Although it is not certain that the increase in KS and PC pneumonia is restricted to homosexual men, the vast majority of recent cases have been reported from this group. Physicians should be alert for Kaposi’s sarcoma, PC pneumonia, and other opportunistic infections associated with immunosuppression in homosexual men.
:::

### References

:::bibliography{title=""}

:::references{format=bibtex}
@article{safai1981,
  author = {Safai, B. and Good, R. A.},
  title = {Kaposi's sarcoma: a review and recent developments},
  journal = {CA}, year = 1981, volume = 31, pages = {1--12}}
@article{oettle1962,
  author = {Oettle, A. G.},
  title = {Geographical and racial differences in the frequency of Kaposi's sarcoma as evidence of environmental or genetic causes},
  journal = {Acta Un Int Cancr}, year = 1962, volume = 18, pages = {330--363}}
@article{rothman1962,
  author = {Rothman, S.},
  title = {Remarks on sex, age, and racial distribution of Kaposi's sarcoma and on possible pathogenetic factors},
  journal = {Acta Un Int Cancr}, year = 1962, volume = 18, pages = {326--329}}
@article{safai1980,
  author = {Safai, B. and Mik\'{e}, V. and Giraldo, G. and Beth, E. and Good, R. A.},
  title = {Association of Kaposi's sarcoma with second primary malignancies: possible etiopathogenic implications},
  journal = {Cancer}, year = 1980, volume = 45, pages = {1472--1479}}
@article{harwood1979,
  author = {Harwood, A. R. and Osoba, D. and Hofstader, S. L. and others},
  title = {Kaposi's sarcoma in recipients of renal transplants},
  journal = {Am J Med}, year = 1979, volume = 67, pages = {759--765}}
@article{stribling1978,
  author = {Stribling, J. and Weitzner, S. and Smith, G. V.},
  title = {Kaposi sarcoma in renal allograft recipients},
  journal = {Cancer}, year = 1978, volume = 42, pages = {442--446}}
@article{myers1974,
  author = {Myers, B. D. and Kessler, E. and Levi, J. and Pick, A. and Rosenfeld, J. B. and Tikvah, P.},
  title = {Kaposi sarcoma in kidney transplant recipients},
  journal = {Arch Intern Med}, year = 1974, volume = 133, pages = {307--311}}
@article{penn1979,
  author = {Penn, I.},
  title = {Kaposi's sarcoma in organ transplant recipients: report of 20 cases},
  journal = {Transplantation}, year = 1979, volume = 27, pages = {8--11}}
@article{gange1978,
  author = {Gange, R. W. and Jones, E. W.},
  title = {Kaposi's sarcoma and immunosuppressive therapy: an appraisal},
  journal = {Clin Exp Dermatol}, year = 1978, volume = 3, pages = {135--146}}
@article{klepp1978,
  author = {Klepp, O. and Dahl, O. and Stenwig, J. T.},
  title = {Association of Kaposi's sarcoma and prior immunosuppressive therapy: a 5-year material of Kaposi's sarcoma in {Norway}},
  journal = {Cancer}, year = 1978, volume = 42, pages = {2626--2630}}
@article{hoshaw1980,
  author = {Hoshaw, R. A. and Schwartz, R. A.},
  title = {Kaposi's sarcoma after immunosuppressive therapy with prednisone},
  journal = {Arch Dermatol}, year = 1980, volume = 116, pages = {1280--1282}}
@article{girard1970,
  author = {Girard, C. and Johnson, W. C. and Graham, J. H.},
  title = {Cutaneous angiosarcoma},
  journal = {Cancer}, year = 1970, volume = 26, pages = {868--883}}
@article{rosai1976,
  author = {Rosai, J. and Sumner, H. W. and Kostianovsky, M. and Perez-Mesa, C.},
  title = {Angiosarcoma of the skin. {A} clinicopathologic and fine structural study},
  journal = {Hum Pathol}, year = 1976, volume = 7, pages = {83--109}}
@article{drew1981,
  author = {Drew, W. L. and Mintz, L. and Miner, R. C. and Sands, M. and Ketterer, B.},
  title = {Prevalence of cytomegalovirus infection in homosexual men},
  journal = {J Infect Dis}, year = 1981, volume = 143, pages = {188--192}}
@article{giraldo1975,
  author = {Giraldo, G. and Beth, E. and Kourilsky, F. M. and others},
  title = {Antibody patterns to herpesvirus in Kaposi's sarcoma: serologic association of {European} Kaposi's sarcoma with cytomegalovirus},
  journal = {Int J Cancer}, year = 1975, volume = 15, pages = {839--848}}
@article{giraldo1978,
  author = {Giraldo, G. and Beth, E. and Henle, W. and others},
  title = {Antibody patterns to herpesvirus in Kaposi's sarcoma. {II}. {Serological} association of {American} Kaposi's sarcoma with cytomegalovirus},
  journal = {Int J Cancer}, year = 1978, volume = 22, pages = {126--131}}
@article{giraldo1972,
  author = {Giraldo, G. and Beth, E. and Haguenau, F.},
  title = {Herpes-type virus particles in tissue culture of Kaposi's sarcoma from different geographic regions},
  journal = {J Natl Cancer Inst}, year = 1972, volume = 49, pages = {1509--1526}}
@article{kapadia1977,
  author = {Kapadia, S. B. and Krause, J. R.},
  title = {Kaposi's sarcoma after long-term alkylating agent therapy for multiple myeloma},
  journal = {South Med J}, year = 1977, volume = 70, pages = {1011--1013}}
:::

# Notifiable Diseases — United States {style="back" kicker="From the issue of July 3, 1981" dateline="25th week ending June 27, 1981 · Vol. 30, No. 25 · p. 308"}

:::paragraphs{style="editor"}
Each weekly issue carried the counts of notifiable diseases that the states reported to CDC. The summary table of the July 3 issue is reprinted as it was printed (:ref{id="tbl-notifiable"}).
:::

::resource{id="tbl-notifiable"}

:::paragraphs{style="colophon"}
Surveillance Notes, Reprint No. 1. Re-set from the printed issues of June 5 and July 3, 1981 (Morbidity and Mortality Weekly Report, vol. 30, nos. 21 and 25, Centers for Disease Control, Atlanta), which are in the public domain. The text and the tables keep the wording and figures of 1981, including the terminology of the time. Figure 1 and the case table on page 2 were added by the editors. Set in IBM Plex Serif, Libre Franklin and IBM Plex Sans Condensed (SIL Open Font License).
:::
`; // MMWR 1981;30(25):305–308, and its notifiable diseases
const CASES = String.raw`Patient	Age	Pneumonia	CMV evidence	Other findings	Course
1	33	Mar 1981	CF titer 256 (Oct 1980), 32 (May 1981); viruria	Oral candidiasis; leukopenia; raised liver enzymes	Died May 3
2	30	Apr 1981	Seroconversion (16 to 28); viruria	Mucosal candidiasis; leukopenia	Pneumonia responded; daily fever
3	30	Feb 1981	CF titer 8 (Mar 1981); esophageal biopsy positive	Esophageal and oral candidiasis	Pneumonia responded
4	29	Feb 1981	CMV in lung tissue at postmortem	Hodgkins disease 3 years earlier, treated	Died in March
5	36	Apr 1981	Clinical CMV infection (Sep 1980); retinitis; CF titer 128	Oral candidiasis; sulfa-induced neutropenia	Under treatment
`; // TSV: the five case reports, compiled by the editors
const COMPLAINTS = String.raw`Presenting complaint	Number (percentage) of patients
Skin lesion(s) only	10 (50%)
Skin lesions plus lymphadenopathy	4 (20%)
Oral mucosal lesion only	1 (5%)
Inguinal adenopathy plus perirectal abscess	1 (5%)
Weight loss and fever	2 (10%)
Weight loss, fever, and pneumonia (one due to *Pneumocystis carinii*)	2 (10%)
`; // TSV: Table 1 of the July 3 report
const NOTIFIABLE = String.raw`Disease	25th week ending		Median 1976–1980	Cumulative, first 25 weeks		
	June 27, 1981	June 21, 1980		June 27, 1981	June 21, 1980	Median 1976–1980
Aseptic meningitis	133	107	107	1,859	1,619	1,126
Brucellosis	3	5	6	73	82	82
Chickenpox	3,801	4,577	4,353	157,690	144,465	144,465
Diphtheria	–	–	–	3	2	35
Encephalitis: Primary (arthropod-borne & unspec.)	20	8	18	360	282	287
Encephalitis: Post-infectious	2	4	4	44	98	102
Hepatitis, Viral: Type B	404	364	317	9,425	7,977	7,262
Hepatitis, Viral: Type A	430	579	595	11,981	12,945	14,023
Hepatitis, Viral: Type unspecified	185	214	178	5,367	5,296	4,253
Malaria	43	58	19	634	853	265
Measles (rubeola)	85	463	924	2,292	11,224	20,679
Meningococcal infections: Total	48	50	40	2,021	1,533	1,357
Meningococcal infections: Civilian	46	49	39	2,009	1,522	1,299
Meningococcal infections: Military	2	1	1	12	11	21
Mumps	54	158	422	2,665	6,382	11,698
Pertussis	23	26	26	482	524	524
Rubella (German measles)	30	82	362	1,443	2,756	9,753
Tetanus	4	4	1	27	29	29
Tuberculosis	505	613	689	12,863	12,672	13,778
Tularemia	8	7	4	87	70	62
Typhoid fever	7	10	8	224	171	171
Typhus fever, tick-borne (Rky. Mt. spotted)	57	39	48	436	318	299
Venereal diseases:						
Gonorrhea: Civilian	18,174	20,192	20,192	463,413	452,245	452,245
Gonorrhea: Military	579	368	495	13,731	12,756	12,844
Syphilis, primary & secondary: Civilian	610	488	452	14,248	12,387	11,575
Syphilis, primary & secondary: Military	6	5	5	178	151	146
Rabies in animals	162	136	73	3,479	3,250	1,509
`; // TSV: Table I of the July 3 issue
const markdown = `${june}\n\n${july}`; // one document: the July report runs on

// #region resources: three tables read from TSV, and the chart built from the case reports
const NUMBER = /^[\d,.()% –]*$/; // a column of counts aligns right, its heading centred
const table = (tsv, headerRows, columnWidths) => {
  const model = parseTSV(tsv, { headerRows });
  const numeric = model.rows[0].map((_, c) => model.rows.slice(headerRows)
    .every((row) => NUMBER.test(row[c].content)));
  return { ...model, columnWidths, rows: model.rows.map((cells, r) => cells.map((cell, c) =>
    (numeric[c] ? { ...cell, align: r < headerRows ? 'center' : 'right' } : cell))) };
};
// Two header rows, as the weekly printed them: "25th week ending" spans two columns.
const notifiable = [[0, 0, 1, 0], [0, 1, 0, 2], [0, 3, 1, 3], [0, 4, 0, 6]]
  .reduce((m, [r1, c1, r2, c2]) => mergeCells(m, { start: { row: r1, col: c1 },
    end: { row: r2, col: c2 } }), table(NOTIFIABLE, 2, [3.6, 1, 1, 1, 1.1, 1.1, 1.1]));
const added = 'Compiled for this reprint from the case reports; not part of the 1981 report.';
const res = (id, typeId, kind, placement, more) => ({ id, typeId, kind, placement,
  createdAt: 0, updatedAt: 0, ...more });
const resources = [
  res('fig-timeline', 'figure', 'svg', { position: 'bottom', span: 'page' }, {
    svg: { fileId: 'timeline.svg', width: 1780, height: 490 },
    caption: 'The five patients, September 1980 to May 1981, by month. The illness before '
      + 'the pneumonia is counted back from the month of diagnosis, as each report gives it.',
    note: `Drawn by the editors. ${added}`,
    altText: 'A timeline of five rows, one per patient: Pneumocystis pneumonia diagnosed '
      + 'between February and April 1981, two deaths in March and May.' }),
  res('tbl-cases', 'table', 'table', { position: 'top', span: 'page' }, {
    caption: 'The five case reports at a glance.', note: added,
    table: { model: table(CASES, 1, [0.7, 0.45, 1, 2.2, 2.4, 1.5]) } }),
  res('tbl-complaints', 'table', 'table', { position: 'bottom' }, {
    caption: 'Presenting complaints in 20 patients with Kaposi’s sarcoma',
    table: { model: table(COMPLAINTS, 1, [2, 1.15]) } }),
  res('tbl-notifiable', 'table', 'table', { position: 'here' }, {
    caption: 'Summary — cases of specified notifiable diseases, United States',
    note: 'Cumulative totals include revised and delayed reports through previous weeks.',
    table: { model: notifiable, styleId: 'weekly' } }),
];
// #endregion

// #region art: the timeline, drawn from the dates in the case reports
// Months counted from September 1980 (0) to May 1981 (8). `ill`: the months of illness
// before the pneumonia that the report gives; `pcp`: the month of diagnosis; `cmv`: a dated
// CMV finding; `died`: the month of death.
const PATIENTS = [
  { label: 'Patient 1 · 33', ill: [4, 6], pcp: 6, cmv: [1], died: 8 },
  { label: 'Patient 2 · 30', ill: [2, 7], pcp: 7, cmv: [] },
  { label: 'Patient 3 · 30', ill: [4, 5], pcp: 5, cmv: [6] },
  { label: 'Patient 4 · 29', pcp: 5, cmv: [], died: 6 },
  { label: 'Patient 5 · 36', ill: [3, 7], pcp: 7, cmv: [0] },
];
const MONTHS = ['Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May'];
const r2 = (v) => +v.toFixed(2);
// An SVG drawn as an image cannot see the page's web fonts (gotcha: svg-no-webfonts), so the
// chart carries its faces inline, as data URLs of the Fontsource files.
async function inlineFace(family, weight) {
  const id = family.toLowerCase().replace(/\s+/g, '-');
  const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${weight}`
    + '-normal.woff2';
  const bytes = new Uint8Array(await (await fetch(url)).arrayBuffer());
  let bin = '';
  for (const b of bytes) bin += String.fromCharCode(b);
  return `<style>@font-face{font-family:F;font-weight:${weight};src:url(data:font/woff2;`
    + `base64,${btoa(bin)}) format('woff2')}text{font-family:F}</style>`;
}
function timeline(faces) { // 178 × 47 mm: the width of the page's two columns
  const [W, H, L, R, T, ROW] = [178, 49, 26, 2, 9.5, 6.2];
  const step = (W - L - R) / MONTHS.length;
  const x = (m) => L + m * step;
  const label = (tx, ty, s, size = 2.75, anchor = 'start', weight = 400, fill = palette.ink) =>
    `<text x="${r2(tx)}" y="${r2(ty)}" font-size="${size}" font-weight="${weight}" `
    + `text-anchor="${anchor}" fill="${fill}">${s}</text>`;
  const cross = (cx, cy) => `<path d="M${r2(cx - 1.3)} ${r2(cy - 1.3)}l2.6 2.6M${r2(cx + 1.3)} `
    + `${r2(cy - 1.3)}l-2.6 2.6" stroke="${palette.ink}" stroke-width="0.6"/>`;
  let out = '';
  MONTHS.forEach((m, i) => {
    if (i % 2 === 0) out += `<rect x="${r2(x(i))}" y="${T - 1}" width="${r2(step)}" `
      + `height="${PATIENTS.length * ROW + 1}" fill="${palette.tint}"/>`;
    out += label(x(i) + step / 2, T - 2.4, m, 2.6, 'middle', 600, palette.muted);
  });
  out += label(x(0), T - 5.6, '1980', 2.6, 'start', 600, palette.accent)
    + label(x(4), T - 5.6, '1981', 2.6, 'start', 600, palette.accent);
  PATIENTS.forEach((p, i) => {
    const cy = T + i * ROW + ROW / 2;
    out += label(0, cy + 1, p.label, 2.9, 'start', 600);
    if (p.ill) out += `<rect x="${r2(x(p.ill[0]) + 0.6)}" y="${r2(cy - 1.1)}" `
      + `width="${r2((p.ill[1] - p.ill[0]) * step - 1.2)}" height="2.2" rx="1.1" `
      + `fill="${palette.rule}"/>`;
    for (const c of p.cmv) {
      const cx = x(c) + step / 2;
      out += `<path d="M${r2(cx)} ${r2(cy - 1.5)}l1.5 1.5l-1.5 1.5l-1.5-1.5Z" fill="#fff" `
        + `stroke="${palette.ink}" stroke-width="0.35"/>`;
    }
    out += `<circle cx="${r2(x(p.pcp) + step / 2)}" cy="${r2(cy)}" r="1.7" `
      + `fill="${palette.accent}"/>`;
    if (p.died !== undefined) {
      const cx = x(p.died) + step / 2;
      out += cross(cx, cy);
    }
  });
  const ly = H - 2.2;
  out += `<rect x="${L}" y="${ly - 2}" width="8" height="2.2" rx="1.1" fill="${palette.rule}"/>`
    + label(L + 10, ly, 'illness before the pneumonia')
    + `<circle cx="${L + 52}" cy="${ly - 0.9}" r="1.7" fill="${palette.accent}"/>`
    + label(L + 55, ly, '<tspan font-style="italic">Pneumocystis</tspan> pneumonia diagnosed')
    + `<path d="M${L + 109} ${ly - 2.4}l1.5 1.5l-1.5 1.5l-1.5-1.5Z" fill="#fff" `
    + `stroke="${palette.ink}" stroke-width="0.35"/>` + label(L + 112, ly, 'dated CMV finding')
    + cross(L + 139, ly - 0.9) + label(L + 142, ly, 'died');
  return `<svg xmlns="http://www.w3.org/2000/svg" width="1780" height="490" `
    + `viewBox="0 0 ${W} ${H}">${faces}${out}</svg>`;
}
// #endregion

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { 'IBM Plex Serif': ['400', '400i', '600', '600i'], 'Libre Franklin': ['800', '800i'],
  'IBM Plex Sans Condensed': ['400', '400i', '500', '600', '600i', '700'] };

// ─── 4 · Build & show ───────────────────────────────────────────────────────
await loadFonts(FONTS, markdown);
const faces = await inlineFace(LABEL, 400) + await inlineFace(LABEL, 600);
await loadSvg('timeline.svg', timeline(faces));
const doc = await buildWithFonts(() => buildDocument({ markdown, resources }, config()), markdown);
showPages(doc, { title: 'Surveillance Notes · Reprint No. 1' });
offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider,
  resourceBytes: imageBytes }), `${RECIPE}.pdf`);

// ─── 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 · 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 ───────────────────────────────────────────────────────────────────────
```

## 变化

### 上标的参考文献序号

多数医学期刊把序号排成上标，而不是放在括号里。

```diff
-const citations = { style: 'american-medical-association', marker: 'parentheses',
+const citations = { style: 'american-medical-association', marker: 'superscript',
```

### 每篇报告另起一页

分篇归档的重印系列让每篇报告从新的一页开始。

```diff
-    { id: 'report', span: 'page', spanBreak: false, advancedDesign: opener },
+    { id: 'report', span: 'page', advancedDesign: opener },
```

### 温哥华格式的文献表

[温哥华格式的医学论文](https://postext.dev/zh/cookbook/medical-article-vancouver.md)修改了自带的`elsevier-vancouver`样式，使之符合NLM的参考文献格式。

## 常见问题

- **传入任何headings对象都会关掉H1换页.** 默认情况下，H1换页到右页（always-odd），但只要传入headings对象，这个默认值就会被重置，于是各章接排，span: 'page'也不起作用。在每份配置中重新写明headings.levels[0].breakBefore: { enabled: true, parity }。
- **SVG <img>中的文字不能使用网络字体.** SVG作为图像绘制，而图像无法使用页面的网络字体，所以其中的标签会退回系统字体。把文字转成轮廓，在SVG中嵌入@font-face子集，或者把标签移到题注里。
- **排版前加载所有字体.** 排版用浏览器已加载的字体测量文字，并缓存宽度，所以首次构建之后才到的字体会造成断行错误，PDF也不再与屏幕一致。先加载所有字重和样式；有字体迟到时，重新构建前调用clearMeasurementCache()。
- **配置按对象身份缓存：每次新建一个对象.** 引擎按对象身份缓存解析后的配置，所以就地修改配置再构建，会复用旧的结果。每次构建都新建一个对象，这也是食谱的配置写成工厂函数config()的原因。

- 不设`numbering: 'chapter'`时，参考文献序号贯穿整个文档，`scope: 'chapter'`只是把文献表分开：7月的报告会引用第4和第6至23号文献，而周报当年印的是1至19号。
- 1981年的脚注*Paired specimens not run in parallel*用星号标注：`footnotes.numberFormat: 'symbols'`依次使用*、†、‡，并在每页重新开始。这些符号中只有星号在Fontsource字体的latin文件里（剑号在latin-ext里，双剑号两者都没有），所以一页有两三条脚注时，需要换用带这些符号的字体，或者设`symbols: ['*', '§', '¶']`。
- 1981年的文字按原样保留，包括当时的术语，版权说明里写明了这一点。重印不修改所复制的正文；编者添加的内容（时间线、病例表、第一段下面的蓝色斜体说明）用自己的样式并加以标注。

## 致谢

- 食谱: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- 文字: "Pneumocystis Pneumonia — Los Angeles", MMWR 1981;30(21):250–252, and "Kaposi's Sarcoma and Pneumocystis Pneumonia Among Homosexual Men — New York City and California", MMWR 1981;30(25):305–308, re-set in full from the printed issues; Table 1 of the first report and Figure 1 added by the editors from the case reports. "All material in the MMWR Series is in the public domain and may be used and reprinted without special permission; citation as to source, however, is appreciated.": Centers for Disease Control (CDC) ([来源](https://stacks.cdc.gov/view/cdc/50022)), 公有领域
- 图片: Figure 1, timeline of the five cases, drawn in code from the dates in the case reports: Ignacio Ferro, 原创
- 字体: IBM Plex Serif (OFL-1.1), Libre Franklin (OFL-1.1), IBM Plex Sans Condensed (OFL-1.1)
- 代码: MIT · 示例内容: CC-BY-4.0

## 相关食谱

- [No. 095 · 温哥华格式的医学论文](https://postext.dev/zh/cookbook/medical-article-vancouver.md): 一篇A4双栏的临床试验报告：上标引用序号，温哥华格式的编号参考文献表，DOI在PDF中是可点击的链接。 · 难度 2 (中级) · 论文与学术出版物
- [No. 098 · 每章自带参考文献表的论文集](https://postext.dev/zh/cookbook/edited-volume-chapter-bibliographies.md): 三位作者的三篇论文，各是buildBundle里的一个独立文档，每章末尾有一份Chicago著者-出版年制的文献表，只列本章引用的文献。 · 难度 3 (高级) · 论文与学术出版物, 教科书
- [No. 093 · 同一篇文字的四种引用样式](https://postext.dev/zh/cookbook/one-text-four-citation-styles.md): 一篇带六处引文的短文，分别按APA 7、Chicago著者-出版年制、IEEE和ISO 690排版：唯一改动的是样式设置。 · 难度 2 (中级) · 论文与学术出版物, 报告
