Em poucas palavras
Um ensaio sobre um poema de Keats, como uma revista literária o imprime. A autora escreve um código curto para cada fonte; o Postext imprime o autor e a página entre parênteses e a lista de obras citadas no fim, no estilo MLA.
O que você vai compor
Um ensaio de uma revista literária trimestral: quatro páginas sobre quem diz os dois últimos versos da “Ode on a Grecian Urn”, de Keats. A página é de revista, 150 × 230 mm, composta em Spectral, com títulos de seção e cabeços em Spectral SC, e a abertura fica numa faixa da cor do verniz negro sobre uma tira de argila vermelha, as duas cores de um vaso ático. As citações seguem o MLA 9, o estilo da maioria dos departamentos de literatura e de história da arte: autor e página entre parênteses, só a página quando a frase já nomeia o autor, um título abreviado quando duas obras são do mesmo autor e a lista de obras citadas numa página própria. Cada citação é uma chave no texto, e o estilo é uma única configuração.
Esta receita responde a
- Como cito obras e monto a bibliografia em APA, IEEE ou outro estilo de citação?
A resposta curta
// The text cites with [@keats-letters, 48] → (Keats, Letters 48): MLA prints the author
// and the page, and adds a short title when two works share an author. [-@eliot1932, 230]
// drops the name the sentence already gives → (230). The list is alphabetical, and a
// second work by the same author opens with a dash (MLA's ---) instead of the name.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
style: 'modern-language-association', // MLA 9th edition, author-page
link: true, // a citation jumps to its entry in Works Cited
bibliography: {
// MLA's half-inch hanging indent, scaled to a 108 mm measure.
fontSize: em(0.94), lineHeight: pt(13.4), hangingIndent: em(2.2), entrySpacing: pt(3),
},
};
Ingredientes
- Funcionalidades
- Citações em um estilo de citaçãoBibliografia a partir das referênciasAberturas desenhadasAtributos de títuloEstilos de títuloCabeços e fóliosMargens espelhadasEstilos de parágrafoPaleta de cores semânticaExportação para PDF
- Também usa
- Referências cruzadasCor do papelCabeços por tipo de páginaFontes incorporadas ao PDFEspaço vertical explícito
- Tipografia
- Spectral, Spectral SC (SIL OFL 1.1)
- Materiais
- Nenhum: todas as imagens são desenhadas em código
Preparo
#1 · O estilo é MLA; os dados são CSL
O código é a resposta curta acima. O motor de citações é registrado uma vez e citations.style indica o estilo MLA incluído; o citeproc-js escreve com ele cada citação e a lista. As referências ficam num bloco :::references{format=csl-yaml} no fim do ensaio. Aqui se usa CSL-YAML em vez de BibTeX porque o MLA precisa de um título abreviado para as duas obras de Keats, e o leitor de BibTeX do postext 1.12.1 descarta o campo shorttitle; em CSL o campo se chama title-short.
#2 · Três tipos de citação
O texto cita uma obra com [@jack1967], que imprime “(Jack)” porque o ensaio se refere ao livro como um todo. Com página, [@keats-letters, 41] imprime “(Keats, Letters 41)”: há duas obras de Keats citadas, então o citeproc acrescenta o título abreviado para distingui-las. Quando a frase já nomeia o crítico, [-@eliot1932, 230] omite o nome e imprime “(230)”. Depois que a ode foi citada com os números dos versos, os trechos seguintes levam só os números, digitados entre parênteses comuns, como o MLA permite.
#3 · Uma citação em bloco com a referência depois do ponto final
// More than four lines of prose go in a Markdown blockquote: in ink, indented, with no
// first-line indent. Its citation follows the final full stop, as MLA asks.
const blockquote = {
color: col('ink'), italic: false, indent: em(2), firstLineIndent: em(0),
};
A carta sobre a Negative Capability passa de quatro linhas de prosa, e por isso é destacada como uma citação em bloco do Markdown: em redondo, na cor do texto, recuada 2 em e sem recuo de primeira linha. O MLA põe a referência de uma citação em bloco depois do ponto final, e o texto-fonte faz o mesmo: …fact and reason. [@keats-letters, 48]. Um :::space{lines=0.5} acima e outro abaixo separam a citação dos parágrafos em volta.
#4 · As obras citadas numa página própria
headingStyles: [{ id: 'works-cited', breakBefore: { enabled: true, parity: 'any' } }],
O título usa um estilo que quebra para uma página nova, e :::bibliography{title=""} põe a lista sob ele sem um segundo título. A lista é alfabética com recuo deslocado, e a segunda obra de Keats começa com um travessão triplo no lugar do nome, como o MLA pede para autores repetidos. Um fólio no pé aparece nesta página e na abertura, onde não há cabeços.
#5 · A abertura nas duas cores de um vaso
const BAND = 116; // mm from the trim's top
const opener = {
enabled: true,
minHeight: mm(BAND - MARGIN.top + 4), // the text starts a line under the band
slot: { elements: [
{ kind: 'box', id: 'band', style: { backgroundColor: col('glaze') },
placement: { ...at('page', 'top-left'), size: { width: 'fill', height: mm(BAND) } } },
{ kind: 'box', id: 'foot', style: { backgroundColor: col('clay') },
placement: { ...at('page', 'top-left', 0, BAND - 3), size: { width: 'fill',
height: mm(3) } } },
{ kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: LABEL, fontWeight: 600,
fontSize: pt(9), letterSpacing: pt(1.6), color: col('slip'), align: 'left',
placement: at('container', 'top-left', 0, 6) },
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: TEXT, fontWeight: 300,
italic: true, fontSize: pt(56), lineHeight: 0.95, color: col('paper'), align: 'left',
overflow: 'wrap', placement: { ...at('container', 'top-left', -1, 30),
size: { width: mm(110), height: 'auto' } } },
{ kind: 'text', id: 'sub', content: '{attr.sub}', fontFamily: TEXT, fontWeight: 300,
fontSize: pt(13), lineHeight: 1.25, color: col('paper'), align: 'left', overflow: 'wrap',
placement: { ...at('container', 'top-left', 0, 66),
size: { width: mm(96), height: 'auto' } } },
{ kind: 'text', id: 'byline', content: '{attr.byline}', fontFamily: LABEL, fontWeight: 600,
fontSize: pt(9.5), letterSpacing: pt(1.2), color: col('slip'), align: 'left',
placement: at('container', 'top-left', 0, 82) },
] },
};
const palette = {
ink: '#1f1a17', glaze: '#231c19', clay: '#9a3d22', slip: '#e3a27e', rule: '#c9bdb0',
muted: '#6b6159', paper: '#fffdf9',
};
// 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, 'main-color': palette.clay })
.map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
A faixa tem 116 mm de altura e traz o chapéu, o título em 56 pt de Spectral Light itálico, o subtítulo e a assinatura, todos alimentados por atributos do título. A tira de argila é uma segunda caixa ancorada 3 mm acima do pé da faixa.
A receita completa
// ═══ Postext Cookbook · Nº 096 · A humanities essay with MLA works cited ══════════ // https://postext.dev/en/cookbook/mla-humanities-essay // Code: MIT · Text: original (CC BY 4.0) · Pictures: none // Fonts: Spectral, Spectral SC (SIL OFL 1.1) · Needs postext ≥ 1.12.1 import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerCitationEngine, } from 'https://esm.sh/postext'; import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf'; import { createCiteprocEngine, STYLES, LOCALES } from 'https://esm.sh/postext-citeproc'; const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'mla-humanities-essay'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: the black glaze and the red clay of an Attic vase const palette = { ink: '#1f1a17', glaze: '#231c19', clay: '#9a3d22', slip: '#e3a27e', rule: '#c9bdb0', muted: '#6b6159', paper: '#fffdf9', }; // 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, 'main-color': palette.clay }) .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })); // #endregion const TEXT = 'Spectral', LABEL = 'Spectral SC'; const TRIM = { width: 150, height: 230 }; // a literary quarterly const MARGIN = { top: 22, bottom: 22, inner: 19, outer: 21 }; const LEAD = 14.4; // pt const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } }); // #region answer: MLA 9 author-page citations and a Works Cited list // The text cites with [@keats-letters, 48] → (Keats, Letters 48): MLA prints the author // and the page, and adds a short title when two works share an author. [-@eliot1932, 230] // drops the name the sentence already gives → (230). The list is alphabetical, and a // second work by the same author opens with a dash (MLA's ---) instead of the name. registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES })); const citations = { style: 'modern-language-association', // MLA 9th edition, author-page link: true, // a citation jumps to its entry in Works Cited bibliography: { // MLA's half-inch hanging indent, scaled to a 108 mm measure. fontSize: em(0.94), lineHeight: pt(13.4), hangingIndent: em(2.2), entrySpacing: pt(3), }, }; // #endregion // #region quotation: a block quotation, set in and upright // More than four lines of prose go in a Markdown blockquote: in ink, indented, with no // first-line indent. Its citation follows the final full stop, as MLA asks. const blockquote = { color: col('ink'), italic: false, indent: em(2), firstLineIndent: em(0), }; // #endregion // #region opener: the title in a band of black glaze with a red clay foot const BAND = 116; // mm from the trim's top const opener = { enabled: true, minHeight: mm(BAND - MARGIN.top + 4), // the text starts a line under the band slot: { elements: [ { kind: 'box', id: 'band', style: { backgroundColor: col('glaze') }, placement: { ...at('page', 'top-left'), size: { width: 'fill', height: mm(BAND) } } }, { kind: 'box', id: 'foot', style: { backgroundColor: col('clay') }, placement: { ...at('page', 'top-left', 0, BAND - 3), size: { width: 'fill', height: mm(3) } } }, { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: LABEL, fontWeight: 600, fontSize: pt(9), letterSpacing: pt(1.6), color: col('slip'), align: 'left', placement: at('container', 'top-left', 0, 6) }, { kind: 'text', id: 'title', content: '{titleText}', fontFamily: TEXT, fontWeight: 300, italic: true, fontSize: pt(56), lineHeight: 0.95, color: col('paper'), align: 'left', overflow: 'wrap', placement: { ...at('container', 'top-left', -1, 30), size: { width: mm(110), height: 'auto' } } }, { kind: 'text', id: 'sub', content: '{attr.sub}', fontFamily: TEXT, fontWeight: 300, fontSize: pt(13), lineHeight: 1.25, color: col('paper'), align: 'left', overflow: 'wrap', placement: { ...at('container', 'top-left', 0, 66), size: { width: mm(96), height: 'auto' } } }, { kind: 'text', id: 'byline', content: '{attr.byline}', fontFamily: LABEL, fontWeight: 600, fontSize: pt(9.5), letterSpacing: pt(1.2), color: col('slip'), align: 'left', placement: at('container', 'top-left', 0, 82) }, ] }, }; // #endregion const head = (id, content, parity, edge, x, align) => ({ kind: 'text', id, content, parity, pages: 'body', fontFamily: LABEL, fontWeight: 500, fontSize: pt(8.5), letterSpacing: pt(1), color: col('muted'), align, placement: at('page', edge, x, 12), }); const config = () => ({ // a factory: the engine caches resolved configs per object locale: t({ en: 'en-us', es: 'es' }), // exact codes (gotcha: hyphenation-locales) colorPalette, citations, 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 }, }, layout: { layoutType: 'single' }, bodyText: { fontFamily: TEXT, fontSize: pt(10.6), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'), textAlign: 'justify', firstLineIndent: mm(5), indentAfterHeading: false, hyphenation: { enabled: true }, optimalLineBreaking: true, avoidWidows: true, avoidOrphans: true, avoidRunts: true, blockquote, }, headings: { fontFamily: LABEL, fontWeight: 600, color: col('clay'), levels: [ // Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break). { level: 1, fontFamily: TEXT, fontSize: pt(56), marginBottom: pt(0), breakBefore: { enabled: true, parity: 'any' }, advancedDesign: opener }, { level: 2, fontSize: pt(11.5), letterSpacing: pt(0.9), lineHeight: pt(LEAD), marginTop: pt(LEAD), marginBottom: pt(LEAD / 2) }, ], }, // #region works-cited: MLA starts the list on a page of its own headingStyles: [{ id: 'works-cited', breakBefore: { enabled: true, parity: 'any' } }], // #endregion paragraphStyles: [ { id: 'colophon', fontFamily: LABEL, fontSize: pt(7.5), lineHeight: pt(10), color: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD * 2) }, ], header: { elements: [ head('verso-folio', '{pageNumber}', 'even', 'top-left', MARGIN.outer, 'left'), head('verso-author', '{attr.byline}', 'even', 'top-left', MARGIN.outer + 8, 'left'), head('recto-title', '{title}', 'odd', 'top-right', -(MARGIN.outer + 8), 'right'), head('recto-folio', '{pageNumber}', 'odd', 'top-right', -MARGIN.outer, 'right'), ] }, footer: { elements: [ // a drop folio where the running heads stand down { ...head('drop-folio', '{pageNumber}', 'all', 'top', 0, 'center'), pages: 'opener', placement: at('container', 'top', 0, 9) }, ] }, }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Amostra em Markdown · 95 linhas · content.en.md
title: "Cold Pastoral" author: "Helena Ward" --- # Cold Pastoral {#essay kicker="Essays · Romantic Poetry" sub="On the voice in the last two lines of Keats’s “Ode on a Grecian Urn”" byline="Helena Ward"} No Greek urn answers to the one Keats describes. Readers who went looking for it in the British Museum found parts of several: the heifer led to sacrifice on the Parthenon frieze, which Keats saw among the Elgin Marbles in the spring of 1817, and the Bacchic dancers of the Townley Vase, with those of the Borghese Vase in the Louvre, known in London through engravings [@jack1967]. The nearest thing to a single source is a sheet of paper. Keats traced an engraving of the Sosibios Vase, a marble krater signed by an Athenian sculptor of the first century BCE, from Henry Moses’s book of antique vases, and the tracing survives [@mcdermott1948]. The urn of the poem is a composite, assembled from marbles and from prints of marbles, and at the end it speaks. Keats wrote the ode in May 1819. It was first printed in January 1820 in the *Annals of the Fine Arts*, a London magazine about painting and sculpture, and that summer he collected it in his third and last book of poems. The two printings differ in one mark of punctuation, and much of what has been written about the poem since turns on that mark. ## The questions The ode opens by naming its object three times, as a “still unravish’d bride of quietness,” a “foster-child of silence and slow time” and a “Sylvan historian” [@keats-ode, lines 1–3]. The names are a courtesy: the speaker wants the historian to tell its tale. “What men or gods are these? What maidens loth? / What mad pursuit? What struggle to escape?” (8–9). The urn does not answer, and for three stanzas the speaker answers for it, praising the “unheard” melodies of the painted pipes over heard ones (11–12) and a lover who will never reach the girl but will always love her. The fourth stanza turns from the dancers to a procession, a priest leading a heifer “lowing at the skies” (33), and then to a town that is not on the urn at all, emptied of its people by the sacrifice and left silent “for evermore” (38). Here the questions stop being answerable even in principle. The town exists only because the procession has to have come from somewhere. ## The punctuation of the last lines In the last stanza the speaker gives up questioning. The urn “dost tease us out of thought / As doth eternity: Cold Pastoral!” (44–45), and it will outlast this generation as “a friend to man, to whom thou say’st, / ‘Beauty is truth, truth beauty,’—that is all / Ye know on earth, and all ye need to know” (48–50). In the 1820 volume the inverted commas close after “beauty,” so the urn says five words and someone else says the rest. The *Annals* text has no inverted commas, and neither have the transcripts made by Keats’s friends [@stillinger1974]. An editor has to choose, and the choice decides who says “that is all”: the urn to mankind, the poet to his reader, or the poet to the figures on the urn. The lines have had hostile readers. T. S. Eliot found the couplet “a serious blemish on a beautiful poem” [-@eliot1932, 230], a statement that was either untrue or one he could not understand. Cleanth Brooks answered that the words are “a speech ‘in character’” [-@brooks1947, 165], fitted to the urn that utters them, and set them beside Edgar’s “Ripeness is all” in *King Lear*. His reading needs the 1820 punctuation: if the urn speaks only the five words inside the commas, the motto is the urn’s, and the comment after the dash belongs to the poet who has listened to it. ## The letters Keats had written about beauty and truth before he wrote about the urn. “What the Imagination seizes as Beauty must be truth—whether it existed before or not,” he told Benjamin Bailey in November 1817 [@keats-letters, 41]. A month later, walking home from the Christmas pantomime, he named the quality he thought a poet most needed: :::space{lines=0.5} > I had not a dispute, but a disquisition, with Dilke upon various subjects; several things dove-tailed in my mind, and at once it struck me what quality went to form a Man of Achievement, especially in Literature, and which Shakspeare possessed so enormously—I mean Negative Capability, that is, when a man is capable of being in uncertainties, mysteries, doubts, without any irritable reaching after fact and reason. [@keats-letters, 48] :::space{lines=0.5} Read with the letter beside it, the couplet is less a doctrine than a refusal. The urn has answered none of the speaker’s questions about who the figures are or where the town stands. What it offers instead is a sentence that cannot be checked, and the poet’s comment accepts that this is all the urn will give. The speaker who began by demanding a tale ends content with half knowledge, which is the state the letter asks a poet to bear. ## Works Cited {style="works-cited"} :::bibliography{title=""} :::paragraphs{style="colophon"} Set in Spectral and Spectral SC (SIL OFL). Text: original essay, CC BY 4.0; quotations from Keats’s 1820 volume and Colvin’s edition of the letters. ::: :::references{format=csl-yaml} - id: keats-ode type: chapter author: [{family: Keats, given: John}] title: Ode on a Grecian Urn title-short: Ode container-title: "Lamia, Isabella, The Eve of St. Agnes, and Other Poems" publisher: Taylor and Hessey issued: 1820 page: 113-116 - id: keats-letters type: book author: [{family: Keats, given: John}] editor: [{family: Colvin, given: Sidney}] title: Letters of John Keats to His Family and Friends title-short: Letters publisher: Macmillan issued: 1925 - id: jack1967 type: book author: [{family: Jack, given: Ian}] title: Keats and the Mirror of Art publisher: Clarendon Press issued: 1967 - id: mcdermott1948 type: article-journal author: [{family: McDermott, given: William C.}] title: Keats and Sosibios container-title: The Classical Journal volume: 44 issue: 1 page: 33-34 issued: 1948 - id: stillinger1974 type: book author: [{family: Stillinger, given: Jack}] title: "The Texts of Keats’s Poems" publisher: Harvard UP issued: 1974 - id: eliot1932 type: chapter author: [{family: Eliot, given: T. S.}] title: Dante container-title: "Selected Essays, 1917–1932" publisher: Faber and Faber issued: 1932 - id: brooks1947 type: book author: [{family: Brooks, given: Cleanth}] title: "The Well Wrought Urn: Studies in the Structure of Poetry" publisher: Reynal and Hitchcock issued: 1947 :::`; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── // Every face the design uses (gotcha: fonts-first). const FONTS = { Spectral: ['300', '300i', '400', '400i', '600', '600i'], 'Spectral SC': ['400', '500', '600'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); const doc = await buildWithFonts(() => buildDocument({ markdown }, config()), markdown); const title = t({ en: 'A humanities essay with MLA works cited', es: 'Un ensayo de humanidades con obras citadas en MLA' }); showPages(doc, { title }); offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider }), `${RECIPE}.pdf`);Kit · core, fonts, viewer, pdf: igual em todas as receitas · 281 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 · pdf v2 ── the same in every recipe that exports a PDF /** The Fontsource files the screen used, as TrueType: the nearest weight the * family ships, upright if it has no italic; latin, then what the face's * letters need (kitSubsetsFor). */ async function fontsourceProvider(family, weight, style, request) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); const weights = meta?.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style; const text = String.fromCodePoint(...(request?.codePoints ?? [])); const more = kitSubsetsFor(text, meta); const files = await Promise.all(['latin', ...more].map(async (subset) => { const res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-${subset}-${w}-${s}.woff2`); if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} ${subset}`); return decompressWoff2(new Uint8Array(await res.arrayBuffer())); })); return files.length === 1 ? files[0] : files; } /** A "Build the PDF" button; then "Open the PDF" (a new tab: CodePen's frame * shows no PDFs) and a download link. */ function offerPdf(makePdf, filename) { viewer(); const button = Object.assign(document.createElement('button'), { type: 'button', textContent: 'Build the PDF' }); button.dataset.postextPdf = filename; button.addEventListener('click', async () => { button.disabled = true; button.textContent = 'Building the PDF…'; try { const bytes = await makePdf(); const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' })); const size = `${Math.max(1, Math.round(bytes.length / 1024))} KB`; button.replaceWith( Object.assign(document.createElement('a'), { href: url, target: '_blank', rel: 'noopener', textContent: 'Open the PDF ↗' }), Object.assign(document.createElement('a'), { href: url, download: filename, textContent: `Download ${filename} · ${size}` })); } catch (error) { button.disabled = false; button.textContent = 'Build the PDF'; kitFail(error); } }); document.getElementById('pt-actions').append(button); } // ─── /Kit ───────────────────────────────────────────────────────────────────────
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
#Pôr a lista logo depois do ensaio
Uma revista que compõe as obras citadas depois do último parágrafo, sem página nova, tira a quebra do estilo de título.
- headingStyles: [{ id: 'works-cited', breakBefore: { enabled: true, parity: 'any' } }],
+ headingStyles: [{ id: 'works-cited', breakBefore: { enabled: false } }],#Citar no Chicago autor-data
O mesmo ensaio em Chicago põe o ano depois do autor, “(Brooks 1947, 165)”, e a lista acompanha.
- style: 'modern-language-association', // MLA 9th edition, author-page
+ style: 'chicago-author-date',Erros comuns
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
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
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
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
A configuração fica em cache pela identidade: crie um objeto novo
O motor guarda em cache as configurações resolvidas pela identidade do objeto, então alterar uma configuração no próprio objeto e compor de novo reaproveita o resultado antigo. Crie um objeto novo a cada composição; por isso a configuração de uma receita é uma função, config(). Páginas em um canvas →
- Uma citação narrativa sem página,
@stillinger1974 records, imprime o nome com um segundo espaço depois dele no postext 1.12.1. Cite essa obra entre colchetes no fim da oração, como faz este ensaio, ou dê uma página. - O citeproc-js imprime os títulos como os dados os trazem. O MLA usa maiúscula em cada palavra principal nos títulos em inglês, então escreva-os assim nas referências.
Créditos
- Receita
- Ignacio Ferro
- Texto
- Quotations from “Ode on a Grecian Urn” (1820) · John Keats · domínio público
- Quotations from two letters of 1817, in Letters of John Keats to His Family and Friends, ed. Sidney Colvin (1925) · John Keats · domínio público
- Fontes
- Spectral (SIL OFL 1.1) · Spectral 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)


