Em poucas palavras
Um livrinho com três cartas entre o rei da Prússia e Voltaire. Mostra como compor a data, a saudação e a assinatura de cada carta, com as cartas seguidas sem começar uma página nova.
O que você vai compor
Cinco páginas de Mon sort est changé, uma edição de 14 × 21 cm com três cartas em francês: as duas que Frederico II escreveu a Voltaire seis e doze dias depois de se tornar rei da Prússia, em junho de 1740, e a carta de Voltaire de 1º de abril de 1778, dois meses antes da sua morte. Na capa, uma fotografia de duas cartas dobradas e um selo de lacre vermelho, gerada com modelos de difusão, repousa sobre um marroquim verde desenhado em código dentro de um filete duplo dourado. Por dentro, as cartas vêm seguidas, sem quebras de página. O cabeçalho de cada uma traz o número em versaletes vermelhos e os dois correspondentes em itálico Fell, com o local e a data alinhados à direita logo abaixo. A saudação tem uma linha própria, e a assinatura, em versaletes, fecha a carta na margem direita; o pós-escrito de Frederico vem depois da assinatura, um corpo menor. O cabeço de cada página ímpar dá a data da carta.
Esta receita responde a
- Como componho uma epígrafe, uma dedicatória, uma assinatura ou um olho com aspas grandes?
- Como consigo boa justificação e hifenização em textos em espanhol, francês ou alemão?
- Como configuro os cabeços: título do livro na página esquerda, título do capítulo na direita e fólio do lado de fora?
A resposta curta
// Each letter is a level-1 heading that carries its place and date as attributes:
// # Frédéric à Voltaire {place="À Charlottembourg" date="6 juin 1740"}
// (a value holds no { or }, and one with " goes in single quotes: gotcha attr-values)
const letterHead = { enabled: true, slot: { elements: [
// {number} prints numberingTemplate '{1:I}' (gotcha: heading-number-placeholders).
{ kind: 'text', id: 'number', content: 'Lettre {number}', ...sc, fontSize: pt(9),
letterSpacing: pt(1.8), color: col('seal'), placement: at('container', 'top-left') },
{ kind: 'text', id: 'title', content: '{titleText}', ...fell, fontSize: pt(15),
color: col('ink'),
placement: at('#number', 'below', 0, 1) },
// The dateline spans the measure under the title and sets its words flush right.
{ kind: 'text', id: 'dateline', content: '{attr.place}, le {attr.date}.', ...crimson,
italic: true, fontSize: pt(10.4), color: col('ink'), align: 'right',
placement: { ...at('#title', 'below', 0, 1.5), size: { width: 'fill' } } },
] } };
// The salutation, the signature and the postscript, each a :::paragraphs{style="…"} container:
// :::paragraphs{style="signature"}
// Fédéric.
// :::
const letterParts = [
{ id: 'vedette', firstLineIndent: pt(0) }, // 'Sire,' on a line of its own, flush left
{ id: 'signature', ...sc, fontSize: pt(10.5), textAlign: 'right', marginTop: pt(LEAD / 2) },
{ id: 'postscript', fontSize: pt(9), lineHeight: pt(12.6), marginTop: pt(LEAD / 2) },
];
// Hooked up below: letterHead designs the level-1 heading, letterParts joins paragraphStyles.
Ingredientes
- Funcionalidades
- Atributos de títuloAncoragem de elementos de designEstilos de parágrafoAberturas desenhadasTítulos numeradosCapítulos que abrem em página ímparEstilos de títuloCapas, folhas de rosto e colofõesImagens nos designs de páginaQuebras de linha nos títulosCabeços e fóliosCabeços por tipo de páginaMargens espelhadasCor do papelQuebras de página e de colunaHifenização e idioma do documentoSobrescritos e subscritosMetadados do documentoPaleta de cores semânticaPáginas em um canvas
- Tipografia
- Crimson Pro, IM Fell French Canon, IM Fell DW Pica SC (SIL OFL 1.1)
- Materiais
letters-1160.jpg- The cover: two folded letters and a wax seal, a photograph (Generated With Diffusion Models, original)
- The cover’s green morocco and gilt fillet, drawn in code (Ignacio Ferro, CC BY 4.0)
Preparo
#1 · Leia o local e a data do título
O código é a resposta curta logo acima. Cada carta é um título de nível 1 cujos atributos guardam o local e a data, e o cabeçalho os imprime como {attr.place}, le {attr.date}. (atributos do título). A caixa da data vai até a borda direita da coluna (width: 'fill'), e align: 'right' encosta o texto nessa borda; sem isso, o texto de design sai centralizado. A saudação, a assinatura e o pós-escrito são estilos de parágrafo aplicados com :::paragraphs{style="…"}. Na versão 1.4.1, um estilo de parágrafo não tem ajuste de itálico nem de caixa, então a assinatura usa a IM Fell DW Pica SC, uma família cujas minúsculas são desenhadas como versaletes.
#2 · Componha as cartas em sequência
const letters = { level: 1, numberingTemplate: '{1:I}', advancedDesign: letterHead,
// Written out: 1.4.1 drops the H1 page break for any headings object (gotcha:
// headings-drop-h1-break), and a fixed engine would put each letter on a recto.
breakBefore: { enabled: false }, marginTop: pt(2 * LEAD),
// The hidden heading line is measured in the heading face: italic keeps it the IM Fell cut
// that FONTS loads (gotcha: fonts-first). Upright, it would need the roman, which FONTS
// leaves out; the layout would change only for a title long enough to wrap.
italic: true };
Com breakBefore desligado, uma carta começa duas linhas da grade (marginTop) abaixo da linha da grade em que a anterior termina. Na página 4, o pós-escrito, com entrelinha de 12,6 pt, termina entre duas linhas da grade, e por isso ficam cerca de três linhas em branco acima da carta III. Na página 2, a carta I vai até o pé da página e a carta II abre a página 3. O ajuste está escrito porque a versão 1.4.1 já tira a quebra do nível 1 assim que a configuração tem um objeto headings, e um motor corrigido abriria cada carta numa página ímpar (quebra antes). numberingTemplate: '{1:I}' numera as cartas de I a III, e o cabeçalho imprime o numeral por {number}.
#3 · Ponha a data da carta na página ímpar
const HEAD_Y = 12; // mm from the top edge
const head = (id, content, parity, placement, look = {}) => ({ kind: 'text', id, content,
parity, pages: 'body', ...sc, fontSize: pt(8.5), letterSpacing: pt(0.9), color: col('muted'),
placement, ...look });
const folio = { ...crimson, fontSize: pt(9), letterSpacing: pt(0), color: col('ink') };
const header = { elements: [
head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, HEAD_Y), folio),
head('verso-names', '{author}', 'even', at('page', 'top-left', MARGIN.outer + 8, HEAD_Y)),
// {attr.date} reads the last letter that starts on or before the page.
head('recto-date', '{attr.date}', 'odd', at('page', 'top-right', -(MARGIN.outer + 8), HEAD_Y),
{ ...crimson, italic: true, fontSize: pt(9.5), letterSpacing: pt(0) }),
head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, HEAD_Y),
folio),
] };
Num cabeçalho de página, {attr.date} pega o atributo do último título de nível 1 que começa naquela página ou antes dela. A página 3 imprime 12 juin 1740, e a página 5, onde continua a carta de Voltaire iniciada na 4, imprime 1er avril 1778 (elementos de texto). Os cabeços ficam 12 mm abaixo da borda superior da página, o fólio na margem externa e o texto 8 mm mais para dentro. pages: 'body' os mantém fora da capa, que conta como abertura porque o seu título ocupa a largura da página. A data é composta no itálico da Crimson Pro, o mesmo do local e data que ela repete, e os nomes na página par, nos versaletes dos números das cartas.
#4 · Faça da capa um título e feche a página dela
// The Markdown: # Mon sort \\ est changé {style="cover"}, then :::pagebreak, or the headnote
// and the first letter start on the cover (gotcha: cover-pagebreak).
const onCover = (y) => at('page', 'top', 0, y); // centred, y mm below the top edge
const LETTERS = { x: 12, y: 72, w: 116, h: 120 }; // mm: the photograph, inside the fillet
// numbered: false keeps the cover out of the count, so the first letter is I.
const cover = { id: 'cover', numbered: false,
// span: 'page' although the book has one column. Kept in the column, the design is clipped
// to the column's top and bottom (paper above and below the leather, no names) and its title
// loses the \\ break; page 1 would also count as a 'body' page and print the running heads.
span: 'page', advancedDesign: { enabled: true, slot: { elements: [
{ kind: 'image', id: 'binding', resourceId: 'binding',
placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: 'fill' } } },
{ kind: 'image', id: 'letters', resourceId: 'letters', placement: { ...at('page', 'top-left',
LETTERS.x, LETTERS.y), size: { width: mm(LETTERS.w), height: mm(LETTERS.h) } } },
{ kind: 'text', id: 'names', content: '{author}', ...sc, fontSize: pt(9.5),
letterSpacing: pt(2), color: col('gilt'), placement: onCover(18) },
// \\ in the heading breaks the title here; lineHeight is a multiple (gotcha:
// design-lineheight-multiple), and 'wrap' keeps the ellipsis off (overflow-ellipsis-default).
{ kind: 'text', id: 'title', content: '{titleText}', ...fell, fontSize: pt(50),
lineHeight: 1, color: col('paper'), align: 'center', overflow: 'wrap',
placement: onCover(24) },
{ kind: 'text', id: 'subtitle', content: '{subtitle}', ...crimson, italic: true,
fontSize: pt(12), color: col('paper'), placement: onCover(62) },
] } } };
A capa é o primeiro título, no estilo de título cover: um elemento de imagem ancorado à sangria leva a encadernação desenhada, um segundo põe a fotografia 72 mm abaixo do alto, e o título, os nomes e o subtítulo vêm do título e do frontmatter. numbered: false a deixa fora da contagem, e assim a primeira carta de Frederico é a I. O design reserva espaço só até o seu texto mais baixo, o subtítulo, e nenhum para as imagens. Sem o :::pagebreak depois do título, a nota introdutória e a carta I começariam na página 1, por cima das cartas fotografadas e do selo.
#5 · Hifenize o francês e mantenha os espaços abertos
// Justification, hyphenation, whole-paragraph line breaking and the widow, orphan and runt
// rules are defaults; locale 'fr' (in the config) picks the French patterns.
// The letters keep the transcription's unspaced ; : ? and !, because a narrow no-break
// space is a place to break the line in 1.4.1 (gotcha: nbsp-breaks).
const bodyText = { fontFamily: 'Crimson Pro', fontSize: pt(10), lineHeight: pt(LEAD),
color: col('ink'), firstLineIndent: mm(5),
// No :ref here, but 1.4.1 leaves this one blue whatever main-color says (gotcha:
// palette-skips-designs), and the default-skin check reads it.
referenceColor: col('ink'),
// A word space never shrinks below 75 % of the font's. At the default 60 %, the tightest
// line on page 5 sets its spaces at 0.70 (each VDT line carries its justifiedSpaceRatio).
minWordSpacing: 0.75,
// A runt fix may add tracking 1.4.1 measures but never paints (gotcha:
// runt-tracking-unpainted); no paragraph here needs one, edited text might.
maxRuntTracking: 0 };
locale: 'fr' hifeniza com os padrões franceses (ou-vrage na página 2, représen-tation na página 4). A justificação, a quebra de linhas pelo parágrafo inteiro e as regras de viúvas, órfãs e linhas curtas vêm ligadas por padrão. minWordSpacing: 0.75 impede que um espaço entre palavras encolha abaixo de três quartos do espaço da Crimson Pro; com o padrão 0,6, a linha justificada mais apertada da página 5 comporia os espaços a 0,70 dessa largura. Com entrelinha de 14,4 pt, a página comporta 33 linhas, e a margem inferior é o que sobra abaixo delas, 20,4 mm.
#6 · Ligue cada cor à paleta
const palette = {
ink: '#2a2320', // the text: a warm near-black
paper: '#f6efe2', // the page, and the lettering on the cover
seal: '#9c2b24', // the letter numbers
leather: '#2a4536', // the cover: a green morocco binding
gilt: '#d0b67c', // its tooled border and the names on it
muted: '#75695d', // the running heads
};
// A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs).
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 default colours, the italic of the headnote among them, link to 'main-color';
// here it is the ink, not the default blue.
{ id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } },
];
col() escreve o hexadecimal de cada cor ao lado do seu id na paleta. Na versão 1.4.1, a paleta chega aos estilos de texto, mas não aos elementos de design, que pintam esse hexadecimal: os cabeçalhos das cartas, os cabeços e a capa. O itálico da nota introdutória e do colofão usa a cor de itálico padrão, que está ligada a main-color; com essa entrada na cor da tinta, ele sai na tinta e não no azul do motor, #295AA3.
A receita completa
// ═══ Postext Cookbook · Nº 064 · Letters edition: datelines and signatures ════════════ // https://postext.dev/en/cookbook/letters-edition // Code: MIT · Text: Frederick II and Voltaire, 1740 and 1778 (PD) · Cover photo: diffusion models // Fonts: Crimson Pro, IM Fell French Canon, IM Fell DW Pica SC (SIL OFL) · Needs postext ≥ 1.4.1 import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerResourceImage } from 'https://esm.sh/postext'; const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'letters-edition'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: iron-gall ink on cream paper, wax red, green morocco and gilt const palette = { ink: '#2a2320', // the text: a warm near-black paper: '#f6efe2', // the page, and the lettering on the cover seal: '#9c2b24', // the letter numbers leather: '#2a4536', // the cover: a green morocco binding gilt: '#d0b67c', // its tooled border and the names on it muted: '#75695d', // the running heads }; // A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs). 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 default colours, the italic of the headnote among them, link to 'main-color'; // here it is the ink, not the default blue. { id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } }, ]; // #endregion const TRIM = { width: 140, height: 210 }; // the French 14 × 21 format const LEAD = 14.4; // pt: the body's leading, the grid every letter starts on const LINES = 33; // lines of text on a full page const TOP = 22; // mm: the top margin const MARGIN = { top: TOP, inner: 17.5, outer: 14.5, // mm, mirrored bottom: TRIM.height - TOP - (LINES * LEAD * 25.4) / 72 }; // ends the page on line 33 const sc = { fontFamily: 'IM Fell DW Pica SC' }; // its lower case is cut as small capitals const fell = { fontFamily: 'IM Fell French Canon', italic: true }; // the display italic const crimson = { fontFamily: 'Crimson Pro' }; // the text face const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } }); // #region answer: a letter head read from the heading, and styles for the letter's parts // Each letter is a level-1 heading that carries its place and date as attributes: // # Frédéric à Voltaire {place="À Charlottembourg" date="6 juin 1740"} // (a value holds no { or }, and one with " goes in single quotes: gotcha attr-values) const letterHead = { enabled: true, slot: { elements: [ // {number} prints numberingTemplate '{1:I}' (gotcha: heading-number-placeholders). { kind: 'text', id: 'number', content: 'Lettre {number}', ...sc, fontSize: pt(9), letterSpacing: pt(1.8), color: col('seal'), placement: at('container', 'top-left') }, { kind: 'text', id: 'title', content: '{titleText}', ...fell, fontSize: pt(15), color: col('ink'), placement: at('#number', 'below', 0, 1) }, // The dateline spans the measure under the title and sets its words flush right. { kind: 'text', id: 'dateline', content: '{attr.place}, le {attr.date}.', ...crimson, italic: true, fontSize: pt(10.4), color: col('ink'), align: 'right', placement: { ...at('#title', 'below', 0, 1.5), size: { width: 'fill' } } }, ] } }; // The salutation, the signature and the postscript, each a :::paragraphs{style="…"} container: // :::paragraphs{style="signature"} // Fédéric. // ::: const letterParts = [ { id: 'vedette', firstLineIndent: pt(0) }, // 'Sire,' on a line of its own, flush left { id: 'signature', ...sc, fontSize: pt(10.5), textAlign: 'right', marginTop: pt(LEAD / 2) }, { id: 'postscript', fontSize: pt(9), lineHeight: pt(12.6), marginTop: pt(LEAD / 2) }, ]; // Hooked up below: letterHead designs the level-1 heading, letterParts joins paragraphStyles. // #endregion // #region letters: numbered I, II, III and run on, two grid lines apart const letters = { level: 1, numberingTemplate: '{1:I}', advancedDesign: letterHead, // Written out: 1.4.1 drops the H1 page break for any headings object (gotcha: // headings-drop-h1-break), and a fixed engine would put each letter on a recto. breakBefore: { enabled: false }, marginTop: pt(2 * LEAD), // The hidden heading line is measured in the heading face: italic keeps it the IM Fell cut // that FONTS loads (gotcha: fonts-first). Upright, it would need the roman, which FONTS // leaves out; the layout would change only for a title long enough to wrap. italic: true }; // #endregion // #region running-heads: the correspondents on the verso, the date of the letter on the recto const HEAD_Y = 12; // mm from the top edge const head = (id, content, parity, placement, look = {}) => ({ kind: 'text', id, content, parity, pages: 'body', ...sc, fontSize: pt(8.5), letterSpacing: pt(0.9), color: col('muted'), placement, ...look }); const folio = { ...crimson, fontSize: pt(9), letterSpacing: pt(0), color: col('ink') }; const header = { elements: [ head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, HEAD_Y), folio), head('verso-names', '{author}', 'even', at('page', 'top-left', MARGIN.outer + 8, HEAD_Y)), // {attr.date} reads the last letter that starts on or before the page. head('recto-date', '{attr.date}', 'odd', at('page', 'top-right', -(MARGIN.outer + 8), HEAD_Y), { ...crimson, italic: true, fontSize: pt(9.5), letterSpacing: pt(0) }), head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, HEAD_Y), folio), ] }; // #endregion // #region cover: page 1 is a heading style with the art and the title; :::pagebreak ends it // The Markdown: # Mon sort \\ est changé {style="cover"}, then :::pagebreak, or the headnote // and the first letter start on the cover (gotcha: cover-pagebreak). const onCover = (y) => at('page', 'top', 0, y); // centred, y mm below the top edge const LETTERS = { x: 12, y: 72, w: 116, h: 120 }; // mm: the photograph, inside the fillet // numbered: false keeps the cover out of the count, so the first letter is I. const cover = { id: 'cover', numbered: false, // span: 'page' although the book has one column. Kept in the column, the design is clipped // to the column's top and bottom (paper above and below the leather, no names) and its title // loses the \\ break; page 1 would also count as a 'body' page and print the running heads. span: 'page', advancedDesign: { enabled: true, slot: { elements: [ { kind: 'image', id: 'binding', resourceId: 'binding', placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: 'fill' } } }, { kind: 'image', id: 'letters', resourceId: 'letters', placement: { ...at('page', 'top-left', LETTERS.x, LETTERS.y), size: { width: mm(LETTERS.w), height: mm(LETTERS.h) } } }, { kind: 'text', id: 'names', content: '{author}', ...sc, fontSize: pt(9.5), letterSpacing: pt(2), color: col('gilt'), placement: onCover(18) }, // \\ in the heading breaks the title here; lineHeight is a multiple (gotcha: // design-lineheight-multiple), and 'wrap' keeps the ellipsis off (overflow-ellipsis-default). { kind: 'text', id: 'title', content: '{titleText}', ...fell, fontSize: pt(50), lineHeight: 1, color: col('paper'), align: 'center', overflow: 'wrap', placement: onCover(24) }, { kind: 'text', id: 'subtitle', content: '{subtitle}', ...crimson, italic: true, fontSize: pt(12), color: col('paper'), placement: onCover(62) }, ] } } }; // #endregion // #region text: Crimson Pro at 10/14.4 pt, set in French // Justification, hyphenation, whole-paragraph line breaking and the widow, orphan and runt // rules are defaults; locale 'fr' (in the config) picks the French patterns. // The letters keep the transcription's unspaced ; : ? and !, because a narrow no-break // space is a place to break the line in 1.4.1 (gotcha: nbsp-breaks). const bodyText = { fontFamily: 'Crimson Pro', fontSize: pt(10), lineHeight: pt(LEAD), color: col('ink'), firstLineIndent: mm(5), // No :ref here, but 1.4.1 leaves this one blue whatever main-color says (gotcha: // palette-skips-designs), and the default-skin check reads it. referenceColor: col('ink'), // A word space never shrinks below 75 % of the font's. At the default 60 %, the tightest // line on page 5 sets its spaces at 0.70 (each VDT line carries its justifiedSpaceRatio). minWordSpacing: 0.75, // A runt fix may add tracking 1.4.1 measures but never paints (gotcha: // runt-tracking-unpainted); no paragraph here needs one, edited text might. maxRuntTracking: 0 }; // #endregion const config = () => ({ // a factory: the engine caches resolved configs per object locale: 'fr', // the exact code of the bundled patterns (gotcha: hyphenation-locales) colorPalette, page: { sizePreset: 'custom', width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150, backgroundColor: col('paper'), margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner), right: mm(MARGIN.outer), mirror: true } }, // left = inner layout: { layoutType: 'single' }, bodyText, // A heading's own line is hidden under its design but still measured, in this face and weight. headings: { fontFamily: 'IM Fell French Canon', fontWeight: 400, levels: [letters] }, headingStyles: [cover], paragraphStyles: [...letterParts, // Frederick's verses, one paragraph per line (a one-line paragraph is never stretched), with // a line of space above and below; the two closing alexandrines start 7 mm further left. { id: 'verse', firstLineIndent: mm(14), marginTop: pt(LEAD) }, { id: 'verse-long', firstLineIndent: mm(7), marginBottom: pt(LEAD) }, // The editor's headnote at 9.6 on 13 pt, italic through *…* in the Markdown, since a // paragraph style has no italic setting. { id: 'headnote', fontSize: pt(9.6), lineHeight: pt(13), firstLineIndent: pt(0) }, { id: 'colophon', fontSize: pt(7.8), lineHeight: pt(10.8), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(3 * LEAD) }], header, footer: { elements: [] }, // the folios ride in the header }); // #region art: the binding drawn in code, and the photograph of the letters laid on it const W = TRIM.width; const H = TRIM.height; // The binding: green leather to the edges, a gilt double fillet and a lozenge at each corner. function bindingSvg() { const tooling = [6, 7.6].map((inset, i) => `<rect x="${inset}" y="${inset}" ` + `width="${W - 2 * inset}" height="${H - 2 * inset}" fill="none" stroke="${palette.gilt}" ` + `stroke-width="${i ? 0.25 : 0.7}"/>`).join('') + [[6, 6], [W - 6, 6], [6, H - 6], [W - 6, H - 6]].map(([x, y]) => `<path d="M${x} ${y - 2.4} L${x + 2.4} ${y} L${x} ${y + 2.4} ` + `L${x - 2.4} ${y} Z" fill="${palette.gilt}"/>`).join(''); return `<svg xmlns="http://www.w3.org/2000/svg" width="${W * 10}" height="${H * 10}" ` + `viewBox="0 0 ${W} ${H}"><rect width="${W}" height="${H}" fill="${palette.leather}"/>` + `${tooling}</svg>`; } // The design's image elements name these by id; loadSvg() and loadImage() below register the // files. The photograph is a JPEG in assets/ cut to LETTERS' 116 × 120 mm, declared at its // pixels; its edges fade into the leather's green, so it sits on the drawn binding unseen. const resources = [{ id: 'binding', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0, svg: { fileId: 'binding.svg', width: W * 10, height: H * 10 }, altText: 'A green leather cover with a gilt double fillet and a lozenge at each corner.' }, { id: 'letters', typeId: 'figure', kind: 'bitmap', createdAt: 0, updatedAt: 0, bitmap: { fileId: 'letters-1160.jpg', format: 'jpeg', width: 1160, height: 1200 }, altText: 'Two folded letters on the green leather: the upper one lies face down, its flap ' + 'closed by a red wax seal; the lower one shows an address in brown ink.' }]; // #endregion // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Amostra em Markdown · 120 linhas · content.en.md
title: "Mon sort est changé" subtitle: "Trois lettres, 1740 et 1778" author: "Frédéric II et Voltaire" --- # Mon sort \\ est changé {style="cover"} :::pagebreak :::paragraphs{style="headnote"} *Frédéric-Guillaume I^er^, roi de Prusse, meurt à Potsdam le 31 mai 1740. Six jours plus tard, son fils, qui règne désormais sous le nom de Frédéric II, écrit de Charlottembourg à Voltaire, son correspondant depuis août 1736. Les deux premières lettres de ce choix sont de ce mois de juin. La troisième, écrite de Paris le 1^er^ avril 1778, ferme le volume de 1889 d’où viennent ces textes. Voltaire meurt à Paris le 30 mai suivant. On garde l’orthographe de l’édition; la vedette et la signature sont détachées du texte.* ::: # Frédéric à Voltaire {place="À Charlottembourg" date="6 juin 1740"} :::paragraphs{style="vedette"} Mon cher ami, ::: Mon sort est changé, et j’ai assisté aux derniers moments d’un roi, à son agonie, à sa mort. En parvenant à la royauté, je n’avais pas besoin assurément de cette leçon pour être dégoûté de la vanité des grandeurs humaines. J’avais projeté un petit ouvrage de métaphysique; il s’est changé en un ouvrage de politique. Je croyais joûter avec l’aimable Voltaire, et il me faut escrimer avec Machiavel. Enfin, mon cher Voltaire, nous ne sommes point maîtres de notre sort. Le tourbillon des événements nous entraîne, et il faut se laisser entraîner. Ne voyez en moi, je vous prie, qu’un citoyen zélé, un philosophe un peu sceptique, mais un ami véritablement fidèle. Pour dieu, ne m’écrivez qu’en homme, et méprisez avec moi les titres, les noms, et tout l’éclat extérieur. Jusqu’à présent il me reste à peine le temps de me reconnaître; j’ai des occupations infinies: je m’en donne encore de surplus; mais malgré tout ce travail, il me reste toujours du temps assez pour admirer vos ouvrages et pour puiser chez vous des instructions et des délassements. Assurez la marquise de mon estime. Je l’admire autant que ses vastes connaissances et la rare capacité de son esprit le méritent. Adieu, mon cher Voltaire; si je vis, je vous verrai, et même dès cette année. Aimez-moi toujours, et soyez toujours sincère ami avec votre ami :::paragraphs{style="signature"} Fédéric. ::: # Frédéric à Voltaire {place="À Charlottembourg" date="12 juin 1740"} :::paragraphs{style="verse"} Non, ce n’est plus du mont Rémus, Douce et studieuse retraite D’où mes vers vous sont parvenus, Que je date ces vers confus: Car dans ce moment le poète Et le prince sont confondus. Désormais mon peuple que j’aime Est l’unique Dieu que je sers: Adieu les vers et les concerts. Tous les plaisirs. Voltaire même; Mon devoir est mon Dieu suprême. Qu’il entraîne de soins divers! Quel fardeau que le diadème! Quand ce dieu sera satisfait, Alors dans vos bras, cher Voltaire, Je volerai, plus prompt qu’un trait, ::: :::paragraphs{style="verse-long"} Puiser, dans les leçons de mon ami sincère, Quel doit être d’un roi le sacré caractère. ::: Vous voyez, mon cher ami, que le changement du sort ne m’a pas tout à fait guéri de la métromanie, et que peut-être je n’en guérirai jamais. J’estime trop l’art d’Horace et de Voltaire pour y renoncer; et je suis du sentiment que chaque chose de la vie a son temps. J’avais commencé une épître sur les abus de la mode et de la coutume, lors même que la coutume de la primogéniture m’obligeait de monter sur le trône et de quitter mon épître pour quelque temps. J’aurais volontiers changé mon épître en satire contre cette même mode, si je ne savais que la satire doit être bannie de la bouche des princes. Enfin, mon cher Voltaire, je flotte entre vingt occupations, et je ne déplore que la brièveté des jours, qui me paraissent trop courts de vingt-quatre heures. Je vous avoue que la vie d’un homme qui n’existe que pour réfléchir et pour lui-même, me semble infiniment préférable à la vie d’un homme dont l’unique occupation doit être de faire le bonheur des autres. Vos vers sont charmants. Je n’en dirai rien, car ils sont trop flatteurs. Mon cher Voltaire, ne vous refusez pas plus longtemps à l’empressement que j’ai de vous voir. Faites en ma faveur tout ce que vous croyez que votre humanité comporte. J’irai à la fin d’auguste à Vesel, et peut-être plus loin. Promettez-moi de me joindre, car je ne saurais vivre heureux ni mourir tranquille sans vous avoir embrassé. Adieu. :::paragraphs{style="signature"} Fédéric. ::: :::paragraphs{style="postscript"} Mille compliments à la marquise. Je travaille des deux mains; d’un côté à l’armée, de l’autre au peuple et aux beaux-arts. ::: # Voltaire à Frédéric {place="À Paris" date="1er avril 1778"} :::paragraphs{style="vedette"} Sire, ::: Le gentilhomme français qui rendra cette lettre à Votre Majesté, et qui passe pour être digne de paraître devant Elle, pourra vous dire que si je n’ai pas eu l’honneur de vous écrire depuis longtemps, c’est que j’ai été occupé à éviter deux choses qui me poursuivaient dans Paris: les sifflets et la mort. Il est plaisant qu’à quatre-vingt-quatre ans j’aie échappé à deux maladies mortelles. Voilà ce que c’est que de vous être consacré: je me suis renommé de vous, et j’ai été sauvé. J’ai vu avec surprise et avec une satisfaction bien douce, à la représentation d’une tragédie nouvelle, que le public, qui regardait il y a trente ans Constantin et Théodose comme les modèles des princes, et même des saints, a applaudi avec des transports inouïs à des vers qui disent que Constantin et Théodose n’ont été que des tyrans superstitieux. J’ai vu vingt preuves pareilles du progrès que la philosophie a fait enfin dans toutes les conditions. Je ne désespérerais pas de faire prononcer dans un mois le panégyrique de l’empereur Julien: et assurément si les Parisiens se souviennent qu’il a rendu chez eux la justice comme Caton, et qu’il a combattu pour eux comme César, ils lui doivent une éternelle reconnaissance. Il est donc vrai, Sire, qu’à la fin les hommes s’éclairent, et que ceux qui se croient payés pour les aveugler ne sont pas toujours les maîtres de leur crever les yeux! Grâces en soient rendus à Votre Majesté! Vous avez vaincu les préjugés comme vos autres ennemis: vous jouissez de vos établissements en tout genre. Vous êtes le vainqueur de la superstition, ainsi que le soutien de la liberté germanique. Vivez plus longtemps que moi, pour affermir tous les empires que vous avez fondés. Puisse Frédéric le Grand être Frédéric l’immortel! Daignez agréer le profond respect et l’inviolable attachement de :::paragraphs{style="signature"} Voltaire. ::: :::paragraphs{style="colophon"} *Correspondance de Voltaire avec le roi de Prusse* (Paris, Librairie de la Bibliothèque nationale, 1889), texte du Project Gutenberg, nº 25734. Composé en Crimson Pro et IM Fell (licence SIL OFL). :::`; // content.<lang>.md, inlined by the Cookbook // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { // text, display and label faces (gotcha: fonts-first) 'Crimson Pro': ['400', '400i'], 'IM Fell French Canon': ['400i'], 'IM Fell DW Pica SC': ['400'] }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadSvg('binding.svg', bindingSvg()); await loadImage('letters-1160.jpg', asset('letters-1160.jpg')); await loadFonts(FONTS, markdown); const doc = await buildWithFonts(() => buildDocument({ markdown, resources }, config()), markdown); showPages(doc, { title: t({ en: 'Letters edition', es: 'Edición de cartas' }) });Kit · core, fonts, viewer, images: igual em todas as receitas · 271 linhas
// ─── 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 v2 ── the same in every recipe · postext.dev/cookbook // Postext measures with the loaded faces and caches the widths: load every face // before the first build, from Fontsource, the files the PDF embeds too. /** 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. * Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', greek: 'U+0370-03FF', }; const jobs = []; let added = 0; 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 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 = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().then((ready) => { document.fonts.add(ready); added++; }, () => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return added; } /** Runs `build` and loads any face the pages use that FONTS missed (a regular * one with a warning), then clears the measurement cache and builds again. */ async function buildWithFonts(build, text = '') { const tried = new Set(); for (let round = 0; round < 3; round++) { kitStatus('Laying out…'); await new Promise(requestAnimationFrame); // let the status paint first const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; }); const wanted = { base: {}, variants: {} }; for (const { font, base } of [result].flat().flatMap(fontStringsOf)) { const { family, weight, style } = parseFont(font); const key = `${family}|${weight}|${style}`; if (tried.has(key) || hasFace(family, weight, style)) continue; tried.add(key); (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`); } if (Object.keys(wanted.base).length) { console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`); } const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true }); if (added === 0) return result; clearMeasurementCache(); } throw new Error('The fonts did not settle after three builds.'); } /** Every font string of the layout; `base` marks a block's own face. */ function fontStringsOf(doc) { const found = new Map(); const walk = (node) => { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) { node.forEach(walk); return; } for (const [key, value] of Object.entries(node)) { if (typeof value === 'string' && /fontString$/i.test(key)) { found.set(value, found.get(value) || key === 'fontString'); } else if (value && typeof value === 'object') walk(value); } }; walk(doc.pages); walk(doc.blocks); return [...found].map(([font, base]) => ({ font, base })); } /** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }. * A string with no weight ('95.8px Young Serif', from a design text) is 400. */ function parseFont(font) { const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim()); if (!m) throw new Error(`Unexpected font string: ${font}`); const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]); return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' }; } /** 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, '-'); } /** 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 v1 ── 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. */ function showPages(docs, { title, width = 460 } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── Kit · images v1 ── recipes with pictures · postext.dev/cookbook /** Registers a photo or PNG for the canvas and keeps its bytes for the PDF. * fetch → ImageBitmap never taints the canvas (a plain cross-origin <img> would). */ async function loadImage(fileId, url) { const res = await fetch(url); if (!res.ok) throw new Error(`Image not found (${res.status}): ${url}`); const bytes = new Uint8Array(await res.arrayBuffer()); registerResourceImage(fileId, await createImageBitmap(new Blob([bytes]))); (loadImage.bytes ??= new Map()).set(fileId, bytes); } /** Registers SVG markup (drawn in code, or fetched) as a vector image. */ async function loadSvg(fileId, svg) { const img = new Image(); img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; await img.decode(); registerResourceImage(fileId, img); (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg)); } /** renderToPdf({ resourceBytes: imageBytes }) */ function imageBytes(fileId) { return loadImage.bytes?.get(fileId); } /** renderToHtml({ resourceImageUrl: imageUrl }) */ function imageUrl(fileId) { const bytes = imageBytes(fileId); if (!bytes) return undefined; imageUrl.urls ??= new Map(); if (!imageUrl.urls.has(fileId)) { const type = /\.svg$/i.test(fileId) ? 'image/svg+xml' : /\.png$/i.test(fileId) ? 'image/png' : 'image/jpeg'; imageUrl.urls.set(fileId, URL.createObjectURL(new Blob([bytes], { type }))); } return imageUrl.urls.get(fileId); } // ─── /Kit ───────────────────────────────────────────────────────────────────────
O script.js montado funciona como está: cole-o como script de módulo em qualquer página ou abra a receita no CodePen. Pasta da receita no GitHub ↗ (abre em uma nova aba)
Variações
#Ponha o local no cabeço
Os mesmos atributos podem dar à página ímpar o local e a data completos, À Charlottembourg, le 12 juin 1740.
- head('recto-date', '{attr.date}', 'odd', at('page', 'top-right', -(MARGIN.outer + 8), HEAD_Y),
+ head('recto-date', '{attr.place}, le {attr.date}', 'odd',
+ at('page', 'top-right', -(MARGIN.outer + 8), HEAD_Y),Erros comuns
Erro comum
Valores de atributo: sem { ou }; aspas simples se tiver "
Um valor de atributo termina na chave de fechamento, por isso não pode conter { nem }. Um valor que contém aspas duplas vai entre aspas simples; o cifrão não causa problema. Atributos de título →
Erro comum
Qualquer objeto headings desativa a quebra de página do H1
Por padrão, um H1 salta para uma página ímpar (always-odd), mas passar qualquer objeto headings redefine esse padrão, então os capítulos ficam emendados e span: 'page' não faz nada. Declare de novo headings.levels[0].breakBefore: { enabled: true, parity } em toda configuração. Capítulos que abrem em página ímpar →
Erro comum
{number}/{chapterNumber} imprimem o número do H1; {numberRoman} só nas partes
{number} e {chapterNumber} imprimem o número já formatado do título, mas {numberRoman}, {numberDecimal} e as outras variantes numéricas só são preenchidas nas páginas de parte. Formate o número do capítulo no seu numberingTemplate ({1:I}) ou passe-o como atributo. Títulos numerados →
Erro comum
Coloque :::pagebreak depois de uma capa de página inteira
Em um layout de várias colunas, uma abertura de página inteira, como uma capa, deixa o bloco seguinte começar na coluna 2 da mesma página, por cima da arte. Um :::pagebreak logo depois do título da capa encerra a página sem deixar uma em branco. Capas, folhas de rosto e colofões →
Erro comum
Só 8 idiomas têm hifenização, com o código exato
A hifenização existe para en-us, es, fr, de, it, pt, ca e nl, com o código exato: 'es-ES' ou qualquer outro idioma passa sem aviso para o inglês americano. Hifenização e idioma do documento →
Erro comum
Um espaço não separável ainda quebra a linha
No postext 1.4.1, o algoritmo de quebra de linha trata U+00A0 como um espaço comum, então 0,08 %, 2,006 s ou seção 2 podem ficar em duas linhas. Junte os dois elementos (0,08%) ou reescreva a frase. Escapes e caracteres literais →
Erro comum
Uma paleta trocada não chega aos elementos de design nem à cor das referências
postext 1.4.1 aplica colorPalette aos estilos de texto (corpo, títulos, listas, legendas, tabelas, boxes), mas não aos elementos de cabeçalhos, rodapés, aberturas e páginas de parte, nem a bodyText.referenceColor: eles mantêm o hex escrito ao lado do seu paletteId. Se você trocar a paleta, para uma edição de tela escura ou para mudar as cores, reescreva cada cor vinculada a partir de colorPalette antes de compor. Paleta de cores semântica →
Erro comum
O lineHeight de um texto de design é um múltiplo, nunca uma medida
Num slot de design, o lineHeight de um elemento de texto multiplica o tamanho da fonte (lineHeight: 1.05). No postext 1.4.1, uma medida como pt(15) não é rejeitada: a altura da abertura dá NaN, o espaço que ela reserva, minHeight incluído, se perde sem aviso e o texto passa por baixo do título. Textos, fios e caixas nos designs de página →
Erro comum
O excesso de texto de design é 'ellipsis-end' por padrão
Um elemento de texto de design que não cabe na sua largura termina em reticências por padrão. Use overflow: 'wrap' nos títulos que devem passar para mais linhas. Textos, fios e caixas nos designs de página →
Erro comum
O ajuste de linhas curtas pode apertar um tracking que nunca é pintado
No postext 1.4.1, quando um parágrafo termina numa linha curta, a diagramação o compõe com uma linha a menos: primeiro aperta o espaçamento entre palavras, depois aplica até maxRuntTracking milésimos de em de tracking negativo. Os renderizadores de canvas e PDF só pintam tracking acima de zero, então o parágrafo sai impresso sem ele: as linhas justificadas perdem essa diferença nos espaços entre palavras, que ficam esmagados, e a última linha pode passar da medida e ser cortada na borda da coluna. Defina bodyText.maxRuntTracking: 0, que mantém o ajuste pelo espaçamento entre palavras, e reescreva os parágrafos que voltarem a terminar numa linha curta. Viúvas, órfãs e linhas curtas →
Erro comum
Carregue todas as fontes antes do layout
O motor de layout mede o texto com as fontes que o navegador carregou e guarda as larguras em cache, então uma fonte que chega depois da primeira composição deixa quebras de linha erradas e um PDF que não corresponde mais à tela. Carregue antes todos os pesos e estilos e chame clearMeasurementCache() antes de recompor quando alguma chegar atrasada. Fontes antes da diagramação →
Erro comum
Texto de design não aceita ^sup^ nem **negrito** em linha
Os elementos de texto de design imprimem texto simples, então ^1^ ou **negrito** em um atributo aparecem literalmente. Use sobrescritos Unicode (¹ ² ³ estão no subconjunto latin) ou um segundo elemento com outro peso. Textos, fios e caixas nos designs de página →
- A página ímpar pega a data da última carta que começa nela ou antes. Uma página que termina uma carta e abre a seguinte traz a data da carta nova, do mesmo modo que o cabeço de um dicionário dá a última entrada da página.
- Um título sem o atributo imprime um texto vazio, sem aviso: uma carta sem
place="…"ganha uma linha de local e data que começa com vírgula. Confira o título de cada carta. - As cartas mantêm os ; : ? e ! da transcrição sem espaço antes. A tradição francesa põe ali um espaço fino inseparável, mas a versão 1.4.1 quebra a linha nesse espaço como em qualquer outro, e um sinal poderia começar uma linha.
- A Crimson Pro não tem as letras modificadoras ᵉ (U+1D49) e ʳ (U+02B3), então 1ᵉʳ escrito em Unicode sairia numa fonte substituta. A nota introdutória, que é texto corrido, compõe
1^er^como sobrescrito; a linha de local e data e o cabeço da página ímpar são texto de design e imprimem 1er com a terminação no mesmo corpo do número.
Créditos
- Receita
- Ignacio Ferro
- Texto
- Frederick II’s letters to Voltaire of 6 and 12 June 1740 and Voltaire’s letter to him of 1 April 1778, in Correspondance de Voltaire avec le roi de Prusse (Paris, Librairie de la Bibliothèque nationale, 1889), one misprint corrected (ouvragés), the salutations and signatures set apart · Frédéric II · Voltaire · domínio público
- The headnote and the colophon · Ignacio Ferro · CC BY 4.0
- Imagens
- The cover: two folded letters and a wax seal, a photograph · Generated With Diffusion Models · original
- The cover’s green morocco and gilt fillet, drawn in code · Ignacio Ferro · CC BY 4.0
- Fontes
- Crimson Pro (SIL OFL 1.1) · IM Fell French Canon (SIL OFL 1.1) · IM Fell DW Pica SC (SIL OFL 1.1)
- Código
- MIT, como o Postext
Editar este texto ↗ (abre em uma nova aba)Pasta da receita no GitHub ↗ (abre em uma nova aba)


