Kurz gesagt
Einige Seiten aus Alice im Wunderland mit den Anmerkungen eines Herausgebers im Seitenrand. Jede Anmerkung steht neben den Zeilen, die sie erklärt, und ist mit einem farbigen Buchstaben markiert.
Was Sie setzen
Den Anfang von Kapitel VII aus Alice im Wunderland, die verrückte Teegesellschaft, gesetzt als drei Seiten einer kommentierten Ausgabe im Endformat 156 × 234 mm. Carrolls Text läuft in einer Spalte Blocksatz aus der Unna, rund 66 Zeichen pro Zeile. Ein 33 mm breiter Kanal am Außensteg nimmt die acht Glossen des Herausgebers auf, jede neben dem Absatz mit ihrem Buchstaben, sodass sie auf einer rechten Seite rechts und auf einer linken Seite links stehen. Glossen zu Wörtern und Wortspielen sind grün, Glossen zur Geschichte rot, und jeder Buchstabe im Text trägt die Farbe seiner Glosse. Das Kapitel beginnt unter einem Aquarell des Teetischs von oben, gefolgt von der Kapitelnummer, dem Titel in Rozha One und einem kursiven Vorspann mit grüner Initiale; die Anmerkung zu dieser Ausgabe steht im Rand neben dem Vorspann.
Dieses Rezept beantwortet
- Wie setze ich Marginalien oder Glossen neben den Absatz, den sie erläutern?
- Wie mache ich Fußnoten?
- Wie setze ich ein Layout mit anderthalb Spalten, mit breiter Textspalte und schmaler Seitenspalte?
- Wie füge ich einem Auftakt eine Autorenzeile, einen Vorspann oder einen Einstieg mit Initiale hinzu?
Die kurze Antwort
const layout = {
layoutType: 'oneAndHalf', // one text column and a narrower side column
sideColumnPercent: 26, // of the 127 mm between the margins: a 33 mm channel
sideColumnRole: 'floats', // no text runs in it: it holds side boxes and figures
sideColumnSide: 'outer', // at the fore-edge, which mirrored margins move page by page
gutterWidth: mm(4.5), // the text column keeps 127 − 33 − 4.5 = 89.5 mm
};
// A gloss is :::callout{type="note" span="side" title="a · …"} in the Markdown.
// span="side" moves it out of the flow into the channel, level with the block after
// the fence, so the fence goes just before the paragraph that carries its letter.
// Glosses never float. One that meets the gloss above stacks under it. One that would
// run past the column's foot slides up, as far as the gloss above allows, until its
// foot sits on the foot; if it still does not fit, it waits for the next page
// (gotcha: side-box-starts-at-fence).
const gloss = (id, hue) => ({ id,
backgroundEnabled: false, // one device: a hairline over the gloss, in its colour
stripe: { enabled: true, side: 'top', width: pt(0.5), color: col(hue) },
padding: { top: mm(1.5), right: pt(0), bottom: pt(0), left: pt(0) },
// Cormorant SC draws lower case as small capitals: the title needs no textTransform.
titleStyle: { fontFamily: LABEL, fontWeight: 600, fontSize: pt(8.5),
letterSpacing: pt(0.3), color: col(hue), gap: mm(0.8) },
// Face and colours come from bodyText. A box body also inherits its 4 mm first-line
// indent, which a gloss sets back to 0.
body: { fontSize: pt(7.8), lineHeight: pt(10), textAlign: 'left',
firstLineIndent: pt(0) } });
const calloutStyles = [gloss('note', 'lawn'), gloss('context', 'jam')]; // words, history
Zutaten
- Funktionen
- MarginalienRandspalte für GleitobjekteAnderthalb SpaltenGespiegelte RänderKästenHoch- und TiefstellungGestaltete AuftakteKapitelband über die ganze BreiteText, Linien und Kästen in SeitengestaltungenBilder in SeitengestaltungenGestaltungselemente verankernInitialen in AuftaktenÜberschriftenattributeKolumnentitel und SeitenzahlenKopfzeilen nach SeitenrolleAbbildungen und Tabellen als RessourcenSemantische FarbpaletteOptimaler Zeilenumbruch (Knuth-Plass)Inline-Chips
- Nutzt außerdem
- FußnotenPapierfarbe
- Schrift
- Unna, Rozha One, Cormorant SC (SIL OFL 1.1)
- Dateien
tea-table-1560.jpg- The tea-table seen from above, a watercolour (Generated With Diffusion Models, eigener Text)
Zubereitung
#1 · Den Außensteg den Glossen geben
Der Code steht oben in der kurzen Antwort. In einem Layout mit anderthalb Spalten gibt sideColumnPercent: 26 der Randspalte 33 mm der 127 mm zwischen den Rändern, und dem Text bleiben nach dem Spaltenabstand von 4,5 mm noch 89,5 mm. sideColumnRole: 'floats' hält Carroll aus der Randspalte heraus; dort steht nur, was mit span: 'side' hineingestellt wird. Eine Fußnote wartet am Fuß der Spalte unter einer Linie; im Rand steht jede Glosse auf Höhe der Zeile, die sie erklärt, und die Glossen nehmen der Textspalte keine Zeilen weg. Die beiden Glossenformate unterscheiden sich nur in der Farbe der Haarlinie und des Titels, sodass man eine Glosse zu einem Wort von einer Glosse zur Geschichte unterscheidet, bevor man sie liest.
#2 · Die Seite spiegeln, damit der Kanal dem Außensteg folgt
const PT = 25.4 / 72; // mm in a point
const [TRIM_W, TRIM_H] = [156, 234]; // mm
const [TOP, BOTTOM, INNER, OUTER] = [22, 22, 15, 14]; // mm: 38 lines of text
const page = { width: mm(TRIM_W), height: mm(TRIM_H), dpi: 150,
backgroundColor: col('paper'),
// left is a recto's inner margin; mirror swaps the sides on a verso, and 'outer' moves
// the channel with them: right on a recto, left on a verso.
margins: { top: mm(TOP), bottom: mm(BOTTOM), left: mm(INNER), right: mm(OUTER),
mirror: true } };
const CONTENT_W = TRIM_W - INNER - OUTER; // 127 mm
const SIDE_W = CONTENT_W * layout.sideColumnPercent / 100; // 33 mm: the glosses' measure
const TEXT_W = CONTENT_W - SIDE_W - layout.gutterWidth.value;
Mit mirror: true tauschen die Ränder auf jeder linken Seite die Plätze, und sideColumnSide: 'outer' nimmt den Kanal mit, sodass die Glossen auf Seite 81 und Seite 83 rechts und auf Seite 82 links stehen. Kopf- und Fußsteg von je 22 mm lassen 190 mm frei, Platz für 38 Zeilen zu 14 pt, und jede Glosse beginnt auf einer Zeile desselben Rasters.
#3 · Jede Glosse vor ihren Absatz setzen
const bodyText = { // justified and hyphenated (en-us) by default
// Copy-fitted to the 89.5 mm column: at 9.5 pt '“Have some wine,” the March Hare said in
// an encouraging tone.' fits one line. At 10.4 pt every measure from 77 to 93 mm left
// it a short last line: 'couraging tone.', 'aging tone.', 'ing tone.' or 'tone.'.
fontFamily: TEXT, fontSize: pt(9.5), lineHeight: pt(LEAD), color: col('ink'),
boldColor: col('ink'), italicColor: col('ink'), firstLineIndent: mm(4),
// The chapter's first paragraph is set flush, though the fences of glosses a and b stand
// between the heading and it: a box that leaves the flow is not the block after a heading.
indentAfterHeading: false,
// Unna's space is narrow, 0.22 em. At the default 0.6 the line breaker set 'Alice felt
// dreadfully puzzled. The Hatter’s remark seemed to have' with its spaces at 0.69.
minWordSpacing: 0.75,
// The dictionary divides the heroine's name, 'said Al-' | 'ice indignantly' on page 81.
// The list keeps it whole wherever it stands; every other word is divided as before.
hyphenation: { exceptions: ['Alice'] } };
Ein Kasten mit span="side" an seinem Direktivenblock :::callout verlässt den Textfluss und steht im Kanal auf Höhe des Blocks nach dem Direktivenblock. Deshalb steht der Direktivenblock jeder Glosse unmittelbar vor dem Absatz mit ihrem Buchstaben, wie unten. Die Glossen a und b gehören neben den ersten Absatz, also stehen ihre Direktivenblöcke zwischen der Überschrift und diesem Absatz. Ein Kasten, der den Fluss verlässt, zählt nicht als Block nach der Überschrift, und so setzt indentAfterHeading: false diesen Absatz weiterhin stumpf. Der Grundtext ist auf die Spalte eingepasst: Bei 9,5 pt passt der Absatz, der mit „Have some wine,“ beginnt, auf eine Zeile von 89,5 mm, während bei 10,4 pt jede Satzbreite zwischen 77 und 93 mm eine zu kurze Ausgangszeile wie „ing tone.“ oder „tone.“ hinterließ. Der Wortabstand der Unna ist schmal, 0,22 em, und beim Standardwert von 0,6 für minWordSpacing wurden die Abstände einer Zeile auf Seite 83 auf 0,69 dieser Breite gestaucht; minWordSpacing: 0.75 schließt diese Zeile aus. Auf Seite 81 trennte das Wörterbuch den Namen der Heldin in „Al-“ und „ice“, daher steht dieser Absatz in :::paragraphs{style="whole"}, einem Format mit hyphenation: false; seine einzige Trennung ist ein weiches Trennzeichen (U+00AD), das in „together“ eingetippt ist.
:::callout{type="context" span="side" title="c · Hair"}
Tenniel drew Alice with long, loose hair. …
:::
“Your hair wants cutting,”:chip[^c^]{style="context"} said the Hatter. …#4 · Jeden Buchstaben in der Farbe seiner Glosse drucken
// A letter in the text is a chip named after its gloss's type, :chip[^a^]{style="note"}:
// the Markdown has no mark for coloured text, and a chip style sets a colour. With no
// fill, outline, padding or gap the chip adds no width, so the lines break as they would
// with a plain ^a^.
const letter = (id, hue) => ({ id, backgroundEnabled: false, borderWidth: pt(0),
paddingX: pt(0), gap: pt(0), color: col(hue), bold: true });
const chipStyles = [letter('note', 'lawn'), letter('context', 'jam')];
Markdown kennt keine Auszeichnung für farbigen Text, aber ein Format für Chips legt eine Textfarbe fest. Jeder Buchstabe ist darum ein Chip, dessen Format denselben Namen trägt wie der Typ seiner Glosse. :chip[^c^]{style="context"} druckt ein rotes c, im Rot von Haarlinie und Titel der Glosse c. Ohne Füllung, Kontur, Innenabstand oder Abstand nimmt der Chip keine Breite ein, und jede Zeile bricht genau dort um, wo sie es mit einem einfachen ^c^ täte.
#5 · Das Kapitel unter dem Teetisch eröffnen
const HEADPIECE = 72; // mm: the depth of the painting at the head of the page
const HEADNOTE = { size: 9.8, lead: LEAD }; // pt: the headnote keeps the body's leading
const CAPS = { text: 0.597, initial: 0.56 }; // cap heights, em: Unna and Rozha One
// The initial's size: its capital runs from the first line's cap height down to the
// last baseline it spans. At the default size it stops short of the first line's capitals.
const dropSize = (lines) => pt(((lines - 1) * HEADNOTE.lead + CAPS.text * HEADNOTE.size)
/ CAPS.initial);
const below = (id, y, width) => ({ anchor: { to: `#${id}`, edge: 'below' },
offset: { y: mm(y) }, size: { width } });
// The opener reserves the height of its lowest element, rounded up to the grid, and the
// heading's default bottom margin adds one blank line: with a four-line headnote the
// text starts on line 23. The kicker, the title and the headnote hang below the painting.
const opener = { enabled: true, slot: { elements: [
// An image element draws a resource without number or caption. It hangs from the
// trim's corner, over the top margin and both side margins.
{ kind: 'image', id: 'headpiece', resourceId: 'tea-table',
placement: { anchor: { to: 'page', edge: 'top-left' },
size: { width: mm(TRIM_W) } } },
// The numeral comes from the heading line,
// # A Mad Tea-Party {num="VII" headnote="…" …}: the excerpt stands alone, so
// {chapterNumber} would count its one heading and print 1.
{ kind: 'text', id: 'kicker', content: 'Chapter {attr.num}', fontFamily: LABEL,
fontWeight: 600, fontSize: pt(9), letterSpacing: pt(2), textTransform: 'uppercase',
color: col('jam'), align: 'left',
placement: { anchor: { to: 'container', edge: 'top-left' },
offset: { y: mm(HEADPIECE - TOP + 8) } } }, // 8 mm under the painting's foot
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY,
fontSize: pt(40), lineHeight: 1.05, color: col('ink'), align: 'left', // wraps when longer
placement: below('kicker', 2, 'fill') },
// The drop cap opens the headnote, the editor's voice, italic and ragged; Carroll's
// text opens in the flow, justified.
{ kind: 'text', id: 'headnote', content: '{attr.headnote}', fontFamily: TEXT,
italic: true, fontSize: pt(HEADNOTE.size),
lineHeight: pt(HEADNOTE.lead), color: col('ink'),
align: 'left',
dropCap: { lines: 2, fontFamily: DISPLAY, fontSize: dropSize(2), color: col('lawn'),
gap: mm(1.5) },
placement: below('title', 5, mm(TEXT_W)) },
// The note on this edition stands in the channel, level with the headnote's top.
{ kind: 'text', id: 'edition-label', content: 'This edition', fontFamily: LABEL,
fontWeight: 600, fontSize: pt(8.5), letterSpacing: pt(0.3), color: col('muted'),
align: 'left', placement: { anchor: { to: '#headnote', edge: 'right-of' },
offset: { x: layout.gutterWidth }, size: { width: mm(SIDE_W) } } },
{ kind: 'text', id: 'edition', content: '{attr.source}', fontFamily: TEXT,
fontSize: pt(7.5), lineHeight: pt(10), color: col('muted'), align: 'left',
placement: { anchor: { to: '#edition-label', edge: 'below' }, offset: { y: mm(0.8) },
size: { width: mm(SIDE_W) } } },
] } };
Der Gestaltungsplatz der Überschrift reicht über die ganze Seitenbreite, damit das Gemälde bis an den Beschnitt gehen kann. Dachzeile, Titel und Vorspann hängen unter dem Gemälde, und der Kapitelauftakt reserviert Platz bis zum untersten von ihnen, aufgerundet auf das Raster; der Standardabstand unter der Überschrift fügt eine Leerzeile hinzu. Mit einem vierzeiligen Vorspann beginnt der Text auf Zeile 23, eine fünfte Zeile würde ihn auf Zeile 24 verschieben. Kapitelnummer, Vorspann und Anmerkung zur Ausgabe stammen aus Überschriftenattributen, {num="VII" headnote="…" source="…"}. Die Nummer kommt von dort, weil {chapterNumber} in einem Auszug, der ein eigenes Dokument ist, eine 1 drucken würde. Die Initiale eröffnet den Vorspann des Herausgebers, kursiv im Flattersatz, und Carrolls Text beginnt darunter im Blocksatz. dropSize() bemisst die Initiale so, dass ihre Oberkante mit den Versalien der ersten Vorspannzeile abschließt.
#6 · Jede Glosse nach ihrem Gegenstand färben
const palette = {
ink: '#1f1b1a', // text: a warm near-black
lawn: '#1e6f6b', // glosses on words and jokes, the drop cap
jam: '#b23a48', // glosses on history, the kicker
muted: '#6e645b', // running heads, the note on this edition
paper: '#f8f3e6', // the page, and the tablecloth
};
// col(id): a colour linked to its palette entry, with the entry's hex beside the id.
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const entry = (id, hex, name = id) => ({ id, name, value: { hex, model: 'hex' } });
const colorPalette = [
...Object.entries(palette).map(([id, hex]) => entry(id, hex)),
// The engine's defaults link to 'main-color': aimed at the green, nothing prints blue.
entry('main-color', palette.lawn, 'lawn (defaults)'),
];
Rasengrün kennzeichnet Glossen zu Wörtern, Redensarten und Wortspielen, Marmeladenrot Personen, Daten und die Textgeschichte. Ein neuer Wert für jam im selben Objekt palette färbt die Glossen zur Geschichte, ihre Buchstaben und die Dachzeile in einem Schritt um; das Aquarell darüber wurde wegen desselben Rasengrüns und Marmeladenrots ausgewählt.
Das ganze Rezept
// ═══ Postext Cookbook · Nº 032 · Annotated classic with margin glosses ════════════ // https://postext.dev/en/cookbook/annotated-classic-glosses // Code: MIT · Text: Lewis Carroll (PD), glosses (CC BY 4.0) · Headpiece: diffusion models // Fonts: Unna, Rozha One, Cormorant SC (SIL OFL 1.1) · Needs postext ≥ 1.28.0 // The mad tea-party as an annotated edition: the text keeps to one column, and its glosses stand // in the outer margin beside the lines they explain, changing sides with the spread. import { buildDocumentWithFonts, renderPageToCanvas, registerResourceImage, } from 'https://esm.sh/postext'; const LANG = 'en'; // @lang: the language of the sample document ('en') const RECIPE = 'annotated-classic-glosses'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: a lawn green and a jam red on cream paper const palette = { ink: '#1f1b1a', // text: a warm near-black lawn: '#1e6f6b', // glosses on words and jokes, the drop cap jam: '#b23a48', // glosses on history, the kicker muted: '#6e645b', // running heads, the note on this edition paper: '#f8f3e6', // the page, and the tablecloth }; // col(id): a colour linked to its palette entry, with the entry's hex beside the id. const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id }); const entry = (id, hex, name = id) => ({ id, name, value: { hex, model: 'hex' } }); const colorPalette = [ ...Object.entries(palette).map(([id, hex]) => entry(id, hex)), // The engine's defaults link to 'main-color': aimed at the green, nothing prints blue. entry('main-color', palette.lawn, 'lawn (defaults)'), ]; // #endregion const [TEXT, DISPLAY, LABEL] = ['Unna', 'Rozha One', 'Cormorant SC']; const LEAD = 14; // body leading in pt: the grid of every page // #region answer: a float-only channel at the fore-edge, and two gloss styles for it const layout = { layoutType: 'oneAndHalf', // one text column and a narrower side column sideColumnPercent: 26, // of the 127 mm between the margins: a 33 mm channel sideColumnRole: 'floats', // no text runs in it: it holds side boxes and figures sideColumnSide: 'outer', // at the fore-edge, which mirrored margins move page by page gutterWidth: mm(4.5), // the text column keeps 127 − 33 − 4.5 = 89.5 mm }; // A gloss is :::callout{type="note" span="side" title="a · …"} in the Markdown. // span="side" moves it out of the flow into the channel, level with the block after // the fence, so the fence goes just before the paragraph that carries its letter. // Glosses never float. One that meets the gloss above stacks under it. One that would // run past the column's foot slides up, as far as the gloss above allows, until its // foot sits on the foot; if it still does not fit, it waits for the next page // (gotcha: side-box-starts-at-fence). const gloss = (id, hue) => ({ id, backgroundEnabled: false, // one device: a hairline over the gloss, in its colour stripe: { enabled: true, side: 'top', width: pt(0.5), color: col(hue) }, padding: { top: mm(1.5), right: pt(0), bottom: pt(0), left: pt(0) }, // Cormorant SC draws lower case as small capitals: the title needs no textTransform. titleStyle: { fontFamily: LABEL, fontWeight: 600, fontSize: pt(8.5), letterSpacing: pt(0.3), color: col(hue), gap: mm(0.8) }, // Face and colours come from bodyText. A box body also inherits its 4 mm first-line // indent, which a gloss sets back to 0. body: { fontSize: pt(7.8), lineHeight: pt(10), textAlign: 'left', firstLineIndent: pt(0) } }); const calloutStyles = [gloss('note', 'lawn'), gloss('context', 'jam')]; // words, history // #endregion // #region letters: the letter that calls each gloss, in the gloss's colour // A letter in the text is a chip named after its gloss's type, :chip[^a^]{style="note"}: // the Markdown has no mark for coloured text, and a chip style sets a colour. With no // fill, outline, padding or gap the chip adds no width, so the lines break as they would // with a plain ^a^. const letter = (id, hue) => ({ id, backgroundEnabled: false, borderWidth: pt(0), paddingX: pt(0), gap: pt(0), color: col(hue), bold: true }); const chipStyles = [letter('note', 'lawn'), letter('context', 'jam')]; // #endregion // #region page: a trade page with mirrored margins, so the channel is always at the fore-edge const PT = 25.4 / 72; // mm in a point const [TRIM_W, TRIM_H] = [156, 234]; // mm const [TOP, BOTTOM, INNER, OUTER] = [22, 22, 15, 14]; // mm: 38 lines of text const page = { width: mm(TRIM_W), height: mm(TRIM_H), dpi: 150, backgroundColor: col('paper'), // left is a recto's inner margin; mirror swaps the sides on a verso, and 'outer' moves // the channel with them: right on a recto, left on a verso. margins: { top: mm(TOP), bottom: mm(BOTTOM), left: mm(INNER), right: mm(OUTER), mirror: true } }; const CONTENT_W = TRIM_W - INNER - OUTER; // 127 mm const SIDE_W = CONTENT_W * layout.sideColumnPercent / 100; // 33 mm: the glosses' measure const TEXT_W = CONTENT_W - SIDE_W - layout.gutterWidth.value; // #endregion // #region text: book texture, and a first paragraph set flush const bodyText = { // justified and hyphenated (en-us) by default // Copy-fitted to the 89.5 mm column: at 9.5 pt '“Have some wine,” the March Hare said in // an encouraging tone.' fits one line. At 10.4 pt every measure from 77 to 93 mm left // it a short last line: 'couraging tone.', 'aging tone.', 'ing tone.' or 'tone.'. fontFamily: TEXT, fontSize: pt(9.5), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), firstLineIndent: mm(4), // The chapter's first paragraph is set flush, though the fences of glosses a and b stand // between the heading and it: a box that leaves the flow is not the block after a heading. indentAfterHeading: false, // Unna's space is narrow, 0.22 em. At the default 0.6 the line breaker set 'Alice felt // dreadfully puzzled. The Hatter’s remark seemed to have' with its spaces at 0.69. minWordSpacing: 0.75, // The dictionary divides the heroine's name, 'said Al-' | 'ice indignantly' on page 81. // The list keeps it whole wherever it stands; every other word is divided as before. hyphenation: { exceptions: ['Alice'] } }; // #endregion // #region opener: a headpiece, the chapter's numeral and title, a headnote with a drop cap const HEADPIECE = 72; // mm: the depth of the painting at the head of the page const HEADNOTE = { size: 9.8, lead: LEAD }; // pt: the headnote keeps the body's leading const CAPS = { text: 0.597, initial: 0.56 }; // cap heights, em: Unna and Rozha One // The initial's size: its capital runs from the first line's cap height down to the // last baseline it spans. At the default size it stops short of the first line's capitals. const dropSize = (lines) => pt(((lines - 1) * HEADNOTE.lead + CAPS.text * HEADNOTE.size) / CAPS.initial); const below = (id, y, width) => ({ anchor: { to: `#${id}`, edge: 'below' }, offset: { y: mm(y) }, size: { width } }); // The opener reserves the height of its lowest element, rounded up to the grid, and the // heading's default bottom margin adds one blank line: with a four-line headnote the // text starts on line 23. The kicker, the title and the headnote hang below the painting. const opener = { enabled: true, slot: { elements: [ // An image element draws a resource without number or caption. It hangs from the // trim's corner, over the top margin and both side margins. { kind: 'image', id: 'headpiece', resourceId: 'tea-table', placement: { anchor: { to: 'page', edge: 'top-left' }, size: { width: mm(TRIM_W) } } }, // The numeral comes from the heading line, // # A Mad Tea-Party {num="VII" headnote="…" …}: the excerpt stands alone, so // {chapterNumber} would count its one heading and print 1. { kind: 'text', id: 'kicker', content: 'Chapter {attr.num}', fontFamily: LABEL, fontWeight: 600, fontSize: pt(9), letterSpacing: pt(2), textTransform: 'uppercase', color: col('jam'), align: 'left', placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: mm(HEADPIECE - TOP + 8) } } }, // 8 mm under the painting's foot { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontSize: pt(40), lineHeight: 1.05, color: col('ink'), align: 'left', // wraps when longer placement: below('kicker', 2, 'fill') }, // The drop cap opens the headnote, the editor's voice, italic and ragged; Carroll's // text opens in the flow, justified. { kind: 'text', id: 'headnote', content: '{attr.headnote}', fontFamily: TEXT, italic: true, fontSize: pt(HEADNOTE.size), lineHeight: pt(HEADNOTE.lead), color: col('ink'), align: 'left', dropCap: { lines: 2, fontFamily: DISPLAY, fontSize: dropSize(2), color: col('lawn'), gap: mm(1.5) }, placement: below('title', 5, mm(TEXT_W)) }, // The note on this edition stands in the channel, level with the headnote's top. { kind: 'text', id: 'edition-label', content: 'This edition', fontFamily: LABEL, fontWeight: 600, fontSize: pt(8.5), letterSpacing: pt(0.3), color: col('muted'), align: 'left', placement: { anchor: { to: '#headnote', edge: 'right-of' }, offset: { x: layout.gutterWidth }, size: { width: mm(SIDE_W) } } }, { kind: 'text', id: 'edition', content: '{attr.source}', fontFamily: TEXT, fontSize: pt(7.5), lineHeight: pt(10), color: col('muted'), align: 'left', placement: { anchor: { to: '#edition-label', edge: 'below' }, offset: { y: mm(0.8) }, size: { width: mm(SIDE_W) } } }, ] } }; // #endregion // The running heads: the book on the verso, the chapter on the recto, folios at the fore-edge. const HEAD = 14; // mm from the top trim to the heads' baseline const INSET = 8; // mm from the folio's outer edge to the running head's const DROP = 13; // mm from the bottom trim up to the foot of the drop folio's box // A design text's first baseline sits 0.96 em below the top of its box: the default // lineHeight, 1.2, times 0.8, where the baseline falls in the line box. const BASELINE = 1.2 * 0.8; const baseline = (size) => mm(HEAD - BASELINE * size * PT); const head = (id, parity, content, x, extra = {}) => ({ kind: 'text', id, parity, content, pages: 'body', fontFamily: LABEL, fontWeight: 600, fontSize: pt(8.5), letterSpacing: pt(1.2), textTransform: 'uppercase', color: col('muted'), ...extra, placement: { anchor: { to: 'page', edge: parity === 'even' ? 'top-left' : 'top-right' }, offset: { x: mm(x), y: baseline(extra.fontSize?.value ?? 8.5) } } }); const folio = { fontFamily: TEXT, fontWeight: 700, fontSize: pt(9), letterSpacing: pt(0), color: col('ink') }; const header = { elements: [ head('verso-folio', 'even', '{pageNumber}', OUTER, folio), head('verso-title', 'even', '{title}', OUTER + INSET), head('recto-title', 'odd', '{chapterTitle}', -(OUTER + INSET)), head('recto-folio', 'odd', '{pageNumber}', -OUTER, folio), ] }; // The opener, a recto, carries a drop folio at the foot of its channel instead. const footer = { elements: [{ ...head('drop-folio', 'odd', '{pageNumber}', -OUTER, folio), pages: 'opener', placement: { anchor: { to: 'page', edge: 'bottom-right' }, offset: { x: mm(-OUTER), y: mm(-DROP) } } }] }; const config = () => ({ locale: 'en-us', // the chapter is in English: its hyphenation patterns, and the bundle's language colorPalette, page, layout, bodyText, calloutStyles, chipStyles, header, footer, headings: { fontFamily: DISPLAY, fontWeight: 400, color: col('ink'), // Rozha One has one weight levels: [ // 'odd' puts the opener on a recto; span 'page' lets its design cross the channel. { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'odd' }, advancedDesign: opener }, ], }, }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Markdown-Beispiel · 119 Zeilen · content.en.md
title: "Alice’s Adventures in Wonderland" author: "Lewis Carroll" --- # A Mad Tea-Party {num="VII" headnote="The tea-party is not in the book Carroll wrote out by hand for Alice Liddell in 1864; he added it, with the Cheshire Cat, for the book of 1865. Letters in the text point to the glosses beside it: green for words and jokes, red for history." source="Text after Project Gutenberg eBook 11; notes CC BY 4.0. Set in Unna, Rozha One and Cormorant SC (SIL OFL)."} :::callout{type="note" span="side" title="a · The March Hare"} ‘Mad as a March hare’ is an old saying. Hares are shy, but in early spring they race about the fields and rear up on their hind legs to box. ::: :::callout{type="context" span="side" title="b · The Hatter"} ‘Mad as a hatter’ was a saying before Carroll used it. Hatters made felt from fur treated with mercury, and the fumes gave many of them tremors and fits of shyness and temper. Carroll never calls him the Mad Hatter. ::: There was a table set out under a tree in front of the house, and the March Hare:chip[^a^]{style="note"} and the Hatter:chip[^b^]{style="context"} were having tea at it: a Dormouse was sitting between them, fast asleep, and the other two were using it as a cushion, resting their elbows on it, and talking over its head. “Very uncomfortable for the Dormouse,” thought Alice; “only, as it’s asleep, I suppose it doesn’t mind.” The table was a large one, but the three were all crowded together at one corner of it: “No room! No room!” they cried out when they saw Alice coming. “There’s *plenty* of room!” said Alice indignantly, and she sat down in a large arm-chair at one end of the table. “Have some wine,” the March Hare said in an encouraging tone. Alice looked all round the table, but there was nothing on it but tea. “I don’t see any wine,” she remarked. “There isn’t any,” said the March Hare. “Then it wasn’t very civil of you to offer it,” said Alice angrily. “It wasn’t very civil of you to sit down without being invited,” said the March Hare. “I didn’t know it was *your* table,” said Alice; “it’s laid for a great many more than three.” :::callout{type="context" span="side" title="c · Hair"} Tenniel drew Alice with long, loose hair. Alice Liddell, in Carroll’s photographs of her, wears hers short and dark, with a fringe. ::: “Your hair wants cutting,”:chip[^c^]{style="context"} said the Hatter. He had been looking at Alice for some time with great curiosity, and this was his first speech. “You should learn not to make personal remarks,” Alice said with some severity; “it’s very rude.” :::callout{type="context" span="side" title="d · The riddle"} Carroll made up the riddle without an answer. So many readers asked for one that in a preface of 1896 he offered this: ‘Because it can produce a few notes, tho they are very flat; and it is nevar put with the wrong end in front!’ Later printings changed *nevar*, raven spelt backwards, to *never*, and the joke was lost. ::: The Hatter opened his eyes very wide on hearing this; but all he *said* was, “Why is a raven like a writing-desk?”:chip[^d^]{style="context"} “Come, we shall have some fun now!” thought Alice. “I’m glad they’ve begun asking riddles.—I believe I can guess that,” she added aloud. “Do you mean that you think you can find out the answer to it?” said the March Hare. “Exactly so,” said Alice. “Then you should say what you mean,” the March Hare went on. “I do,” Alice hastily replied; “at least—at least I mean what I say—that’s the same thing, you know.” :::callout{type="note" span="side" title="e · I see what I eat"} The Hatter has the logic right. Turn a statement round and you get its converse, which can be false when the statement is true. Carroll, as Charles Dodgson, taught mathematics at Christ Church, Oxford, and wrote two books on logic. ::: “Not the same thing a bit!” said the Hatter. “You might just as well say that ‘I see what I eat’ is the same thing as ‘I eat what I see’!”:chip[^e^]{style="note"} “You might just as well say,” added the March Hare, “that ‘I like what I get’ is the same thing as ‘I get what I like’!” “You might just as well say,” added the Dormouse, who seemed to be talking in his sleep, “that ‘I breathe when I sleep’ is the same thing as ‘I sleep when I breathe’!” “It *is* the same thing with you,” said the Hatter, and here the conversation dropped, and the party sat silent for a minute, while Alice thought over all she could remember about ravens and writing-desks, which wasn’t much. The Hatter was the first to break the silence. “What day of the month is it?” he said, turning to Alice: he had taken his watch out of his pocket, and was looking at it uneasily, shaking it every now and then, and holding it to his ear. :::callout{type="context" span="side" title="f · The fourth"} Of May: Alice Liddell was born on 4 May 1852. ::: Alice considered a little, and then said “The fourth.”:chip[^f^]{style="context"} “Two days wrong!” sighed the Hatter. “I told you butter wouldn’t suit the works!” he added looking angrily at the March Hare. :::callout{type="note" span="side" title="g · The best butter"} Grocers sold butter by grade, and ‘best’ fetched the highest price. The March Hare defends its quality, which was never the trouble. ::: “It was the *best* butter,”:chip[^g^]{style="note"} the March Hare meekly replied. “Yes, but some crumbs must have got in as well,” the Hatter grumbled: “you shouldn’t have put it in with the bread-knife.” The March Hare took the watch and looked at it gloomily: then he dipped it into his cup of tea, and looked at it again: but he could think of nothing better to say than his first remark, “It was the *best* butter, you know.” Alice had been looking over his shoulder with some curiosity. “What a funny watch!” she remarked. “It tells the day of the month, and doesn’t tell what o’clock it is!” “Why should it?” muttered the Hatter. “Does *your* watch tell you what year it is?” “Of course not,” Alice replied very readily: “but that’s because it stays the same year for such a long time together.” “Which is just the case with *mine*,” said the Hatter. Alice felt dreadfully puzzled. The Hatter’s remark seemed to have no sort of meaning in it, and yet it was certainly English. “I don’t quite understand you,” she said, as politely as she could. “The Dormouse is asleep again,” said the Hatter, and he poured a little hot tea upon its nose. :::callout{type="note" span="side" title="h · The Dormouse"} Hazel dormice sleep through the day and hibernate for half the year, from autumn to spring. The name is often traced to the French *dormir*, to sleep. ::: The Dormouse:chip[^h^]{style="note"} shook its head impatiently, and said, without opening its eyes, “Of course, of course; just what I was going to remark myself.” “Have you guessed the riddle yet?” the Hatter said, turning to Alice again. “No, I give it up,” Alice replied: “what’s the answer?” “I haven’t the slightest idea,” said the Hatter. “Nor I,” said the March Hare. Alice sighed wearily. “I think you might do something better with the time,” she said, “than waste it in asking riddles that have no answers.” “If you knew Time as well as I do,” said the Hatter, “you wouldn’t talk about wasting *it*. It’s *him*.” “I don’t know what you mean,” said Alice. “Of course you don’t!” the Hatter said, tossing his head contemptuously. “I dare say you never even spoke to Time!”`; // content.<lang>.md, inlined by the Cookbook // The headpiece is a resource that only the opener's design draws: never cited, never placed. // It is a watercolour, a JPEG in assets/ cut to the trim's 156 × 72 mm, declared at its pixels. const resources = [{ id: 'tea-table', typeId: 'figure', kind: 'bitmap', createdAt: 0, updatedAt: 0, bitmap: { fileId: 'tea-table-1560.jpg', format: 'jpeg', width: 1560, height: 720 }, altText: 'The tea-table under a tree, seen from above: a long white cloth laid with cups, a red ' + 'teapot, bread and butter and a watch on its chain; the Dormouse asleep in a teapot at one ' + 'end and a red arm-chair at the other.' }]; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { // text, display and label faces, loaded before the build Unna: ['400', '400i', '700'], // 700: the folios and the gloss letters 'Rozha One': ['400'], 'Cormorant SC': ['600'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadImage('tea-table-1560.jpg', asset('tea-table-1560.jpg')); const continuation = { pageNumbering: { startAt: 81 } }; // chapter VII of a book: an odd folio const doc = await buildDocumentWithFonts({ markdown, resources, continuation }, config(), kitFonts(FONTS)); showPages(doc, { title: 'Annotated classic with margin glosses' });Kit · core, fonts, viewer, images: in jedem Rezept gleich · 270 Zeilen
// ─── 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 · 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 ───────────────────────────────────────────────────────────────────────
Das zusammengesetzte script.js läuft unverändert: Fügen Sie es in das Modulskript einer beliebigen Seite ein, oder öffnen Sie das Rezept auf CodePen. Ordner des Rezepts auf GitHub ↗ (öffnet in neuem Tab)
Varianten
#Die Glossen auf jeder Seite rechts halten
Für eine Ausgabe, die am Bildschirm Seite für Seite gelesen wird, schalten Sie die Spiegelung aus: 'outer' bedeutet dann auf jeder Seite den rechten Rand, und die Kolumnentitel bleiben an ihrem Platz, da sie am Beschnitt verankert sind.
- mirror: true } };
+ mirror: false } };#Lange Anmerkungen an den Seitenfuß schicken
Schreiben Sie eine Anmerkung, die für den Rand zu lang ist, als Fußnote, [^1] im Text und die Definition [^1]: unter dem Absatz: Postext setzt sie am Fuß der Textspalte, 89,5 mm breit unter einer kurzen Linie, und die Glossen behalten den Kanal. Fußnoten am Fuß der Spalte, die sie zitiert gestaltet diese Anmerkungen.
Häufige Fehler
Häufiger Fehler
Ein Seitenkasten beginnt auf Höhe des Blocks nach seiner Einfassung
In postext 1.4.1 steht ein Kasten mit span: 'side' in der Seitenspalte auf der Höhe, die der Text an seiner Einfassung erreicht hat, auf der nächsten Rasterlinie und unter einem bereits vorhandenen Kasten. Setzen Sie eine Glosse direkt vor den Absatz, den sie erläutert: Steht sie danach, beginnt sie neben dem nächsten Absatz. Ein Kasten, der über den Fuß der Spalte hinauslaufen würde, rutscht nach oben, bis sein Fuß auf dem Fuß der Spalte sitzt, soweit der Kasten darüber es zulässt; passt er dann immer noch nicht, wartet er auf die Seitenspalte der nächsten Seite. Marginalien →
- Glossen stapeln sich in der Reihenfolge des Markdowns untereinander, eine lange Glosse schiebt also die nächste nach unten. Auf Seite 81 fallen beide Buchstaben in die zweite Zeile des ersten Absatzes: Glosse a beginnt auf Höhe des Absatzes und Glosse b darunter, fünf Zeilen unter ihrem Buchstaben. Wo Buchstaben dicht beieinanderstehen, halten Sie die Glossen kurz.
Nachweise
- Rezept
- Ignacio Ferro
- Text
- Alice’s Adventures in Wonderland (1865), the opening of chapter VII, “A Mad Tea-Party” · Lewis Carroll · gemeinfrei
- The headnote, the eight glosses and the note on this edition · Ignacio Ferro · CC BY 4.0
- Bilder
- The tea-table seen from above, a watercolour · Generated With Diffusion Models · eigener Text
- Schriften
- Unna (SIL OFL 1.1) · Rozha One (SIL OFL 1.1) · Cormorant SC (SIL OFL 1.1)
- Code
- MIT, wie Postext
Diesen Text bearbeiten ↗ (öffnet in neuem Tab)Ordner des Rezepts auf GitHub ↗ (öffnet in neuem Tab)


