In breve
Due capitoli di un manuale di fisiologia con un indice analitico in fondo. Le parole sono marcate nel testo e il libro trova da solo i loro numeri di pagina: nessuno è scritto a mano.
Che cosa comporrai
La parte IV di Principles of Human Physiology, un manuale su una pagina di 210 × 277 mm: il capitolo 14, The heart as a pump, e il capitolo 15, Vessels and blood pressure, si aprono su recto sotto una fascia rossa e compongono le note cliniche e le didascalie in un canale esterno. La parte si chiude con un indice di circa novanta voci e trenta sottovoci su due colonne, come lo compone un manuale di medicina: voci in minuscolo, lettere di gruppo nel colore d’accento, sottovoci rientrate di un quadratone, la pagina che definisce il termine in grassetto, see e see also in corsivo. Nessun numero di pagina è stato scritto a mano. Ogni termine è marcato nel capitolo, dove il testo lo spiega, e l’indice legge le pagine su cui quelle marche sono cadute, folio da 297 a 304.
Questa ricetta risponde a
- Come costruisco un indice analitico i cui numeri di pagina cambiano da soli quando il testo si sposta?
La risposta breve
// The chapters mark each term where the text discusses it:
// the :index[stroke volume]{main} prints the words, files them, bold page
// mitral:index{term="heart!valves!mitral"} prints nothing, files the word before it
// ## Venous return :index{term="venous return" range="start" main} … range="end"
// :index{term="inotropy" see="contractility"} a cross-reference, no page
// The last chapter is `# Index {style="index"}` and `:::index`. buildBundle hands it every
// chapter's marks with the pages they landed on, and lays the book out again until those
// pages stop moving.
const indexStyle = {
id: 'index', numbered: false, toc: false, // not counted: no chapter 16
layout: { layoutType: 'double', gutterWidth: mm(6) }, // two columns in this section only
advancedDesign: opener(INDEX_BAND), // the chapters' band, shallower and with no number
header: runningHeads(t({ en: 'Index', es: 'Índice analítico' })),
};
const index = {
fontFamily: SERIF, fontSize: pt(8.6), lineHeight: pt(11.4),
indent: em(1), turnoverIndent: em(2), // sub-entries step in 1 em; wrapped lines hang 2 em
// English: Chicago's short ranges (301–3; 298–300 keeps the digit that changes). Spanish
// writes both numbers in full and joins them with a hyphen (301-303).
rangeFormat: t({ en: 'chicago', es: 'full' }), rangeSeparator: t({ en: '–', es: '-' }),
main: { bold: true }, // the defining page in bold
see: { italic: true }, // See / See also, Véase / Véase también by the document's locale
groups: { ...label, fontSize: pt(9.5), letterSpacing: pt(0), color: col('accent') },
};
Ingredienti
- Funzionalità
- Indice analiticoLibri costruiti capitolo per capitoloStili di titoloAperture progettateAttributi dei titoliTitoli numeratiTestatine e folioTestatine secondo il ruolo della paginaMargini speculariColonna a margine per gli elementi flottantiNote a margineDidascalie lateraliCapitoli non numeratiRiquadri flottantiRiquadriDidascalie numerateEsportazione in PDF
- Usa anche
- Colonna e mezzaCitazioni che collocano le figureFigura e Tabella nella tua linguaFascia di capitolo a tutta larghezzaStili di paragrafoFont incorporati nel PDFTipi di risorsa personalizzatiFigure e tabelle come risorsePagine preliminari in numeri romaniGeometria della sezioneTestatine per sezioneApici e pedici
- Tipo
- Literata, Libre Franklin (SIL OFL 1.1)
- Risorse
- Nessuna: ogni immagine è disegnata nel codice
Preparazione
#1 · Marca il passo, non la parola
// The chapters mark each term where the text discusses it:
// the :index[stroke volume]{main} prints the words, files them, bold page
// mitral:index{term="heart!valves!mitral"} prints nothing, files the word before it
// ## Venous return :index{term="venous return" range="start" main} … range="end"
// :index{term="inotropy" see="contractility"} a cross-reference, no page
// The last chapter is `# Index {style="index"}` and `:::index`. buildBundle hands it every
// chapter's marks with the pages they landed on, and lays the book out again until those
// pages stop moving.
const indexStyle = {
id: 'index', numbered: false, toc: false, // not counted: no chapter 16
layout: { layoutType: 'double', gutterWidth: mm(6) }, // two columns in this section only
advancedDesign: opener(INDEX_BAND), // the chapters' band, shallower and with no number
header: runningHeads(t({ en: 'Index', es: 'Índice analítico' })),
};
const index = {
fontFamily: SERIF, fontSize: pt(8.6), lineHeight: pt(11.4),
indent: em(1), turnoverIndent: em(2), // sub-entries step in 1 em; wrapped lines hang 2 em
// English: Chicago's short ranges (301–3; 298–300 keeps the digit that changes). Spanish
// writes both numbers in full and joins them with a hyphen (301-303).
rangeFormat: t({ en: 'chicago', es: 'full' }), rangeSeparator: t({ en: '–', es: '-' }),
main: { bold: true }, // the defining page in bold
see: { italic: true }, // See / See also, Véase / Véase también by the document's locale
groups: { ...label, fontSize: pt(9.5), letterSpacing: pt(0), color: col('accent') },
};
Chi compila un indice marca i punti in cui un termine viene spiegato, non ogni punto in cui la parola compare. I due capitoli usano stroke volume dodici volte, e l’indice manda il lettore in due posti: pagina 298, dove il termine è definito, e 299–300, dove il testo spiega che cosa lo regola. Una marca visibile, :index[cardiac output]{main}, stampa le sue parole e le archivia. Una invisibile, mitral:index{term="heart!valves!mitral"}, non stampa nulla e prende la pagina della parola che la precede: è così che il passo sulle quattro valvole archivia mitral, tricuspid, aortic e pulmonary sotto heart senza riformulare la frase. Il ! separa i livelli, quindi heart ne riceve tre: le valvole, poi ciascuna valvola. Una marca funziona anche in un titolo (gli intervalli di 14.1 e 15.6 cominciano lì) e in un riquadro (heart failure è definito nella nota laterale di pagina 300).
#2 · Pagine principali, intervalli e rimandi
// The chapters mark each term where the text discusses it:
// the :index[stroke volume]{main} prints the words, files them, bold page
// mitral:index{term="heart!valves!mitral"} prints nothing, files the word before it
// ## Venous return :index{term="venous return" range="start" main} … range="end"
// :index{term="inotropy" see="contractility"} a cross-reference, no page
// The last chapter is `# Index {style="index"}` and `:::index`. buildBundle hands it every
// chapter's marks with the pages they landed on, and lays the book out again until those
// pages stop moving.
const indexStyle = {
id: 'index', numbered: false, toc: false, // not counted: no chapter 16
layout: { layoutType: 'double', gutterWidth: mm(6) }, // two columns in this section only
advancedDesign: opener(INDEX_BAND), // the chapters' band, shallower and with no number
header: runningHeads(t({ en: 'Index', es: 'Índice analítico' })),
};
const index = {
fontFamily: SERIF, fontSize: pt(8.6), lineHeight: pt(11.4),
indent: em(1), turnoverIndent: em(2), // sub-entries step in 1 em; wrapped lines hang 2 em
// English: Chicago's short ranges (301–3; 298–300 keeps the digit that changes). Spanish
// writes both numbers in full and joins them with a hyphen (301-303).
rangeFormat: t({ en: 'chicago', es: 'full' }), rangeSeparator: t({ en: '–', es: '-' }),
main: { bold: true }, // the defining page in bold
see: { italic: true }, // See / See also, Véase / Véase también by the document's locale
groups: { ...label, fontSize: pt(9.5), letterSpacing: pt(0), color: col('accent') },
};
main mette in grassetto la pagina che definisce un termine, così uno studente che cerca preload va prima a pagina 299. range="start" e range="end", con lo stesso termine, delimitano una trattazione che scavalca un’interruzione di pagina: stroke volume, regulation, 299–300, baroreceptor reflex, 303–4. Un main sull’inizio rende grassetto tutto l’intervallo. Una pagina marcata di nuovo dentro il proprio intervallo vi si fonde, e un main in quel punto rende grassetto l’intervallo. see prende il posto dei numeri di pagina (inotropy. See contractility), e seealso li segue (heart failure, 300. See also ejection fraction). Un bersaglio con più livelli si scrive con ! e si stampa con i due punti: See blood pressure: mean arterial.
#3 · Un capitolo indice su due colonne
// The chapters mark each term where the text discusses it:
// the :index[stroke volume]{main} prints the words, files them, bold page
// mitral:index{term="heart!valves!mitral"} prints nothing, files the word before it
// ## Venous return :index{term="venous return" range="start" main} … range="end"
// :index{term="inotropy" see="contractility"} a cross-reference, no page
// The last chapter is `# Index {style="index"}` and `:::index`. buildBundle hands it every
// chapter's marks with the pages they landed on, and lays the book out again until those
// pages stop moving.
const indexStyle = {
id: 'index', numbered: false, toc: false, // not counted: no chapter 16
layout: { layoutType: 'double', gutterWidth: mm(6) }, // two columns in this section only
advancedDesign: opener(INDEX_BAND), // the chapters' band, shallower and with no number
header: runningHeads(t({ en: 'Index', es: 'Índice analítico' })),
};
const index = {
fontFamily: SERIF, fontSize: pt(8.6), lineHeight: pt(11.4),
indent: em(1), turnoverIndent: em(2), // sub-entries step in 1 em; wrapped lines hang 2 em
// English: Chicago's short ranges (301–3; 298–300 keeps the digit that changes). Spanish
// writes both numbers in full and joins them with a hyphen (301-303).
rangeFormat: t({ en: 'chicago', es: 'full' }), rangeSeparator: t({ en: '–', es: '-' }),
main: { bold: true }, // the defining page in bold
see: { italic: true }, // See / See also, Véase / Véase también by the document's locale
groups: { ...label, fontSize: pt(9.5), letterSpacing: pt(0), color: col('accent') },
};
L’indice è l’ultimo documento Markdown del libro, # Index {style="index"} seguito da :::index. Lo stile del titolo sostituisce la colonna e mezza dei capitoli con due colonne a 6 mm l’una dall’altra, solo nella propria sezione, e dà all’indice un’apertura e una testatina proprie. L’indice è composto nel carattere del testo a 8,6/11,4 pt, un quadratone per ogni sottolivello, e una riga di rientro sporge di due quadratoni, così una riga che va a capo non si allinea mai a una sottovoce. rangeFormat: 'chicago' elimina le cifre che un intervallo ripete (303–4, ma 299–300). L’edizione spagnola scrive gli intervalli per intero con un trattino (303-304), l’uso comune nei libri spagnoli.
#4 · Perché i numeri seguono il testo
const text = chapters.map((chapter) => chapter.markdown).join('\n');
await loadSvg('pv-loop.svg', pvLoop());
await prepareFonts(text, config, kitFonts(FONTS));
const docs = await withLoadedFonts(() => buildBundle({ chapters, config, resources }),
{ ...kitFonts(FONTS), text });
showPages(docs, { title: t({ en: 'Principles of Human Physiology',
es: 'Principios de fisiología humana' }) });
offerPdf(() => renderToPdf(docs, { fontProvider: fontsourceProvider, resourceBytes: imageBytes }),
`${RECIPE}.pdf`);
buildBundle impagina i capitoli in ordine e scopre che il terzo contiene :::index. Dopo il primo passaggio raccoglie ogni marca di ogni capitolo con la pagina su cui è caduta, e passa quell’elenco al capitolo indice come sua struttura. Poi impagina di nuovo il libro, fino a tre passaggi, finché nessun numero di pagina si sposta. Riscrivi un paragrafo del capitolo 14, cambia il formato o il corpo del testo, e l’indice si ristampa con le pagine nuove. Nel PDF ogni numero di pagina è un collegamento alla sua pagina.
#5 · Aperture e testatine
function opener(band = BAND, kicker = '') {
const onBand = { color: col('paper'), align: 'left', overflow: 'wrap' }; // wrap: never '…'
const inBand = band - MARGIN.top - 8; // the title's box: it stands on a line 8 mm above the foot
return { enabled: true, minHeight: mm(inBand + 8 + 31), slot: { elements: [
{ kind: 'box', id: 'band', style: { backgroundColor: col('accent') },
placement: { ...at('bleed', 'top-left', 0, 0), size: { height: mm(band) } } },
{ kind: 'text', id: 'kicker', content: kicker, ...label, ...onBand, fontSize: pt(8.5),
placement: at('container', 'top-left', 0, -6) },
{ kind: 'text', id: 'title', content: '{titleText}', ...onBand, fontFamily: SANS,
fontWeight: 800, fontSize: pt(30), lineHeight: 1.05, // a multiple, never pt()
verticalAlign: 'bottom', // one line or two, the title sits on the same line
placement: { ...at('container', 'top-left', 0, 0),
size: { width: mm(170), height: mm(inBand) } } },
{ kind: 'text', id: 'lead', content: '{attr.lead}', fontFamily: SERIF, italic: true,
fontSize: pt(10.5), lineHeight: 1.4, color: col('ink'), align: 'left', overflow: 'wrap',
inlineMarks: true, // *See* in the index's lead
placement: { ...at('container', 'top-left', 0, inBand + 15),
size: { width: mm(118) } } },
] } };
}
I capitoli e l’indice condividono un’unica apertura: una fascia dal rifilo fino a 62 mm (46 mm per l’indice), con il titolo composto sul suo piede da un riquadro allineato in basso, così un titolo su due righe e uno su una riga finiscono sulla stessa riga. L’occhiello stampa {chapterNumber}, che l’attributo del titolo {startAt=14} porta a 14, e page.pageNumbering.startAt: 297 colloca la parte dove starebbe nel libro. Lo stile dell’indice è numbered: false, quindi non diventa il capitolo 16.
La ricetta completa
// ═══ Postext Cookbook · Nº 072 · A textbook index that follows the text ═══════════════ // https://postext.dev/en/cookbook/back-of-book-index // Code: MIT · Text: original (CC BY 4.0) · Drawing: generated in code (CC BY 4.0) // Fonts: Literata, Libre Franklin (SIL OFL 1.1) · Needs postext ≥ 1.28.0 // Two chapters of a physiology textbook and the index that closes them, laid out by // buildBundle as one book: the terms are marked where the text discusses them, and the // index chapter prints them with the pages they land on. import { buildBundle, prepareFonts, withLoadedFonts, renderPageToCanvas, registerResourceImage, defaultResourceTypes, } from 'https://esm.sh/postext'; import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf'; const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'back-of-book-index'; // ─── 1 · Design ───────────────────────────────────────────────────────────── const palette = { // every colour in the config links to one of these ids ink: '#1f1a1c', // text: a warm near-black accent: '#9e1b32', // the one accent (7.4:1 on paper): bands, letter heads, numbers, labels tint: '#f7ebe9', // the loop in Figure 14.1 rule: '#d9c6c3', // hairlines muted: '#6b5f61', // running heads, notes, the colophon paper: '#ffffff', // type on the bands }; 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' } })), // The engine's defaults link to 'main-color': point it at the accent, so nothing prints blue. { id: 'main-color', name: 'accent (defaults)', value: { hex: palette.accent, model: 'hex' } }, ]; // A textbook trim, mirrored, in mm. The body is a column and a half: text in the main column, // figures' captions and clinical notes in the outer channel. const TRIM = { width: 210, height: 277 }; const MARGIN = { top: 24, bottom: 22, inner: 20, outer: 14 }; const LEAD = 13.6; // body leading in pt: the baseline grid const [SERIF, SANS] = ['Literata', 'Libre Franklin']; const [BAND, INDEX_BAND] = [62, 46]; // mm from the trim to the foot of the opener bands const HEAD_Y = 13; // mm from the top trim to the running heads const at = (to, edge, x, y) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } }); const label = { fontFamily: SANS, fontSize: pt(7.5), fontWeight: 700, letterSpacing: pt(1.3), textTransform: 'uppercase' }; // #region answer: marks in the chapters, and an index chapter set in two columns // The chapters mark each term where the text discusses it: // the :index[stroke volume]{main} prints the words, files them, bold page // mitral:index{term="heart!valves!mitral"} prints nothing, files the word before it // ## Venous return :index{term="venous return" range="start" main} … range="end" // :index{term="inotropy" see="contractility"} a cross-reference, no page // The last chapter is `# Index {style="index"}` and `:::index`. buildBundle hands it every // chapter's marks with the pages they landed on, and lays the book out again until those // pages stop moving. const indexStyle = { id: 'index', numbered: false, toc: false, // not counted: no chapter 16 layout: { layoutType: 'double', gutterWidth: mm(6) }, // two columns in this section only advancedDesign: opener(INDEX_BAND), // the chapters' band, shallower and with no number header: runningHeads(t({ en: 'Index', es: 'Índice analítico' })), }; const index = { fontFamily: SERIF, fontSize: pt(8.6), lineHeight: pt(11.4), indent: em(1), turnoverIndent: em(2), // sub-entries step in 1 em; wrapped lines hang 2 em // English: Chicago's short ranges (301–3; 298–300 keeps the digit that changes). Spanish // writes both numbers in full and joins them with a hyphen (301-303). rangeFormat: t({ en: 'chicago', es: 'full' }), rangeSeparator: t({ en: '–', es: '-' }), main: { bold: true }, // the defining page in bold see: { italic: true }, // See / See also, Véase / Véase también by the document's locale groups: { ...label, fontSize: pt(9.5), letterSpacing: pt(0), color: col('accent') }, }; // #endregion // #region opener: a band across the top of the page: kicker, title and lead from the heading function opener(band = BAND, kicker = '') { const onBand = { color: col('paper'), align: 'left', overflow: 'wrap' }; // wrap: never '…' const inBand = band - MARGIN.top - 8; // the title's box: it stands on a line 8 mm above the foot return { enabled: true, minHeight: mm(inBand + 8 + 31), slot: { elements: [ { kind: 'box', id: 'band', style: { backgroundColor: col('accent') }, placement: { ...at('bleed', 'top-left', 0, 0), size: { height: mm(band) } } }, { kind: 'text', id: 'kicker', content: kicker, ...label, ...onBand, fontSize: pt(8.5), placement: at('container', 'top-left', 0, -6) }, { kind: 'text', id: 'title', content: '{titleText}', ...onBand, fontFamily: SANS, fontWeight: 800, fontSize: pt(30), lineHeight: 1.05, // a multiple, never pt() verticalAlign: 'bottom', // one line or two, the title sits on the same line placement: { ...at('container', 'top-left', 0, 0), size: { width: mm(170), height: mm(inBand) } } }, { kind: 'text', id: 'lead', content: '{attr.lead}', fontFamily: SERIF, italic: true, fontSize: pt(10.5), lineHeight: 1.4, color: col('ink'), align: 'left', overflow: 'wrap', inlineMarks: true, // *See* in the index's lead placement: { ...at('container', 'top-left', 0, inBand + 15), size: { width: mm(118) } } }, ] } }; } // #endregion // #region running-heads: book title on the verso, chapter on the recto, folios outside function runningHeads(recto) { const head = (id, content, parity, edge, x, extra = {}) => ({ kind: 'text', id, content, parity, pages: 'body', ...label, fontWeight: 600, color: col('muted'), // never on openers placement: at('page', edge, x, HEAD_Y), ...extra }); const folio = { fontSize: pt(8.5), fontWeight: 700, letterSpacing: pt(0), color: col('accent') }; const { outer } = MARGIN; return { elements: [ head('verso-folio', '{pageNumber}', 'even', 'top-left', outer, folio), head('verso-title', '{title}', 'even', 'top-left', outer + 10), head('recto-title', recto, 'odd', 'top-right', -(outer + 10)), head('recto-folio', '{pageNumber}', 'odd', 'top-right', -outer, folio), ] }; } const header = runningHeads(t({ en: 'Chapter {chapterNumber} · {chapterTitle}', es: 'Capítulo {chapterNumber} · {chapterTitle}' })); // Openers carry a drop folio at the foot, on the outer edge. const footer = { elements: [{ kind: 'text', id: 'drop-folio', content: '{pageNumber}', pages: 'opener', ...label, fontSize: pt(8.5), color: col('accent'), align: 'right', placement: { ...at('page', 'bottom-right', -MARGIN.outer, -12) } }] }; // #endregion const note = { fontFamily: SANS, fontSize: pt(8), lineHeight: pt(11.3), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), textAlign: 'left', firstLineIndent: pt(0) }; const config = { locale: t({ en: 'en-gb', es: 'es' }), // hyphenation and the index's sort order // The built-in types in the edition's language; their captions stand in the channel. resourceTypes: defaultResourceTypes(LANG).map((type) => ({ ...type, defaultPlacement: { captionSide: true } })), colorPalette, page: { sizePreset: 'custom', width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150, pageNumbering: { startAt: 297 }, // this part of the book opens on page 297 margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner), right: mm(MARGIN.outer), mirror: true } }, layout: { layoutType: 'oneAndHalf', sideColumnPercent: 28, sideColumnRole: 'floats', sideColumnSide: 'outer', gutterWidth: mm(7) }, // main column 119.7 mm, channel 49.3 mm bodyText: { fontFamily: SERIF, fontSize: pt(9.6), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'), textAlign: 'justify', firstLineIndent: mm(4), indentAfterHeading: false, hyphenateAcrossColumns: false }, // no word is divided over a page turn headings: { fontFamily: SANS, color: col('ink'), fontWeight: 700, levels: [ // parity 'odd': the next recto; the default 'always-odd' also leaves a blank page before it. { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'odd' }, marginBottom: pt(0), numberingTemplate: '{1}', advancedDesign: opener(BAND, t({ en: 'Chapter {chapterNumber}', es: 'Capítulo {chapterNumber}' })) }, { level: 2, fontSize: pt(12), lineHeight: pt(LEAD * 1.5), numberingTemplate: '{1}.{2}', color: col('accent'), marginTop: pt(LEAD), marginBottom: pt(0) }, ] }, headingStyles: [indexStyle], index, paragraphStyles: [{ id: 'formula', textAlign: 'center', firstLineIndent: pt(0), italic: true, marginTop: pt(LEAD * 0.5), marginBottom: pt(LEAD * 0.5) }], calloutStyles: [ { id: 'clinical', span: 'side', backgroundEnabled: false, padding: { top: mm(2.4), right: pt(0), bottom: pt(0), left: pt(0) }, stripe: { enabled: true, side: 'top', width: pt(2.5), color: col('accent') }, titleStyle: { ...label, color: col('accent'), gap: mm(1.6) }, body: note, marginTop: pt(0), marginBottom: pt(LEAD) }, { id: 'colophon', span: 'page', placement: 'bottom', backgroundEnabled: false, padding: { top: mm(2), right: pt(0), bottom: pt(0), left: pt(0) }, stripe: { enabled: true, side: 'top', width: pt(0.5), color: col('rule') }, body: { ...note, fontSize: pt(7), lineHeight: pt(9.5), color: col('muted') } }, ], captionStyle: { fontFamily: SANS, fontSize: pt(7.8), labelColor: col('accent'), gap: mm(2) }, tableStyle: { rules: 'horizontal', borderColor: col('rule'), borderWidth: pt(0.5), headerBackground: col('ink'), headerColor: col('paper'), headerFontFamily: SANS, bodyFontFamily: SANS, bodyFontSize: pt(8.2), bodyColor: col('ink'), cellPadding: mm(1.4) }, header, footer, }; // ─── 2 · Content ──────────────────────────────────────────────────────────── const heart = String.raw`---Esempio Markdown · 68 righe · content.en.md
title: "Principles of Human Physiology" subtitle: "Part IV · The Circulation" author: "Elena Marsh and Tomás Ibarra" --- # The heart as a pump {startAt=14 lead="Two muscular pumps in series move the whole blood volume round the body about once a minute at rest. This chapter follows one heartbeat from the first electrical signal to the last drop of blood ejected, and then asks what sets the size of each beat."} The human heart is a pair of pumps built into a single organ. The right side receives venous blood from the body and drives it through the lungs; the left side receives the oxygenated blood and drives it round the systemic circulation. Because the two are connected in series, they must, over any period longer than a few beats, move exactly the same volume of blood. Most of what follows concerns the left ventricle, but the principles apply to both sides of the heart: the right ventricle does the same work against a pressure about one-fifth as high.:index{term="ventricle!right"} ## The cardiac cycle :index{term="cardiac cycle" range="start" main} The sequence of events from the beginning of one heartbeat to the beginning of the next is the cardiac cycle. At a resting heart rate of 72 beats per minute each cycle lasts 0.83 s, of which ventricular :index[systole] (contraction) takes about 0.3 s and :index[diastole] (relaxation) the remaining 0.5 s. When the heart rate rises, diastole shortens far more than systole: at 180 beats per minute the whole cycle lasts 0.33 s and less than half of it is left for filling. The flow of blood through the heart is governed entirely by pressure differences and by four one-way valves. The mitral:index{term="heart!valves!mitral"} and tricuspid:index{term="heart!valves!tricuspid"} valves separate atria from ventricles; the aortic:index{term="heart!valves!aortic"} and pulmonary:index{term="heart!valves!pulmonary"} valves guard the exits of the ventricles. A valve opens when the pressure behind it exceeds the pressure ahead of it and closes when the gradient reverses. Nothing else moves them. It is convenient to divide the cycle into five phases. **Atrial systole.**:index{term="cardiac cycle!atrial systole"} Contraction of the atria pushes a final volume of blood into ventricles that are already mostly full. At rest this atrial kick:index{term="atrial kick" see="cardiac cycle!atrial systole"} adds only 10–20 % of ventricular filling; it matters more at high heart rates and in a stiff ventricle, and its loss in atrial fibrillation:index{term="atrial fibrillation"} is felt mainly on exertion. **Isovolumetric contraction.**:index{term="cardiac cycle!isovolumetric contraction"} As the ventricle begins to contract, its pressure rises above atrial pressure and the mitral valve closes, producing the first heart sound.:index{term="heart!sounds"} For about 0.05 s all four valves are shut. The ventricle contracts around a fixed volume of blood and its pressure climbs steeply. **Ejection.**:index{term="cardiac cycle!ejection"} When left ventricular pressure exceeds aortic pressure, about 80 mmHg, the aortic valve opens and blood leaves the ventricle, rapidly at first and then more slowly. Ventricular and aortic pressures rise together to a peak of about 120 mmHg. **Isovolumetric relaxation.**:index{term="cardiac cycle!isovolumetric relaxation"} As the muscle relaxes, ventricular pressure falls below aortic pressure; the aortic valve closes and gives the second heart sound, and a brief notch, the incisura,:index{term="incisura"} appears on the aortic pressure trace.:index{term="heart!sounds"} Again the ventricle is a closed chamber, now relaxing at constant volume. **Ventricular filling.**:index{term="cardiac cycle!filling"} Once ventricular pressure drops below atrial pressure, the mitral valve opens and blood that has collected in the atrium during systole rushes in. Most filling occurs in this first third of diastole; the middle third adds little, and atrial systole completes the process. ## The pressure–volume loop Plotting left ventricular pressure against volume turns the cycle into a single closed curve, the :index[pressure–volume loop]{main} (:ref{id="pv-loop"}). The ventricle ends diastole holding about 120 mL, the :index[end-diastolic volume]{main}, and ends systole with about 50 mL, the :index[end-systolic volume]. The difference, some 70 mL, is the :index[stroke volume]{main}. The fraction of the end-diastolic volume that is ejected, here 70/120 or 58 %, is the :index[ejection fraction]{main}; values between 55 and 70 % are normal, and the ejection fraction is the single number most often used to describe the pumping performance of a ventricle. The area enclosed by the loop is the external work done by the ventricle in one beat, the :index[stroke work]. Each side of the loop corresponds to one phase of the cycle, and each corner to the opening or closing of a valve, so a single loop records the whole beat.:index{term="cardiac cycle" range="end"} ## Cardiac output The volume of blood pumped by each ventricle per minute is the :index[cardiac output]{main}. It is the product of stroke volume and heart rate: 70 mL × 72 beats per minute gives about 5 L/min in a resting adult, which means that the entire blood volume passes through each side of the heart roughly once a minute. Because output scales with body size, it is often divided by body surface area;:index{term="body surface area"} the resulting :index[cardiac index] is about 3 L/min per square metre. During maximal exercise cardiac output rises four- to fivefold in an untrained young adult and to 30–35 L/min in an endurance athlete,:index{term="athletes, endurance"} whose larger heart achieves this mainly through a greater stroke volume.:index{term="exercise!cardiac output"} Every change in cardiac output is a change in :index[heart rate] or in stroke volume, or in both. The rest of this chapter considers each in turn. ## The sinoatrial node and the conduction system :index{term="heart!conduction system" range="start"} Cardiac muscle does not need a nerve to contract. The beat starts in the :index[sinoatrial node]{main}, a small strip of specialised muscle in the wall of the right atrium near the entry of the superior vena cava. Its cells have no stable resting potential: after each action potential the membrane depolarises slowly, largely through the so-called :index[funny current], until it reaches threshold and fires again. Isolated from all nervous influence, the node fires about 100 times a minute.:index{term="pacemaker" see="sinoatrial node"} The resting rate of 60–80 beats per minute is lower because the :index[vagus nerve] continuously releases :index[acetylcholine] onto the node and slows this pacemaker depolarisation. From the node the impulse spreads through both atria and converges on the :index[atrioventricular node]{main}, which conducts at only about 0.05 m/s.:index{term="AV node" see="atrioventricular node"} The resulting delay of roughly 0.1 s lets the atria finish emptying before the ventricles contract. Beyond the node the impulse runs down the :index[bundle of His] and its branches into the :index[Purkinje fibres], which conduct at up to 4 m/s and activate the whole ventricular mass within about 0.08 s. The sinoatrial node sets the pace only because it is the fastest. If it fails, the atrioventricular node takes over at 40–60 beats per minute, and the Purkinje fibres, if conduction through the node is blocked, at 15–40.:index{term="heart block"}:index{term="heart!conduction system" range="end"} ## Regulating stroke volume Three factors determine how much blood the ventricle ejects with each beat: the volume it holds before it contracts, the pressure it must overcome, and the strength of its contraction. :index{term="stroke volume!regulation" range="start"} **Preload and the Frank–Starling mechanism.** The more a ventricle is filled during diastole, the more forcefully it contracts and the more blood it ejects. This intrinsic property of heart muscle is the :index[Frank–Starling mechanism]{main}, after the German physiologist Otto Frank:index{term="Frank, Otto"} and the British physiologist Ernest Starling,:index{term="Starling, Ernest"} who described it in 1895 and 1914. Stretching a cardiac muscle fibre brings its :index[sarcomeres]{term="sarcomere"} towards an optimal length of about 2.2 µm, at which the overlap of actin and myosin filaments, and the sensitivity of the filaments to calcium, are greatest.:index{term="calcium!contraction"} The degree of stretch before contraction is the :index[preload]{main}, usually taken as the end-diastolic volume. The mechanism keeps the two ventricles in step: if the right ventricle pumps a little more for a few beats, more blood reaches the left ventricle, which stretches and ejects the extra volume. **Afterload.** The :index[afterload]{main seealso="blood pressure"} is the load the ventricle works against once it starts to eject, which for the left ventricle is essentially the aortic pressure. A sudden rise in afterload means the aortic valve opens later and closes earlier, so the ventricle ejects less and is left with a larger end-systolic volume. Over the next few beats the Frank–Starling mechanism restores the stroke volume at the cost of a larger heart. :::callout{type="clinical" title="Heart failure"} In :index[heart failure]{main} the heart cannot pump enough blood for the needs of the body at normal filling pressures. When the ejection fraction falls to 40 % or less, the condition is called heart failure with reduced ejection fraction;:index{term="heart failure!reduced ejection fraction"} many patients, however, have a normal ejection fraction and a stiff ventricle that fills poorly.:index{term="heart failure!preserved ejection fraction"} Older texts use the term cardiac insufficiency.:index{term="cardiac insufficiency" see="heart failure"}:index{term="heart failure" seealso="ejection fraction"} ::: **Contractility.** A ventricle can also eject more blood from the same end-diastolic volume if its fibres contract more strongly. This property, :index[contractility]{main} or inotropy,:index{term="inotropy" see="contractility"} is raised above all by the :index[sympathetic nervous system]{term="sympathetic nervous system!heart"}. Noradrenaline:index{term="noradrenaline"} released from sympathetic nerves acts on beta-1 :index[adrenergic receptors]{term="adrenergic receptors!beta-1"} of the muscle cells, increases calcium entry:index{term="calcium!contraction"} during each action potential, and makes each contraction stronger and shorter. The same transmitter acting on the sinoatrial node increases the heart rate, so sympathetic stimulation raises cardiac output through both of its factors at once.:index{term="norepinephrine" see="noradrenaline"} These three mechanisms rarely act alone. During exercise, venous return increases the preload, sympathetic activity raises contractility and heart rate, and dilatation of the muscle arterioles keeps the afterload from rising in proportion to the flow.:index{term="stroke volume!regulation" range="end"}:index{term="exercise!stroke volume"} ## Listening to the heart :index{term="heart!sounds" range="start" main} The closure of the valves can be heard through the chest wall, and auscultation with the :index[stethoscope], which the French physician René Laennec:index{term="Laennec, René"} introduced in 1816, remains the first examination of the heart. The first heart sound, low-pitched and relatively long, marks the closure of the mitral and tricuspid valves at the start of systole; it is best heard over the apex. The second, shorter and sharper, marks the closure of the aortic and pulmonary valves at the end of ejection. During inspiration the fall in intrathoracic pressure increases venous return to the right ventricle, which then ejects for slightly longer, so the pulmonary valve closes a few hundredths of a second after the aortic valve: the second sound is heard split.:index{term="heart!sounds!splitting of the second sound"} A third heart sound in early diastole comes from the rapid inflow of blood into the ventricle. It is normal in children and young athletes, but after the age of about forty it usually means a dilated, failing ventricle.:index{term="heart failure"} A fourth sound, just before the first, is produced by atrial systole against a stiff ventricle.:index{term="heart!sounds!third and fourth"} Between the sounds the heart is normally silent, because blood flows smoothly through open valves. A :index[murmur]{term="murmurs" seealso="turbulent flow"} is the noise of turbulent flow: through a narrowed valve that must still open (stenosis) or through a valve that fails to close (regurgitation). Its timing in the cycle tells the examiner which valve is at fault; an aortic stenosis, for example, produces a murmur during ejection, between the first and the second sounds.:index{term="heart!sounds" range="end"}:index{term="heart!valves!stenosis and regurgitation"}`; // chapter 14, with the book's frontmatter (content.<lang>.md) const vessels = String.raw`# Vessels and blood pressure {lead="The heart supplies the pressure; the vessels decide where the blood goes. This chapter explains how a few micrometres of change in the radius of small arteries redistribute the cardiac output, and how the brainstem keeps arterial pressure steady from one heartbeat to the next."}Esempio Markdown · 54 righe · content.vessels.en.md
Blood leaves the left ventricle in pulses and reaches the capillaries as a steady stream. Between the two lies a branching system of tubes whose walls differ in thickness, in elastic tissue and in smooth muscle, and whose properties explain most of what follows: why arterial pressure has a systolic and a diastolic value, why pressure falls so steeply in the smallest arteries, and why most of the blood in the body is found in the veins. ## The vascular tree The :index[aorta] and its large branches are :index[arteries]{term="arteries!elastic"} rich in elastic fibres. They expand as each stroke volume enters and recoil during diastole, so that flow continues between beats. Smaller :index[arteries]{term="arteries!muscular"} carry more smooth muscle and distribute blood to the organs. The :index[arterioles]{main}, 10–100 µm across, have the thickest walls for their size and are the main site of resistance to flow: as :ref{id="pressures"} shows, mean pressure falls from about 85 to 35 mmHg across them. By contracting or relaxing their smooth muscle, the arterioles set both the total resistance of the circulation and the share of the cardiac output that each organ receives. The :index[capillaries]{main} are tubes of a single layer of endothelial cells,:index{term="endothelium"} 5–8 µm in diameter, just wide enough for a red cell to squeeze through. There are some ten billion of them, with a total exchange surface of 500–700 m², and blood moves through them at about 0.3 mm/s, a thousandth of its mean velocity in the aorta.:index{term="blood flow!velocity"} Blood returns through :index[venules] and :index[veins]{main}, whose walls are thin and easily distended. At rest the systemic veins hold about 64 % of the blood volume, which is why they are called capacitance vessels.:index{term="capacitance vessels" see="veins"} ## Flow, pressure and resistance Flow through any vessel obeys a relation analogous to :index[Ohm's law]: flow equals the pressure difference between the two ends divided by the resistance, *Q* = (*P*~1~ − *P*~2~)/*R*. Applied to the whole systemic circulation, with a mean arterial pressure of 93 mmHg, a right atrial pressure close to zero and a cardiac output of 5 L/min, it gives a :index[total peripheral resistance]{term="resistance, vascular!total peripheral"} of about 19 mmHg·min/L.:index{term="total peripheral resistance" see="resistance, vascular!total peripheral"} What determines the resistance of a vessel? For steady, streamlined flow of a fluid through a rigid tube, the answer is :index[Poiseuille's law]{main}, derived in the 1840s by the French physician Jean Poiseuille:index{term="Poiseuille, Jean"} from experiments on water flowing through glass capillaries. The resistance is proportional to the length of the tube and to the :index[viscosity] of the fluid, and inversely proportional to the fourth power of the radius, *r*^4^. Halving the radius of an arteriole multiplies its resistance by sixteen; a rise in radius of only 19 % doubles the flow through it at the same pressure. This extreme sensitivity is what gives the arterioles their control over the circulation. The viscosity of blood, about three to four times that of water, depends mainly on the haematocrit, and it rises steeply when the haematocrit exceeds 60 %.:index{term="haematocrit"} Poiseuille's law assumes :index[laminar flow], in which the blood moves in concentric layers, fastest at the centre. When velocity is high, the vessel wide or the wall irregular, the layers break into eddies. The tendency to such :index[turbulent flow]{seealso="Reynolds number"} is expressed by the :index[Reynolds number], the product of the density of blood, its velocity and the diameter of the vessel divided by the viscosity. Above about 2000 flow becomes turbulent, and turbulence is audible: a murmur:index{term="murmurs"} over a narrowed valve, a :index[bruit] over a stenosed artery, and the :index[Korotkoff sounds] heard when blood pressure is measured with a cuff. Vessels arranged in series add their resistances; vessels arranged in parallel add their conductances, so the total resistance of a parallel network is lower than that of any one of its branches.:index{term="resistance, vascular!series and parallel"} The organs of the body are supplied in parallel, which allows each to adjust its own blood flow without much effect on the others. ## Arterial pressure :index{term="blood pressure" range="start"} In a healthy young adult arterial pressure rises to about 120 mmHg with each ejection, the :index[systolic pressure]{term="blood pressure!systolic"}, and falls to about 80 mmHg before the next, the :index[diastolic pressure]{term="blood pressure!diastolic"}. The difference, 40 mmHg, is the :index[pulse pressure]{term="blood pressure!pulse pressure"}. Because diastole lasts about twice as long as systole at rest, the average pressure over the cycle lies closer to the diastolic value. It is estimated as the diastolic pressure plus one-third of the pulse pressure: :::paragraphs{style="formula"} MAP = DBP + (SBP − DBP)/3 = 80 + 40/3 = 93 mmHg ::: This :index[mean arterial pressure]{term="blood pressure!mean arterial" main} is the pressure that drives blood through the systemic circulation. At high heart rates diastole shortens and the true mean moves towards the arithmetic mean of systolic and diastolic pressure.:index{term="mean arterial pressure" see="blood pressure!mean arterial"} The pulse pressure depends mainly on two quantities: the stroke volume and the :index[arterial compliance]{term="compliance, arterial" main}, the change in arterial volume per unit change in pressure. The elastic arteries store about half of each stroke volume during systole and return it during diastole, an arrangement known as the :index[Windkessel effect]{main} after the air chamber of eighteenth-century fire engines, which turned the strokes of the pump into a steady jet. With age the aorta stiffens,:index{term="ageing!arteries"} compliance falls, and the same stroke volume produces a higher systolic and a lower diastolic pressure. :::callout{type="clinical" title="Hypertension"} Sustained :index[hypertension]{main} is diagnosed when the arterial pressure measured at rest on repeated occasions is 140/90 mmHg or higher; American guidelines since 2017 use 130/80 mmHg. In older people a high systolic pressure with a normal or low diastolic pressure, isolated systolic hypertension, reflects the loss of aortic compliance.:index{term="hypertension!isolated systolic"}:index{term="ageing!arteries"} ::: ## Capillary exchange Across the capillary wall, fluid movement is determined by the balance between hydrostatic pressure, which pushes fluid out, and the :index[colloid osmotic pressure]{main} of the plasma proteins,:index{term="oncotic pressure" see="colloid osmotic pressure"} which holds it in. These :index[Starling forces]{term="capillaries!Starling forces"} favour filtration at the arterial end of the capillary, where the hydrostatic pressure:index{term="capillaries!hydrostatic pressure"} is about 35 mmHg against an oncotic pressure of 25 mmHg, and reabsorption at the venous end, where the hydrostatic pressure has fallen to about 15 mmHg. Of the 20 L or so filtered each day in the systemic capillaries, some 17 L return directly to the blood; the rest is carried back by the :index[lymph]{main} vessels. When filtration outstrips this return, fluid gathers in the tissues as oedema.:index{term="oedema"}:index{term="Starling forces" see="capillaries!Starling forces"} ## The baroreceptor reflex :index{term="baroreceptor reflex" range="start" main} Arterial pressure is held within a few millimetres of mercury of its set point from moment to moment by the baroreceptor reflex. :index[Baroreceptors]{term="baroreceptors"} are stretch-sensitive nerve endings in the wall of the :index[carotid sinus]{term="baroreceptors!carotid sinus"}, at the bifurcation of each common carotid artery, and in the :index[aortic arch]{term="baroreceptors!aortic arch"}. Their firing rises with each systolic distension. Signals from the carotid sinus travel in the :index[glossopharyngeal nerve] and those from the aortic arch in the :index[vagus nerve]{seealso="baroreceptor reflex"}; both end in the :index[nucleus tractus solitarius] of the medulla.:index{term="medulla oblongata"} A rise in pressure increases baroreceptor firing, which inhibits the sympathetic outflow to the heart and vessels and increases vagal activity. Heart rate and contractility fall, the arterioles dilate, and pressure returns towards normal.:index{term="sympathetic nervous system!vessels"} A fall in pressure has the opposite effects. The carotid receptors respond between about 60 and 180 mmHg and are most sensitive around the normal mean arterial pressure. Within one or two days, however, they reset to any pressure that is sustained, so the reflex corrects rapid changes but cannot, by itself, set the long-term level of arterial pressure, which depends on the handling of salt and water by the kidneys.:index{term="kidneys!long-term control of pressure"}:index{term="blood pressure!long-term control"}:index{term="blood pressure" range="end"} The reflex is tested every time a person stands up.:index{term="standing, circulatory effects of"} Some 500 mL of blood pools in the veins of the legs, venous return and stroke volume fall, and arterial pressure starts to drop; within a few seconds the reflex raises the heart rate and constricts the arterioles. When it fails, as it may in the elderly or in diseases of the autonomic nerves, standing causes a fall in systolic pressure of 20 mmHg or more, :index[orthostatic hypotension]{term="hypotension, orthostatic" main}, with dizziness or fainting.:index{term="fainting" see="hypotension, orthostatic"}:index{term="baroreceptor reflex" range="end"}:index{term="venous return"} ## Venous return :index{term="venous return" range="start" main} Whatever the heart pumps out must first come back to it. The flow of blood from the veins into the right atrium, the venous return, is driven by a small pressure difference: about 7 mmHg between the venules and the right atrium, where the :index[central venous pressure] is close to zero. Because the veins are so compliant, a small change in their tone moves a large volume of blood. Sympathetic constriction of the veins can shift several hundred millilitres from the peripheral veins towards the heart within seconds, raising the preload and, through the Frank–Starling mechanism, the stroke volume.:index{term="sympathetic nervous system!vessels"} Two pumps outside the heart help. In the legs, the deep veins run between the muscles and contain one-way valves every few centimetres.:index{term="veins!valves"} Each contraction of the calf squeezes the veins and drives blood upwards, and the valves stop it from falling back; this :index[skeletal muscle pump]{main} can lower the venous pressure at the ankle of a walking person from about 90 mmHg to 25 mmHg or less. When the valves fail, the veins of the legs dilate and become tortuous, as :index[varicose veins].:index{term="veins!varicose" see="varicose veins"} In the chest, each inspiration lowers the intrathoracic pressure and raises the abdominal pressure, drawing blood from the abdominal veins into the thorax: the :index[respiratory pump]. Over any period longer than a few beats, venous return and cardiac output are equal. This is why the heart, although it generates the pressure, is not the only organ that sets the output: in a healthy person at rest, the cardiac output follows the venous return, and the venous return follows the metabolic needs of the tissues, each of which controls its own blood flow through its arterioles.:index{term="venous return" range="end"}:index{term="cardiac output!venous return"}`; // chapter 15 (content.vessels.<lang>.md) const indexChapter = String.raw`# Index {style="index" lead="Numbers in bold mark the principal discussion of a term. A range such as 301–3 means that the discussion runs across those pages. *See* sends you to the entry that holds the page numbers; *see also* to a related entry."}Esempio Markdown · 6 righe · content.index.en.md
:::index :::callout{type="colophon"} *Principles of Human Physiology*, Part IV, chapters 14 and 15, a sample set with Postext. Set in Literata and Libre Franklin (SIL OFL 1.1). Text and drawing: original, CC BY 4.0. The book and its authors are fictional. :::`; // # Index and :::index (content.index.<lang>.md) const chapters = [heart, vessels, indexChapter].map((markdown) => ({ markdown })); // #region art: the pressure–volume loop of Figure 14.1, drawn in code // Paths only: the drawing carries no lettering, and the caption reads it. const PX = 10; // SVG pixels per unit const [W, H] = [100, 52]; const vx = (volume) => 12 + volume * 0.56; // 0–150 mL across const py = (pressure) => 47 - pressure * 0.3; // 0–140 mmHg up const pv = (v, p) => `${vx(v).toFixed(1)} ${py(p).toFixed(1)}`; function pvLoop() { const stroke = (id, w, extra = '') => `fill="none" stroke="${palette[id]}" stroke-width="${w}" ${extra}`; const loop = `M${pv(50, 6)}Q${pv(88, 2)} ${pv(120, 10)}L${pv(120, 80)}` + `C${pv(108, 128)} ${pv(70, 132)} ${pv(50, 100)}Z`; const arrow = (x, y, dx, dy) => `<path d="M${x} ${y}l${dx} ${dy}" ${stroke('ink', 0.5)}/>` + `<path d="M${x + dx} ${y + dy}${dx ? 'l-2.6 -1.2v2.4z' : 'l-1.2 2.6h2.4z'}"` + ` fill="${palette.ink}"/>`; const guide = (v, p) => `<path d="M${pv(v, p)}V${py(0)}" ${stroke('muted', 0.35, 'stroke-dasharray="1.2 1"')}/>`; const dot = (v, p) => `<circle cx="${vx(v)}" cy="${py(p)}" r="1.5" fill="${palette.ink}"/>`; return `<svg xmlns="http://www.w3.org/2000/svg" width="${W * PX}" height="${H * PX}" ` + `viewBox="0 0 ${W} ${H}"><path d="${loop}" fill="${palette.tint}"/>` + `<path d="M${pv(10, 0)}L${pv(62, 130)}" ` + `${stroke('muted', 0.45, 'stroke-dasharray="2 1.4"')}/>` + guide(50, 6) + guide(120, 10) + `<path d="${loop}" ${stroke('accent', 1.1, 'stroke-linejoin="round"')}/>` + [[50, 6], [120, 10], [120, 80], [50, 100]].map(([v, p]) => dot(v, p)).join('') + arrow(vx(0), py(0), W - 16, 0) + arrow(vx(0), py(0), 0, -(py(0) - 3)) + '</svg>'; } // #endregion const TABLE = t({ en: [['Segment', 'Blood volume (%)', 'Mean pressure (mmHg)'], ['Arteries', '13', '100 to 85'], ['Arterioles', '7', '85 to 35'], ['Capillaries', '', '35 to 15'], ['Venules and veins', '64', '15 to 0'], ['Heart', '7', '–'], ['Pulmonary circulation', '9', '15 to 8']], es: [['Segmento', 'Volumen de sangre (%)', 'Presión media (mmHg)'], ['Arterias', '13', '100 a 85'], ['Arteriolas', '7', '85 a 35'], ['Capilares', '', '35 a 15'], ['Vénulas y venas', '64', '15 a 0'], ['Corazón', '7', '–'], ['Circulación pulmonar', '9', '15 a 8']], }); // Arterioles and capillaries share one volume cell: the cell under a rowSpan stays in the row, // marked hiddenBy (gotcha: merged-cells-hiddenby). const rows = TABLE.map((row, r) => row.map((content, c) => ({ content, ...(r === 0 && { isHeader: true }), ...(r === 2 && c === 1 && { rowSpan: 2 }), ...(r === 3 && c === 1 && { hiddenBy: { row: 2, col: 1 } }) }))); const resources = [ { id: 'pv-loop', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0, svg: { fileId: 'pv-loop.svg', width: W * PX, height: H * PX }, caption: t({ en: 'Pressure–volume loop of the left ventricle at rest: volume from 0 to 150 mL across, ' + 'pressure from 0 to 140 mmHg up. The dots, anticlockwise from bottom right: mitral ' + 'valve closes (120 mL), aortic valve opens (80 mmHg), aortic valve closes, mitral ' + 'valve opens (50 mL). Dashed: the end-systolic pressure–volume relation.', es: 'Bucle presión-volumen del ventrículo izquierdo en reposo: volumen de 0 a 150 mL en ' + 'horizontal, presión de 0 a 140 mmHg en vertical. Los puntos, en sentido antihorario ' + 'desde abajo a la derecha: cierre de la válvula mitral (120 mL), apertura de la aórtica ' + '(80 mmHg), cierre de la aórtica y apertura de la mitral (50 mL). A trazos, la relación ' + 'presión-volumen telesistólica.' }), altText: t({ en: 'A closed loop of ventricular pressure against volume.', es: 'Un bucle cerrado de presión ventricular frente a volumen.' }) }, { id: 'pressures', typeId: 'table', kind: 'table', createdAt: 0, updatedAt: 0, caption: t({ en: 'Blood volume and mean pressure along the circulation of a resting adult.', es: 'Volumen de sangre y presión media a lo largo de la circulación de un adulto en ' + 'reposo.' }), table: { model: { headerRowCount: 1, columnWidths: [2.2, 1.4, 1.6], rows } } }, ]; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── // Every face the design uses, loaded before the first build with the ones the config names. const FONTS = { Literata: ['400', '400i', '700', '700i'], 'Libre Franklin': ['400', '600', '700', '800'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── // #region build: one book of three documents; one PDF whose index numbers are links const text = chapters.map((chapter) => chapter.markdown).join('\n'); await loadSvg('pv-loop.svg', pvLoop()); await prepareFonts(text, config, kitFonts(FONTS)); const docs = await withLoadedFonts(() => buildBundle({ chapters, config, resources }), { ...kitFonts(FONTS), text }); showPages(docs, { title: t({ en: 'Principles of Human Physiology', es: 'Principios de fisiología humana' }) }); offerPdf(() => renderToPdf(docs, { fontProvider: fontsourceProvider, resourceBytes: imageBytes }), `${RECIPE}.pdf`); // #endregionKit · core, fonts, viewer, pdf, images: uguale in ogni ricetta · 315 righe
// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ───── // ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook function mm(value) { return { value, unit: 'mm' }; } function pt(value) { return { value, unit: 'pt' }; } function em(value) { return { value, unit: 'em' }; } /** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */ function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; } /** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */ function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; } // ─── Kit · fonts v3 ── the same in every recipe · postext.dev/cookbook // Postext loads the faces a config asks for before layout (prepareFonts, // buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the // files the PDF embeds too. /** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts: * the Fontsource resolver, and the faces of FONTS loaded with the ones the * config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */ function kitFonts(FONTS = {}) { const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) => ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' }))); return { resolve: fontsourceResolver, faces }; } /** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek * when `text` needs them and the family ships them (kitSubsetsFor), each with * its unicode-range. Null for a weight or slant Fontsource does not ship. */ async function fontsourceResolver(family, weight, style, { text = '' } = {}) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null; kitStatus('Loading fonts…'); const at = await fontsourceAt(id); return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({ source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`, unicodeRange: kitRanges()[subset], })); } /** The unicode-range of each Fontsource file the kit loads (a function: the * kit is inlined after the pen's code, which runs before a const would be set). */ function kitRanges() { return { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', greek: 'U+0370-03FF', }; } /** For the cjk, arabic and comics blocks, which add their own files: the * faces of `faces` = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With * `optional`, a face Fontsource does not ship is skipped instead of failing. * The files are added once all have loaded, in the order asked (latin, then * latin-ext, then greek), whatever order they arrive in: the last one added * wins a character two files hold. Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = kitRanges(); const jobs = []; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal']) .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await const at = fontsourceAt(id); const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null; const subsets = ['latin', ...kitSubsetsFor(text, meta)]; for (const [weight, style] of todo) { if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `${await at}files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().catch(() => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return ready.filter((face) => face && document.fonts.add(face)).length; } /** A loaded FontFace covers this family, weight and style (fonts.check() would * also say yes for families nobody declared). */ function hasFace(family, weight, style) { for (const face of document.fonts) { if (face.status !== 'loaded' || face.style !== style) continue; if (face.family.replace(/^["']|["']$/g, '') !== family) continue; const [low, high = low] = face.weight.split(' ').map(Number); if (weight >= low && weight <= high) return true; } return false; } /** The files beyond latin `text` needs that `meta`'s family ships. */ function kitSubsetsFor(text, meta) { return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']] .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x); } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** A package's folder at the one version @5 names now (#766). */ function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) { return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json()) .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`); } /** The family's Fontsource metadata (weights, styles, subsets), or null. */ function fontsourceMeta(family) { fontsourceMeta.cache ??= new Map(); const id = fontsourceId(family); if (!fontsourceMeta.cache.has(id)) { fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`) .then((res) => (res.ok ? res.json() : null), () => null)); } return fontsourceMeta.cache.get(id); } // ─── Kit · viewer v2 ── the same in every recipe · postext.dev/cookbook /** The pages as spreads on a dark desk, page 1 alone, then verso | recto, * each painted when it scrolls near. Other options go to renderPageToCanvas. */ function showPages(docs, { title, width = 460, ...render } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { ...render, scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── Kit · pdf v3 ── the same in every recipe that exports a PDF /** The Fontsource files the screen used, as TrueType, with their range and * place: the nearest weight the family ships, upright with no italic; * latin, then what its letters need (kitSubsetsFor). */ async function fontsourceProvider(family, weight, style, request) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); const weights = meta?.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style; const text = String.fromCodePoint(...(request?.codePoints ?? [])); const files = await Promise.all(['latin', ...kitSubsetsFor(text, meta)].map(async (subset, order) => { const res = await fetch(`${await fontsourceAt(id)}files/${id}-${subset}-${w}-${s}.woff2`); if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} ${subset}`); return Object.assign(await decompressWoff2(new Uint8Array(await res.arrayBuffer())), { unicodeRange: kitRanges()[subset], order }); })); return files.length === 1 ? files[0] : files; } /** A "Build the PDF" button; then "Open the PDF" (a new tab: CodePen's frame * shows no PDFs) and a download link. */ function offerPdf(makePdf, filename) { viewer(); const button = Object.assign(document.createElement('button'), { type: 'button', textContent: 'Build the PDF' }); button.dataset.postextPdf = filename; button.addEventListener('click', async () => { button.disabled = true; button.textContent = 'Building the PDF…'; try { const bytes = await makePdf(); const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' })); const size = `${Math.max(1, Math.round(bytes.length / 1024))} KB`; button.replaceWith( Object.assign(document.createElement('a'), { href: url, target: '_blank', rel: 'noopener', textContent: 'Open the PDF ↗' }), Object.assign(document.createElement('a'), { href: url, download: filename, textContent: `Download ${filename} · ${size}` })); } catch (error) { button.disabled = false; button.textContent = 'Build the PDF'; kitFail(error); } }); document.getElementById('pt-actions').append(button); } // ─── Kit · images v3 ── 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 as a vector image, its fonts inside (inlineSvgFonts, ≥ 1.25). * `options` go to registerResourceImage: { singleInk: true } tints it on the canvas. */ async function loadSvg(fileId, svg, options) { if (typeof inlineSvgFonts === 'function') svg = await inlineSvgFonts(svg, svgFaces); const img = new Image(); img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; await img.decode(); registerResourceImage(fileId, img, options); (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg)); } /** A face's Fontsource files: nearest weight; latin + what the SVG sets. */ async function svgFaces(family, weight, style, { codePoints = [] } = {}) { const [id, m] = [fontsourceId(family), await fontsourceMeta(family)]; const w = m?.weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)) ?? weight; const s = m?.styles.includes(style) === false ? 'normal' : style; return Promise.all(['latin', ...kitSubsetsFor(String.fromCodePoint(...codePoints), m)].map(async (x) => new Uint8Array( await (await fetch(`${await fontsourceAt(id)}files/${id}-${x}-${w}-${s}.woff2`)).arrayBuffer()))); } /** 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 ───────────────────────────────────────────────────────────────────────
Lo script.js composto funziona così com’è: incollalo nello script modulo di qualsiasi pagina, oppure apri la ricetta su CodePen. Cartella della ricetta su GitHub ↗ (si apre in una nuova scheda)
Varianti
#Scrivi ogni intervallo per intero
Alcune norme redazionali stampano per intero entrambi i numeri di un intervallo.
- rangeFormat: t({ en: 'chicago', es: 'full' }), rangeSeparator: t({ en: '–', es: '-' }),
+ rangeFormat: 'full', rangeSeparator: t({ en: '–', es: '-' }),#Un indice senza lettere di gruppo
Un indice breve spesso fa scorrere i gruppi uno dopo l’altro, separati solo da una riga bianca.
- groups: { ...label, fontSize: pt(9.5), letterSpacing: pt(0), color: col('accent') },
+ groups: { enabled: false },Errori frequenti
- La cattura di questa ricetta non segnala
indexSeeUnknown: un rimando il cui bersaglio è scritto in modo diverso dalla voce (Reynolds number contro number, Reynolds) si stampa senza proteste. Confronta ogni See dell’indice finito con il suo bersaglio. - Un elenco di sottovoci che si spezza tra due colonne non ripete la sua voce principale come riga heart (cont.); una voce senza una pagina propria resta sempre con la sua prima sottovoce, così un titolo di voce non chiude mai da solo una colonna.
Crediti
- Ricetta
- Ignacio Ferro
- Testo
- Testo originale, CC BY 4.0
- Font
- Literata (SIL OFL 1.1) · Libre Franklin (SIL OFL 1.1)
- Codice
- MIT, come Postext
Modifica questo testo ↗ (si apre in una nuova scheda)Cartella della ricetta su GitHub ↗ (si apre in una nuova scheda)


