Pular para o conteúdo principal

Capítulo 3 · Parte II · O ofício

Configuração do Postext

Referência completa de todas as opções de configuração de diagramação do Postext

Atualizado 2026-10-0730 minenescaptzhjaar

Em poucas palavras

Esta página lista todos os ajustes que definem a aparência das suas páginas. Um só grupo de ajustes reúne todas as escolhas: tamanho da página, número de colunas, fontes, tamanho do texto, títulos, boxes, cores e muito mais. Você só escreve os ajustes que quer mudar, porque cada um já começa com um valor sensato. É uma página de consulta, então vá direto à parte de que precisa. As últimas seções mostram aos programadores como gerar páginas web, arquivos PDF e livros digitais EPUB a partir do código.

Toda decisão de diagramação no Postext é controlada por um único objeto de configuração.

PostextConfig controla as dimensões da página, a disposição das colunas, a tipografia do texto corrido, os estilos de título, o idioma do documento (locale) e mais. Todas as propriedades são opcionais: o Postext vem com valores padrão sensatos, inspirados na tipografia tradicional do livro. Você só precisa especificar o que quer mudar.

import { buildDocument } from 'postext';
 
const document = buildDocument(content, {
  page: { sizePreset: '21x28', dpi: 300 },
  layout: { layoutType: 'double', gutterWidth: { value: 0.5, unit: 'cm' } },
  bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 9, unit: 'pt' } }, // 9 pt substitui o padrão de 8 pt
  headings: { fontFamily: 'Open Sans' },
});

Para uma visão geral de como o motor processa essa configuração, veja a página Arquitetura.

#Índice

Esta referência é longa. Estes são os blocos principais:

#Página

A propriedade page controla as dimensões físicas e a aparência da página.

PropriedadeTipoPadrãoDescrição
sizePresetPageSizePreset'17x24'Tamanho de página predefinido. Use 'custom' para definir largura e altura explícitas.
widthDimension17 cmLargura da página. Vem de sizePreset quando omitida; um valor explícito sempre prevalece (use sizePreset: 'custom' para tamanhos totalmente personalizados).
heightDimension24 cmAltura da página. Vem de sizePreset quando omitida; um valor explícito sempre prevalece.
marginsPageMargins2 cm em todos os ladosEspaço entre a borda da página e a área de conteúdo. Cada lado (superior, inferior, esquerdo, direito) é definido de forma independente. Com mirror: true as margens passam a ser de páginas espelhadas: left é a margem interna (do lado da lombada) e right a externa; as páginas ímpares (a página 1 é ímpar) as mantêm como foram escritas e as pares as trocam, de modo que a área de conteúdo (e com ela as colunas, as faixas de flutuantes, os contêineres de cabeçalho e rodapé e as faixas de abertura) muda de lado na página dupla. Padrão false. Veja abaixo.
backgroundColorColorValuetransparentCor de fundo da página.
dpinumber300Os pixels por polegada com que a diagramação trabalha: como as unidades físicas (cm, mm, in, pt) viram os seus pixels. Não é a resolução das imagens, mas um bitmap composto no seu próprio tamanho ocupa um pixel da diagramação por pixel da imagem e, assim, é impresso com esse número de ppi: mantenha 300 para impressão. O preflight verifica a resolução efetiva de cada imagem.
cutLinesCutLinesConfigdesativadoMostra marcas de corte nos cantos da página para o refile na gráfica. Quando ativado, o canvas cresce para incluir a área de sangria e as marcas de corte. Veja abaixo.
baselineGridBaselineGridConfigdesativadoDesenha a grade de linhas de base sobre as páginas, para conferir o ritmo vertical. A diagramação se ajusta à grade quer ela seja desenhada, quer não. Veja abaixo.
binding'auto' | 'left' | 'right''auto'A borda em que o livro é encadernado. 'auto' equivale a 'right' quando layout.writingMode é 'vertical-rl', quando o documento corre da direita para a esquerda (direction) ou quando a sua seção comics é lida da direita para a esquerda (um mangá, uma edição em japonês ou em chinês tradicional), e a 'left' nos demais casos. Um livro encadernado à direita abre numa página da esquerda e espelha as margens no sentido contrário. Vale para o livro inteiro: o layout próprio de um estilo de título nunca o altera. Veja Encadernação.

#Margens espelhadas

Livros são lidos em páginas duplas, e a margem interna costuma ser diferente da externa. margins.mirror transforma as quatro margens em margens de páginas espelhadas:

{
  "page": {
    "margins": {
      "top": { "value": 2, "unit": "cm" },
      "bottom": { "value": 2.5, "unit": "cm" },
      "left": { "value": 2.2, "unit": "cm" },
      "right": { "value": 1.4, "unit": "cm" },
      "mirror": true
    }
  }
}

Com essa configuração, toda página ímpar tem uma margem de 2,2 cm à esquerda (a lombada) e de 1,4 cm à direita (o corte frontal); toda página par tem 1,4 cm à esquerda (o corte frontal) e 2,2 cm à direita (a lombada). Cada página diagramada traz sua própria contentArea no VDTPage, então tudo o que deriva dela (colunas, faixas de flutuantes na largura total, contêineres de cabeçalho e rodapé e faixas de abertura span: 'page') segue automaticamente a geometria espelhada. Os quadros de página e de sangria usados pelos elementos de design ancorados em 'page' / 'bleed' não são afetados: eles descrevem a folha física, não as margens.

#Encadernação

“Documentos em chinês compostos na vertical são encadernados do lado direito, e os compostos na horizontal, do lado esquerdo” (clreq §7.1.1.1). page.binding: 'right' diagrama um livro para a borda direita:

  • A página 1 continua ímpar e continua sendo o recto, então breakBefore.parity, :::pagebreak{parity}, os elementos de design com parity e toda contagem de páginas mantêm o sentido. O que muda é o lado em que fica o recto: a página da esquerda da página dupla. Um capítulo que abre num novo recto abre numa página da esquerda (clreq §7.1.3.3).
  • Com margins.mirror, left continua sendo a margem interna, mas são as páginas ímpares que trocam: a página 1 tem a margem interna à direita, a página 2 à esquerda. Uma coluna lateral oneAndHalf em 'outer' / 'inner', um flutuante girado que encosta na lombada, as margens das páginas de parte e os ícones de canto dos boxes em 'outer' / 'inner' seguem a mesma regra.
  • O documento informa isso (VDTDocument.binding: 'right'), então um host nunca precisa ler a configuração: o Sandbox mostra suas páginas duplas como [3 | 2], com a página 1 sozinha à esquerda da lombada, e o visualizador HTML dele avança as páginas da direita para a esquerda, abrindo na ponta direita, com a seta da esquerda indo para a página seguinte. renderToHtml no modo múltiplo dispõe a fileira da direita para a esquerda.
  • O PDF traz /ViewerPreferences << /Direction /R2L >> e /PageLayout /TwoPageRight (a página 1 sozinha, depois em pares), marcado ou não. O Acrobat e o Foxit seguem essas indicações; o leitor embutido do Chrome ignora as duas.

Fólios e cabeços não mudam de lugar sozinhos: um modelo que imprime o número da página no canto externo precisa ter seus elementos de páginas ímpares e pares ajustados para a borda direita (os elementos aceitam parity).

Um livro escrito da direita para a esquerda (árabe, persa, hebraico…) também é encadernado à direita: 'auto' dá a borda direita quando a direction do documento resulta em 'rtl'. Um livro assim também espelha todo o fluxo, de modo que sua primeira coluna é a da direita, e os recuos, os marcadores de lista, os flutuantes e as notas ficam à direita; os espaços de cabeçalho e rodapé continuam físicos. Veja Composição árabe.

Uma HQ lida da direita para a esquerda também é encadernada à direita: 'auto' dá a borda direita quando a configuração tem uma seção comics cujo sentido de leitura resulta em 'rtl'. É o caso de um mangá (comics.artDirection: 'rtl') e de uma edição em japonês ou em chinês tradicional de uma HQ ocidental. Quem decide é a seção, para o livro inteiro, e não os blocos :::page de um capítulo. Veja Quadrinhos.

#Predefinições de tamanho de página

PredefiniçãoLarguraAlturaUso comum
'11x17'11 cm17 cmLivros de bolso
'12x19'12 cm19 cmBrochura padrão
'17x24'17 cm24 cmLivros técnicos, didáticos
'21x28'21 cm28 cmRevistas, relatórios (perto do A4)
'broadsheet'375 mm597 mmJornais em formato standard (broadsheet)
'berliner'315 mm470 mmJornais em formato berliner
'tabloid'280 mm430 mmJornais em formato tabloide
'compact'297 mm420 mmJornais em formato compacto (um standard dobrado ao meio)
Predefinições de tamanho de páginaQuatro predefinições de tamanho de página embutidas, desenhadas em escala proporcional: bolso 11x17, brochura 12x19, técnico 17x24 e quase A4 21x28 cm.21×28 · ~A417×24 · Didático12×19 · Brochura11×17 · Bolso21 cm28 cm
Predefinições desenhadas em escala proporcional.

Os quatro formatos de jornal são novos no postext 1.18 e ficaram fora do desenho: uma página standard tem quase quatro vezes a área de uma de 21 × 28. Costumam ser compostos com o layout 'multiple'; seis colunas são comuns num standard, e cinco num tabloide.

#Grade de linhas de base

A grade de linhas de base é o ritmo do texto corrido: linhas separadas por uma entrelinha do corpo, contadas a partir do topo da área de conteúdo. A diagramação a usa quer ela seja desenhada, quer não: títulos, fins de lista, boxes, figuras e fórmulas destacadas devolvem o texto à grade (a menos que o próprio snapToGrid deles esteja desligado), e assim as linhas de colunas vizinhas continuam alinhadas. enabled só desenha as linhas, no canvas, no PDF e nas visualizações do Sandbox, para conferir esse ritmo; ligá-lo ou desligá-lo não move nada. As linhas cobrem apenas o texto real da página (da primeira linha de texto à última), então faixas de flutuantes, páginas em branco de paridade e o espaço vazio no fim não mostram grade.

PropriedadeTipoPadrãoDescrição
enabledbooleanfalseSe as linhas da grade são desenhadas (canvas e PDF). Afeta só o desenho: a diagramação é a mesma nos dois casos.
colorColorValue#ccccccCor das linhas da grade.
lineWidthDimension0.5 ptEspessura das linhas da grade.
page: {
  baselineGrid: { enabled: true, color: { hex: '#e0e0e0', model: 'hex' } }
}

#Marcas de corte

Quando ativadas, o canvas cresce para incluir uma área de sangria, e o motor desenha marcas de corte em cada canto para a produção gráfica. O PDF dá a cada página uma TrimBox e uma BleedBox; um arquivo PDF/X as tem mesmo sem marcas de corte. No Sandbox, elas são definidas em Exportação › Preparação para impressão.

PropriedadeTipoPadrãoDescrição
enabledbooleanfalseSe o canvas cresce com a sangria e desenha as marcas de corte.
bleedDimension3 mmÁrea extra em volta da página, usada como sangria de impressão.
markLengthDimension5 mmComprimento de cada marca de corte.
markOffsetDimension3 mmDistância entre a borda de refile e o início de cada marca de corte. Uma marca nunca começa dentro da sangria: quando bleed é maior, a marca começa na borda da sangria.
markWidthDimension0.25 ptEspessura das marcas de corte.
colorColorValue#000000Cor das marcas de corte no canvas (a visualização na tela). O PDF sempre as pinta em cor de registro (veja abaixo).

A folha cresce bleed + markOffset + markLength em cada lado, e a página refilada fica no meio dela. Cada canto do refile recebe duas marcas de comprimento markLength, cada uma alinhada com uma das bordas que se encontram ali. Uma marca começa markOffset fora do refile, ou na borda da sangria quando bleed é o maior dos dois, para que nenhuma marca fique sobre arte que entra na sangria. Com os padrões (3 mm de sangria, 3 mm de afastamento), as marcas vão de 3 a 8 mm fora do refile, e uma faixa em branco de 3 mm de largura contorna a folha por fora delas. cropMarkSegments(page, doc.config.page, doc.trimOffset) devolve as oito marcas de uma página em pixels da página, as mesmas que os renderizadores canvas e PDF desenham. O terceiro argumento é a posição do refile dentro da folha, o valor a partir do qual o TrimBox do PDF é escrito; se omitido, é calculado a partir de cutLines da mesma forma.

Nada além das marcas é impresso fora da sangria. Tudo o que a página pinta, incluindo os elementos de design ancorados em 'page' ou 'bleed', é recortado na caixa de sangria no canvas, no PDF e na saída HTML, como faz uma exportação de DTP: uma faixa ou uma imagem colocada de propósito além da sangria é cortada na borda da sangria, onde o refile a perderia de qualquer forma. Até o postext 1.4, um elemento assim passava por cima das marcas até a borda da folha.

No PDF (postext-pdf), a MediaBox de cada página é a folha inteira. A página também traz uma TrimBox, a página refilada, e uma BleedBox, o refile mais a sangria, que as ferramentas de imposição e de preflight leem. As marcas são pintadas em cor de registro, a separação /All, e por isso saem em todas as chapas. Isso vale qualquer que seja o espaço de cor em que o PDF é escrito (RGB, escala de cinza ou CMYK): um preto comum chegaria à gráfica como preto composto ou apenas na chapa do preto. color só se aplica ao canvas.

#Numeração

O bloco page.pageNumbering controla como os rótulos de página são formatados e onde o contador começa. Ele define apenas o padrão do documento inteiro; para reiniciar a numeração no meio do documento (por exemplo, elementos pré-textuais em algarismos romanos passando a capítulos em decimais a partir de 1), use a diretiva :::numbering (veja Formato do documento → Diretivas).

PropriedadeTipoPadrãoDescrição
format'decimal' | 'lower-roman' | 'upper-roman' | 'lower-alpha' | 'upper-alpha', ou um estilo do Leste Asiático'decimal'Estilo numérico usado nos rótulos de página. Os estilos do Leste Asiático ('trad-chinese-informal' numera as páginas 一, 二, 三) estão listados em Grafias dos formatos de numeração.
startAtnumber1Valor numérico atribuído à primeira página, qualquer que seja o formato. format: 'lower-roman', startAt: 1 gera i, ii, iii, …; format: 'decimal', startAt: 17 gera 17, 18, 19, ….

O rótulo calculado fica guardado em cada VDTPage como pageLabel e é o valor em que o marcador {pageNumber} de cabeçalho e rodapé se resolve. Os PDFs emitem uma árvore numérica /PageLabels, para que o indicador de página e a navegação “Ir para a página” do Preview e do Acrobat correspondam exatamente aos rótulos impressos. Um estilo para o qual o PDF não tem código (numerais chineses, algarismos circulados e de largura total) é escrito página por página, e assim o leitor também mostra 一, 二, 三.

Grafias dos formatos de numeração

Três ajustes escolhem um formato de numeração, e cada um ganhou a sua própria grafia: os rótulos de página (page.pageNumbering.format e :::numbering{format=…}) dizem lower-roman, as listas numeradas (orderedLists.numberFormat) dizem arabic para decimal, e os tipos de recurso (counterFormat) dizem roman-lower. Todos aceitam todas as grafias abaixo, então um formato copiado de um ajuste funciona nos outros. Os nomes não diferenciam maiúsculas de minúsculas; as formas de um caractere, sim (i e I são diferentes).

FormatoImprimeGrafias aceitas
Decimal1, 2, 3decimal, arabic, 1
Romano minúsculoi, ii, iiilower-roman, roman-lower, i
Romano maiúsculoI, II, IIIupper-roman, roman-upper, I
Letras minúsculasa, b, clower-alpha, alpha-lower, lower-latin, a
Letras maiúsculasA, B, Cupper-alpha, alpha-upper, upper-latin, A
Numerais chineses, simplificado一, 十二, 一百零一simp-chinese-informal, 一 num documento em chinês simplificado
Numerais chineses, tradicional一, 十二, 一萬trad-chinese-informal, cjk-ideographic, 一 num documento em chinês tradicional
Numerais financeiros chineses, simplificado壹, 壹拾贰, 壹佰贰拾simp-chinese-formal, 壹 num documento em chinês simplificado
Numerais financeiros chineses, tradicional壹, 壹拾貳, 壹佰貳拾trad-chinese-formal, 壹 num documento em chinês tradicional
Algarismos chineses一二〇, 二〇二六cjk-decimal, 〇
Troncos celestes甲, 乙, 丙 … 癸cjk-heavenly-stem, 甲
Ramos terrestres子, 丑, 寅 … 亥cjk-earthly-branch, 子
Numerais japoneses一, 十二, 百一, 一万一japanese-informal, 一 num documento em japonês
Numerais japoneses formais壱, 壱拾弐, 壱百japanese-formal, 壱
Hiragana, ordem gojūonあ, い, う … ん, ああhiragana, あ
Katakana, ordem gojūonア, イ, ウ … ン, アアkatakana, ア
Hiragana, ordem irohaい, ろ, は … す, いいhiragana-iroha, い
Katakana, ordem irohaイ, ロ, ハ … ス, イイkatakana-iroha, イ
Circulados①, ②, ③ … ㊿circled-decimal, ①
Algarismos de largura total1, 2, 3fullwidth-decimal, 1
Algarismos arábico-índicos١, ٢, ٣ … ١٠arabic-indic, ١
Algarismos persas۱, ۲, ۳ … ۱۰persian, urdu, ۱
Letras árabes, ordem abjadأ, ب, ج, د, هـ … غ, أأabjad, أبجد
Letras árabes, ordem alfabéticaأ, ب, ت, ث … ي, أأhijai, arabic-alpha, arabic-alphabetic, أبتث
Numerais abjadا, ب … يا (11), غتمو (1.446)arabic-abjad
Numerais abjad, valores magrebinosص (60), ض (90), ش (1.000)arabic-abjad-maghrebi, maghrebi-abjad

Os estilos do Leste Asiático mantêm seus nomes de CSS Counter Styles nos três ajustes. Os numerais chineses informais escrevem 十 de 10 a 19 sem um 一 à frente (十二, mas 一百一十), um único 零 para uma sequência de zeros dentro do número (一百零一, 一千零五十), e 万 ou 萬 para dez mil, 亿 ou 億 para cem milhões (一万零一十). Só cjk-decimal usa 〇, algarismo por algarismo, como se escrevem os anos (二〇二六, GB/T 15835—2011). Os troncos param em 10, os ramos em 12 e os algarismos circulados em 50; além disso, o número sai em algarismos. 一 e 壹 seguem a escrita do locale do documento: tradicional para zh-Hant, zh-TW ou zh-HK, simplificada nos outros casos. Algumas edições antigas escrevem 101 como 一百一, sem 零; o Postext não imprime essa forma.

Os estilos japoneses (desde o postext 1.16) também seguem as CSS Counter Styles. japanese-informal escreve 十 sem um 一 à frente e sem 零 para uma casa vazia (百一, 千十), e japanese-formal os 大字 壱 弐 参 拾 百 阡, com 壱 mantido antes de cada unidade (壱拾, 壱百). O CSS para os dois em 9.999; o Postext continua em grupos de quatro algarismos com 万 億 兆 (no formal, 萬 億 兆), e cada grupo mantém seu 一 antes de uma unidade: 一万一 é 10.001, 一億一万 é 100.010.000. Num documento em japonês (ja, ja-JP…), 一 significa japanese-informal, então 第{1:一}章 imprime 第百一章 ali e 第一百零一章 num documento em chinês; 壹 continua sendo os numerais formais chineses. As séries de kana são as listas do CSS: 48 kana na ordem gojūon (incluindo ゐ e ゑ) e 47 na ordem do poema iroha; depois do último, continuam com dois kana (ああ, いい), e não têm zero, então um item numerado 0 imprime 0. Os algarismos kanji posicionais (二〇二六), a forma dos anos e dos fólios, são cjk-decimal.

Os estilos árabes mantêm o nome do CSS quando o CSS tem um (arabic-indic, persian; urdu e maghrebi-abjad, das Ready-made Counter Styles do W3C, são lidos como persian e arabic-abjad-maghrebi). arabic mantém o sentido antigo, os algarismos europeus. arabic-abjad escreve os numerais abjad aditivos do árabe clássico e da foliação de manuscritos, do valor mais alto para o mais baixo: 11 é يا, 1.446 é غتمو, e a contagem dos milhares vem antes de غ (2.000 بغ, 1.002 غب); acima de 999.999, o número sai em algarismos. A nota do W3C usa esse nome para uma série de 28 letras em que 11 é ك; o Postext chama essa série de abjad, a numeração por letras dos itens de lista na ordem abjad (أ، ب، ج، د، هـ), e de hijai a ordem alfabética (أ، ب، ت، ث). As duas escrevem a primeira letra com sua hamza (أ), e um heh que ficaria isolado como هـ, com um tatweel, para que nenhuma seja lida como os algarismos ١ e ٥; depois de 28, elas dobram, como faz lower-alpha (أأ, أب). Os valores magrebinos seguem o mnemônico صعفض قرست ثخذ ظغش (ص 60, ض 90); a lista do W3C troca ص e ض. Um único أ não distingue as duas séries de letras, então seus tokens são as quatro primeiras letras de cada uma: {1:أبجد}, {1:أبتث}. Os algarismos do próprio documento são definidos com numerals.

As outras grafias servem para configurações que ninguém verifica por tipo: predefinições em JSON e JavaScript puro. Os tipos do TypeScript continuam nomeando só a grafia própria de cada ajuste (a que o Sandbox escreve e resolveAllConfig devolve, incluindo os nomes do Leste Asiático), então um PostextConfig tipado fica nela, e outra grafia exige um cast.

// JavaScript ou uma predefinição JSON (em TypeScript, a grafia própria de cada ajuste)
orderedLists: { numberFormat: 'decimal' },          // igual a 'arabic'
page: { pageNumbering: { format: 'roman-lower' } }, // igual a 'lower-roman'
resourceTypes: [{ id: 'plate', counterFormat: 'upper-roman', … }], // igual a 'roman-upper'

resolveAllConfig converte os formatos de lista e de página para a grafia do próprio ajuste ('decimal' vira 'arabic' em orderedLists, 'roman-lower' vira 'lower-roman' em page.pageNumbering), e stripConfigDefaults descarta uma grafia do valor padrão. Os painéis do Sandbox mostram cada um dos três ajustes na sua própria grafia, qualquer que seja a usada na configuração. Qualquer outro valor (roman, 01, um erro de digitação) numera em decimal em vez de imprimir undefined, e é informado como um aviso de configuração. Os modelos de numeração de títulos aceitam os mesmos nomes depois dos dois-pontos ({1:roman-upper} é {1:I}), ao lado do seu próprio {1:01} com zeros à esquerda.

#Diagramação

A propriedade layout controla como as colunas se dispõem dentro da área de conteúdo.

Colunas, medianiz e margemUma página dividida em três colunas: cada coluna é a área de conteúdo do texto, as medianizes são os vãos verticais entre as colunas, e a margem é a borda em branco entre o limite da página e a primeira coluna.PáginaMargemColunaMedianiz
As colunas recebem o texto. As medianizes as separam. As margens emolduram o conteúdo.
Sistema de margensUma página com margens superior, direita, inferior e esquerda independentes em volta da área de conteúdo.PáginaÁrea de conteúdosuperior1cminferior2cmesquerda2.5cmdireita1.5cm
Cada lado da página pode ter sua própria margem.
PropriedadeTipoPadrãoDescrição
layoutType'single' | 'double' | 'oneAndHalf' | 'multiple''double'Disposição das colunas. Veja abaixo os detalhes de cada tipo.
columnCountnumber3Em quantas colunas iguais um layout 'multiple' divide o corpo: um número inteiro de 3 a 8. Um valor fora desse intervalo, ou que não seja inteiro, é limitado e informado (veja Tipos de layout). O layout 'multiple' próprio de um estilo de título usa o número do documento, a menos que defina o seu. Desde o postext 1.18.
gutterWidthDimension0.75 cmEspaço horizontal entre as colunas. Só se aplica a layouts de várias colunas.
sideColumnPercentnumber33Largura da coluna lateral como porcentagem da área de conteúdo. Qualquer valor que deixe alguma largura para as duas colunas é usado como foi escrito; um que não deixe é limitado, e o build informa isso (veja Tipos de layout). Só se aplica ao layout 'oneAndHalf'.
sideColumnRole'text' | 'floats''text'O que a coluna lateral recebe: texto corrido (que flui para ela depois da coluna principal), ou apenas os recursos e boxes colocados com span: 'side', uma coluna de margem só para flutuantes. Apenas 'oneAndHalf'. Com 'text', um parágrafo que passa de uma coluna para a outra é quebrado de novo para a largura da coluna em que continua; até o postext 1.4, ele mantinha as linhas da coluna em que tinha começado, e uma linha composta para a coluna principal ultrapassava a lateral, que a cortava.
sideColumnSide'right' | 'left' | 'outer' | 'inner''right'Borda da área de conteúdo em que fica a coluna lateral. 'outer' / 'inner' seguem a paridade da página quando as margens são espelhadas (a borda externa de um recto é a direita; a de um verso, a esquerda). Apenas 'oneAndHalf'.
columnRuleColumnRuleConfigdesativadoFio opcional desenhado entre as colunas. Veja abaixo.
fitFiguresToPagebooleanfalseReduz uma figura (bitmap ou SVG) cuja imagem, legenda e nota ficariam mais altas que a área de conteúdo até que caibam nela (uma imagem com zona segura, Resource.safeArea, primeiro é recortada dentro dela na largura total, e só é reduzida se ainda não couber), e compõe menor uma figura inline um pouco alta demais para o espaço que resta na coluna (até a metade da largura, com a legenda mantendo a medida da coluna), para que ela fique junto do seu texto. Uma imagem reduzida se posiciona no seu espaço conforme placement.align. O visualizador HTML liga essa opção, já que suas páginas têm só a altura da tela; as páginas impressas são dimensionadas para as suas figuras.
hugClosingFloatsbooleantrueNa página que fecha um capítulo (e o documento), as figuras e tabelas na largura da página compostas abaixo da última faixa de texto sobem até ficar a um vão de flutuante dela, empilhadas na sua ordem: nada vem depois delas ali. false as deixa onde o posicionamento as colocou, de modo que um flutuante position: 'bottom' termina no pé da página também na página de fechamento, como em todas as outras (numa ficha técnica cujo contorno termina na mesma altura em todas as páginas, por exemplo). Páginas com coluna lateral nunca os movem.
inlineResourceGap'around' | 'above''around'Onde um recurso inline (placement.position: 'here', incorporado com ::resource) mantém o vão de flutuante, uma linha. 'around' o mantém acima e abaixo do recurso, e o texto seguinte volta à grade de linhas de base abaixo desse vão; um título, uma lista, um boxe ou outro recurso inline logo depois compartilha o vão de baixo com o seu próprio espaço acima, valendo o maior dos dois. 'above' o mantém só acima: o texto depois do recurso recomeça na linha seguinte da grade, por mais perto que ela esteja (de nada a uma linha), como até o postext 1.4. As configurações guardadas por versões anteriores cujos capítulos incorporam um recurso são lidas com 'above', para que suas páginas não se movam (veja Pacotes escritos pelo postext 1.4 ou anterior). Uma configuração escrita em código para a 1.4 mantém o espaçamento antigo definindo ela mesma 'above', ou com pinLegacyInlineGap de postext/bundle.
inlineResourceGapInBoxesbooleantrueSe um recurso inline dentro de um boxe (:::callout) mantém o vão definido por inlineResourceGap, uma linha do próprio texto do boxe: acima do recurso, e também abaixo com 'around', valendo o maior entre ele e o espaço próprio do bloco seguinte. No topo ou no pé do boxe, ou de um fragmento de um boxe dividido, o espaçamento interno já separa o recurso e nenhum vão é acrescentado. false coloca o recurso logo abaixo do texto anterior e o texto seguinte logo abaixo do recurso, como até o postext 1.4. As configurações guardadas por versões anteriores cujos capítulos incorporam um recurso dentro de um boxe são lidas com false (veja Pacotes escritos pelo postext 1.4 ou anterior); em código, pinLegacyBoxResourceGap de postext/bundle faz o mesmo.
boxChildSplitMinLinesnumber2Mínimo de linhas de um parágrafo ou item de lista que um corte dentro dele deixa de cada lado quando um boxe se divide (splitMinLines, em Estilos de boxe, continua contando todas as linhas do boxe de cada lado do corte). Um número inteiro, no mínimo 1. Com o padrão, um corte nunca deixa uma linha solta de um parágrafo ou item no pé de uma coluna ou no alto da seguinte; um estilo de boxe cujo splitMinLines seja menor define o limite no lugar dele. 1 permite que um corte deixe uma linha do parágrafo ou item de um lado, como até o postext 1.4. As configurações guardadas por versões anteriores cujos capítulos têm um :::callout são lidas com 1 (veja Pacotes escritos pelo postext 1.4 ou anterior); em código, pinLegacyBoxChildCut de postext/bundle faz o mesmo.
writingMode'horizontal-tb' | 'vertical-rl''horizontal-tb'Como correm as linhas. 'vertical-rl' compõe texto chinês e japonês na vertical: caracteres de cima para baixo, cada linha à esquerda da anterior. O layout de um estilo de título o herda, a menos que defina o seu, então um apêndice horizontal pode vir depois de um livro vertical. Veja Escrita vertical.

#Escrita vertical

Com writingMode: 'vertical-rl', uma página é diagramada como uma página horizontal girada um quarto de volta no sentido horário. O fluxo é composto num quadro tão largo quanto a folha é alta; as linhas dele são as colunas do texto vertical, lidas a partir da direita, e tudo o que o motor faz com linhas (quebra, justificação, flutuantes, notas de rodapé, regras de manter junto) funciona nesse quadro. Assim, na folha:

  • Uma coluna do layout é um andar (栏): layoutType: 'double' dá dois andares empilhados de cima para baixo, preenchidos a partir do canto superior direito; gutterWidth é o vão entre eles, e o fio entre colunas, um fio horizontal entre eles. Os andares não são equilibrados no fim de um capítulo (clreq §7.1.3.4): o equilíbrio de colunas fica desligado num documento vertical, a menos que headings.balancing.enabled esteja definido.
  • O que o fluxo chama de “topo” é a borda direita da folha, onde a leitura começa: um flutuante no topo fica à direita da página, um no pé à esquerda, uma abertura na largura da página é uma faixa descendo pela borda direita, e as notas de rodapé caem na ponta esquerda de cada andar. Um elemento de design ancorado no topo da página (o design de um título, um boxe fixo) é ancorado na borda direita. sideColumnSide 'left' é o andar de cima, 'right' o de baixo; 'outer' e 'inner' são lidos como 'right' e 'left'.
  • As margens do fluxo são as margens da folha giradas: a margem direita é o topo do fluxo, e a margem superior, a sua esquerda. page.margins mantêm seus nomes na folha.
  • Cabeços, fólios, marcas de corte e o fundo da página ficam na folha, compostos na horizontal, como o clreq descreve para livros verticais.
  • Figuras e tabelas ficam em pé. Uma figura ocupa a altura do seu andar até onde a legenda permite, no máximo com a largura da página; a largura que ela toma é o espaço que usa no fluxo. A legenda é composta na horizontal abaixo dela, assim como as células de uma tabela: as duas são medidas como texto horizontal. Uma tabela é composta em pé atravessando a página, com as linhas cortadas no andar quando é mais alta que ele. placement.align coloca uma figura no topo ('left'), no meio ou no pé do seu andar. Um placement.rotate não é aplicado onde a figura é citada pela primeira vez em texto vertical: o build informa um aviso de conteúdo rotateIgnoredVertical. Uma seção horizontal do livro (um estilo de título cujo layout define 'horizontal-tb') gira suas figuras como pedido.
  • A imagem de um design (a ilustração de uma abertura, a de uma página de parte) também fica em pé. A caixa dela no fluxo é dimensionada com a largura e a altura da imagem trocadas, então size.width é quanto a imagem desce pela coluna, e sua largura na folha decorre das proporções.
  • Caracteres: Han, kana e formas de largura total ficam em pé, um em cada; palavras latinas e números são girados de lado, com suas larguras horizontais; a pontuação usa a forma vertical da fonte. Os sinais de pausa e de ponto nunca são girados: uma fonte da China continental coloca 、。,. no canto superior direito da célula e !?:; na metade direita dela, e uma fonte de Taiwan ou de Hong Kong os centraliza (cjk.region). Os parênteses e colchetes usam suas formas verticais, e “ ” ‘ ’ em texto continental são lidos como 『』「」. Travessões, reticências e o til ondulado usam a forma vertical da fonte quando ela existe (a Noto CJK associa a de — a vert junto com fwid: um fio descendo pelo meio da célula); caso contrário, são girados com a tinta centrada no eixo da coluna. Um 破折号 (——) em texto chinês é um único fio descendo pela coluna: suas formas verticais deixariam um branco nas duas pontas de cada célula, então cada travessão é girado com a linha e esticado como no texto horizontal (veja Larguras da pontuação). Um número de no máximo dois algarismos fica em pé numa única célula, a menos que esteja numa frase latina, cujas palavras ele acompanha (veja Números no texto vertical). O ponto mediano (·) ocupa meia célula em texto continental e uma célula inteira em texto de Taiwan e de Hong Kong. Os sinais que o Unicode define em pé (× © ± § ℃ ① e similares) ficam numa célula própria, também dentro de um número: 3×4 é 3 e 4 de lado, com × em pé entre eles. Um apóstrofo ou um ponto mediano entre duas letras de uma palavra latina (don’t, l·l) fica na palavra, de lado. Um parágrafo latino num fluxo vertical segue as mesmas regras. Fórmulas inline, chips e amostras de cor são girados de lado com a linha.
  • Cada caractere é medido como é pintado: uma célula avança a sua célula linha abaixo, um trecho de lado a sua largura horizontal. O texto que fica na horizontal na folha (cabeços, fólios, legendas, células de tabela) é medido na horizontal.
  • As larguras da pontuação, a pontuação pendurada e o espaço entre Han e latino valem ao longo da linha vertical como valem na horizontal. O branco antes de um glifo fica acima dele, e o branco depois, abaixo: um 、 Kaiming ocupa meia célula, 」「 se comprimem em uma célula e meia, um parêntese de abertura aparado no início de uma linha começa meia célula mais acima, um 。 pendurado fica abaixo do pé da sua linha, e o espaço Han–latino é um quarto de em da coluna acima e abaixo de uma palavra de lado. :;?! mantêm uma célula inteira no texto vertical em todas as regiões.
  • A grade de caracteres conta caracteres ao longo da linha e linhas através da página: charsPerLine define o comprimento de um andar, linesPerPage quantas linhas cabem numa página, e layoutType: 'double' dá dois andares de caracteres inteiros com uma medianiz de ems inteiros entre eles.

Para hosts que leem a diagramação: uma página vertical traz VDTPage.flow. A contentArea, as colunas, os blocos, as linhas, os flutuantes, as áreas de notas de rodapé, a faixa de abertura e as sobreposições de design de bloco dela estão em coordenadas do fluxo; width, height, header e footer estão na folha. flowToPage, pageToFlow, flowRectToPage e pageRectToFlow convertem entre os dois, e verticalOrientation(char, region) diz como um caractere fica. flow.centralBaselines dá, por família tipográfica, o eixo em que a diagramação centralizou os caracteres em pé: o centro da tinta de 中, cujo traço longo ocupa a altura da caixa do em (0,38 em acima da linha de base na Noto Serif e na Noto Sans, SC e TC igualmente), e que toda fonte chinesa, japonesa e coreana tem. Cada linha é centralizada nesse eixo: a linha de base de uma linha vertical fica meia entrelinha mais a linha de base central da família abaixo do topo da caixa da linha (a de uma linha horizontal fica a 0,8 da entrelinha para baixo), de modo que uma coluna de caracteres fica no meio do seu passo, e um fio desenhado entre duas colunas a um passo inteiro cai a meio caminho entre elas. Um texto de design vertical é composto do mesmo modo, nas suas próprias linhas. O canvas pinta ele mesmo uma página vertical; para pintar a pontuação com as formas verticais da própria fonte, um host no navegador carrega, uma vez por família, uma fonte gêmea com elas ativadas: loadVerticalAlternates(family, faces), em que faces são as fontes da família (URLs ou bytes) e seus descritores. A gêmea só é mantida quando o navegador aplica o recurso ao texto do canvas (Chrome 140 e posteriores): ele desenha 「(《 com a gêmea e com uma cópia das mesmas fontes carregada sem o recurso, no peso e no estilo das fontes dadas, e mantém a gêmea quando a tinta das duas difere. Uma chamada posterior para outras fontes de uma família cuja gêmea está em uso (o negrito depois do regular) as acrescenta a ela. O Sandbox faz isso para toda família de um documento vertical. Sem uma gêmea, os parênteses são girados em torno da caixa do em, e os sinais de pausa e de ponto continentais são movidos dentro da célula para onde as formas verticais da fonte os colocam: 、。,. para o canto superior direito (a até 0,07 em das formas verticais da Noto Serif SC), !?:; meio em para a direita e um pouco para cima (a até 0,02 em). O PDF e o HTML compõem a mesma página: veja Texto vertical no PDF e a tabela em Como a saída HTML difere do canvas e do PDF. Conferido célula por célula com o HarfBuzz (layout vertical com vert) na Noto Serif TC e SC, todo caractere de 「賈雨村」云云,宜乎?故曰!;:、。“引”‘單’…… fica a até 0,02 em dele no canvas e no PDF, e a até 0,05 em no HTML (o Chrome centraliza um glifo girado no meio entre o ascendente e o descendente da fonte, que na Noto fica 0,05 em acima do centro da caixa do em). flow.dashAdvances dá, por família, o avanço horizontal em ems de cada travessão que a página estica para preencher a célula (— – ― ⸺ ⸻ -), para um renderizador sem métricas de fonte próprias: o HTML estica o travessão com ele, como o canvas e o PDF fazem a partir das suas. Cabeços e fólios também podem ser compostos na vertical: veja Elementos de texto vertical.

#Fio entre colunas

Desenha uma linha vertical fina na medianiz para separar visualmente as colunas.

PropriedadeTipoPadrãoDescrição
enabledbooleanfalseSe o fio entre colunas é desenhado.
colorColorValue#ccccccCor do fio.
lineWidthDimension0.5 ptEspessura do fio.

Abaixo de um título na largura da página (span: 'page'), o fio começa onde começa o texto das colunas, sob a faixa do título, seja a faixa pintada pela abertura padrão, seja por um design próprio. Até o postext 1.4, ele corria desde o topo do bloco de texto, atravessando a faixa.

Um estilo de título pode definir um fio próprio no seu layout (veja Estilos de título), e as páginas da sua seção desenham esse fio. Um campo que o estilo deixa sem definir assume o valor do documento, então uma seção que só muda as colunas mantém o fio do documento. Até o postext 1.4, o fio de um estilo nunca era desenhado: toda página desenhava o do documento.

#Tipos de layout

  • 'single': uma coluna que ocupa toda a largura do conteúdo. Funciona melhor em páginas estreitas ou em textos densos, de parágrafos longos.

  • 'double': duas colunas de mesma largura. É o layout editorial clássico, que mantém a medida da linha na faixa ideal de 40 a 50 caracteres para uma leitura confortável.

  • 'oneAndHalf': um layout assimétrico, com uma coluna principal e uma coluna lateral mais estreita. A coluna lateral (controlada por sideColumnPercent) é ideal para notas de margem, figuras pequenas ou conteúdo de apoio. Valores entre 25 % e 40 % funcionam bem, e um canal estreito para números de linha ou marcas marginais fica em torno de 10 % a 15 %. A coluna lateral ocupa sideColumnPercent % da largura do conteúdo, e a coluna principal fica com o que sobra depois da medianiz; assim, com 50 % a coluna lateral é uma medianiz mais larga que a principal. Qualquer valor é diagramado como foi escrito enquanto as duas colunas mantiverem pelo menos 1 % da largura do conteúdo; um valor que deixaria uma delas mais estreita (0 ou menos, ou um valor tão grande que a coluna principal some atrás da medianiz) é limitado ao valor mais próximo que preserve as duas, e um valor que não é número assume o padrão, 33. Nesse caso, os configWarnings do documento trazem { kind: 'sideColumnPercentClamped', path: 'layout.sideColumnPercent', value, used } (o caminho do layout próprio de um estilo de título nomeia o estilo, como em headingStyles[2].layout.sideColumnPercent, e é medido nas margens desse estilo), que o Sandbox lista no painel Verificações; collectConfigWarnings(config) devolve a mesma lista sem diagramar nada. Um layout que não seja 'oneAndHalf' nunca lê o valor e nunca o reporta. Com sideColumnRole: 'floats', o texto corrido nunca entra na coluna lateral: ela vira um canal para as figuras, tabelas e boxes posicionados com span: 'side'. Uma figura ou tabela lateral se empilha a partir do topo do canal na página que a cita primeiro (a figura marginal de um livro didático fica no alto da página mesmo quando o texto a cita mais abaixo); a que não cabe no restante do canal espera o canal da página seguinte. Um boxe lateral se empilha ao lado do texto que interrompe e, quando o restante do canal não o comporta ali, sobe até a posição mais baixa em que ainda cabe (com o pé no pé do canal) ou espera a página seguinte. Quando o texto depois do fechamento do boxe continua na página seguinte (a coluna está cheia, ou as regras de quebra levam esse texto adiante), o boxe fica no seu fechamento, ao lado do texto anterior; um estilo com sideAtColumnEnd: 'after' o alinha, em vez disso, com a primeira linha do texto depois do fechamento, no canal da página seguinte, como pedem os números de linha e os títulos marginais escritos antes da linha a que se referem. Um elemento do design de um título que fica no canal (um numeral de capítulo ancorado na margem externa) também é mantido fora da pilha (veja Altura reservada). Flutuantes e boxes com span: 'page' continuam atravessando as duas colunas, e um flutuante de coluna com placement.captionSide põe a legenda no canal, alinhada com a figura. Combinado com margens espelhadas e sideColumnSide: 'outer', o canal fica na borda externa de todas as páginas: a coluna marginal de um livro didático.

  • 'multiple': de três a oito colunas iguais, tantas quantas indicar columnCount (padrão 3): a grade dos jornais e de muitas revistas (desde o postext 1.18). Cada coluna tem (content width − (n − 1) × gutterWidth) / n de largura. O texto passa de uma coluna para a seguinte como em um layout de duas colunas; o fio de coluna é desenhado em todas as medianizes, o balanceamento de colunas nivela todas as colunas na página que fecha um capítulo e antes de um boxe da largura da página, as notas de rodapé funcionam em todas as colunas, e figuras, boxes e títulos da largura da página atravessam a página inteira. Uma figura ou um boxe flutuante pode ocupar só algumas colunas: placement.columns (veja Tipos de recurso) e o columns de um estilo de boxe. Um columnCount fora de 3 a 8, ou que não seja inteiro, é arredondado e limitado à contagem válida mais próxima (um valor que não é número assume 3), e os configWarnings do documento trazem { kind: 'columnCountClamped', path: 'layout.columnCount', value, used } (o caminho do layout próprio de um estilo de título nomeia o estilo, como em headingStyles[1].layout.columnCount). Um layout de outro tipo nunca lê o valor. Um estilo de título cujo layout é 'multiple' usa o columnCount do documento, a menos que defina o seu, de modo que um jornal composto em cinco colunas pode rodar as páginas de opinião em quatro.

#Cabeços e rodapés

As propriedades header e footer controlam os espaços de cabeçalho e de rodapé de cada página. Cabeços e rodapés são desenhados dentro das margens já existentes da página: não reservam espaço adicional nem reduzem a área de conteúdo.

Quadro do contêiner. Um elemento ancorado em 'container' é posicionado na faixa de margem entre o corpo e a borda de refile, com a largura da área de conteúdo. O contêiner do cabeçalho vai do refile superior até o topo do corpo; o do rodapé, da base do corpo até o refile inferior. Assim, as âncoras top-* do cabeçalho e bottom-* do rodapé são medidas a partir da borda de refile, enquanto as âncoras bottom-* do cabeçalho e top-* do rodapé são medidas a partir da borda do corpo. O contêiner nunca inclui a sangria nem a faixa das marcas de corte, então um cabeçalho ou rodapé fica na mesma posição na página refilada com page.cutLines ativado ou desativado. Ancore em 'page' (a caixa de refile) ou em 'bleed' para ir além da largura da área de conteúdo ou chegar à sangria.

Os espaços usam o modelo unificado de espaço de design: todo elemento tem um placement com uma anchor (no contêiner ou em outro elemento, por #id), um offset opcional e um size opcional. Os campos planos antigos align, marginFromBody, marginFromEdge e width: 'full' continuam aceitos na entrada e são migrados automaticamente para o novo formato; a descrição equivalente no novo formato está documentada abaixo.

Cada espaço contém uma lista de elementos de texto, fio e caixa. A ordem do array é a ordem de pintura (o primeiro elemento é pintado primeiro, o último fica por cima). Isso vale independentemente das âncoras: um elemento pode se ancorar em outro listado depois dele (anchor.to: '#ttl'), de modo que uma caixa de fundo pode vir primeiro e ainda assim ser posicionada em relação ao texto que fica sobre ela.

Padrões embutidos. Quando header ou footer é undefined, o postext aplica um padrão embutido razoável em vez de um espaço vazio:

  • Cabeçalho padrão: {title} alinhado à direita nas páginas ímpares, {chapterTitle} alinhado à esquerda nas páginas pares e um fio de largura total, tudo na cor principal da paleta, Open Sans 8 pt/600, marginFromBody 16pt (texto) / 13pt (fio).
  • Rodapé padrão: {pageNumber} centralizado em todas as páginas, na cor principal da paleta, Open Sans 8 pt/600, marginFromBody 16pt.

Para dispensar os padrões embutidos, defina header: { elements: [] } (ou footer: { elements: [] }). Um array elements vazio e explícito é preservado como “nenhum elemento”; só undefined aciona os padrões.

PropriedadeTipoPadrãoDescrição
elementsHeaderFooterElement[]padrões embutidos quando undefined; [] desativaLista ordenada de elementos de texto e de fio.

#Elementos de texto

Os elementos de texto desenham uma string de modelo com substituição de marcadores. Os marcadores usam a sintaxe {name}; {{ e }} produzem chaves literais.

Os padrões da tabela abaixo são os de um elemento de texto que você mesmo adiciona. O cabeçalho e o rodapé embutidos descritos em Padrões embutidos, acima, são elementos prontos com valores próprios (Open Sans 8 pt/600 na cor principal da paleta), não os padrões do elemento.

PropriedadeTipoPadrãoDescrição
kind'text'—Discriminador.
idstring—Id estável, único dentro do espaço. Outros elementos se ancoram nele com anchor.to: '#id'. O Sandbox atribui um ao criar o elemento.
contentstring''String de modelo. Aceita os marcadores listados abaixo, além de : um atributo escrito na linha do H1 do capítulo atual (# Title ). Um atributo ausente vira uma string vazia, sem aviso. Uma quebra de linha (ou os dois caracteres \n, escritos no modelo ou no valor de um atributo) sempre começa uma nova linha, qualquer que seja o overflow. O texto que um marcador copia do documento (um título, um campo do frontmatter) é impresso como está escrito: ali, só uma quebra de linha real, como a quebra de um título, começa uma nova linha.
align'left' | 'center' | 'right' | 'justify' | 'start' | 'end''center'Alinhamento horizontal das linhas dentro da caixa do elemento. 'justify' estica os espaços entre palavras de todas as linhas quebradas, exceto a última de cada parágrafo, para que a linha preencha a caixa; as linhas ao lado de uma capitular preenchem o espaço ao lado dela. Com hyphenate, uma palavra que não cabe no restante de uma linha justificada também é cortada em uma sílaba para preenchê-la. A última linha de um parágrafo, uma linha sem espaço para esticar e um texto que não quebra ficam alinhados à esquerda. O texto justificado é diagramado parágrafo a parágrafo, então várias linhas em branco entre parágrafos contam como uma. O canvas e o PDF colocam cada palavra onde o layout a pôs; o HTML alarga os espaços com word-spacing. 'start' e 'end' seguem a direction do texto: a direita e a esquerda de um texto da direita para a esquerda. 'left' e 'right' são os lados da própria caixa; em um design diagramado no fluxo de uma página da direita para a esquerda (uma faixa de abertura, o design de um título) o fluxo é espelhado, então eles são o início e o fim, como no texto corrido.
direction'ltr' | 'rtl' | 'auto'a do documentoDireção base do texto: para onde vão seus caracteres neutros, a ordem dos trechos em uma linha e o que significam os lados 'start' e 'end'. 'auto' lê a primeira letra forte do texto resolvido e, sem ela, usa a direction do documento. Trechos em árabe e hebraico são lidos da direita para a esquerda, qualquer que seja a base. Um texto que contém uma letra árabe nunca recebe espaçamento entre letras (letterSpacing é ignorado para o texto inteiro) e suas palavras nunca são cortadas ao quebrar ou truncar a linha; uma palavra mais larga que a caixa transborda e é reportada como unbreakableWordOverflow.
parity'all' | 'odd' | 'even''all'Em quais páginas o elemento aparece (paridade do número da página: a página 1 é ímpar).
pages'all' | 'body' | 'opener' | 'part' | 'blank''all'Em quais funções de página o elemento aparece, combinado com parity. Depois do posicionamento, cada página é classificada como 'blank' (página de preenchimento por paridade ou separador, ou sem conteúdo), 'part' (uma página divisória de parte), 'opener' (seu primeiro bloco é um título cujo nível ocupa a página ou força uma quebra de página antes dele: a primeira página de um capítulo) ou 'body' (todas as outras). pages: 'body' esconde um cabeço nas aberturas de capítulo; pages: 'opener' mostra um fólio só nelas.
fontFamilystring'EB Garamond'Família tipográfica.
fontSizeDimension8 ptTamanho da fonte.
fontWeightnumber400Peso da fonte (100–900).
italicbooleanfalseSe o texto é desenhado em itálico.
colorColorValue#000000Cor do texto.
overflow'wrap' | 'ellipsis-start' | 'ellipsis-middle' | 'ellipsis-end' | 'clip''ellipsis-end'Como o motor trata o texto que excede a largura disponível do elemento. 'wrap' quebra o texto em várias linhas; as variantes com reticências mantêm cada linha em uma só linha e a truncam com … no início, no meio ou no fim; 'clip' corta rente à caixa delimitadora do elemento, sem inserir nenhum caractere. As quebras de linha do conteúdo valem em todos os modos: os modos de reticências e de corte truncam ou cortam cada linha separadamente. Um elemento que omite overflow recebe 'ellipsis-end', então defina 'wrap' para tudo o que possa ocupar várias linhas (um endereço, um título longo). Um texto com dropCap quebra linhas independentemente deste valor; com 'clip', suas linhas ainda são cortadas na borda de uma caixa de altura fixa. 'ellipsis-end' e 'ellipsis-start' cortam em um limite de palavra (The history of…, não The history of th…), e nenhum espaço ou pontuação de ligação (vírgula, dois-pontos, travessão, barra, parêntese de abertura) encosta nas reticências. Uma palavra só é cortada onde for preciso quando o limite de palavra, descartada essa pontuação, manteria menos da metade do que cabe: uma única palavra longa, ou um URL (http://exampl…, não http…). Um espaço não separável ou um hífen não separável (U+2011, como em MS‑DOS) não é limite de palavra; 'ellipsis-middle' corta em qualquer ponto, mas descarta os espaços ao lado das reticências. Até o postext 1.4, todos os modos cortavam no último caractere que cabia, espaços incluídos.
verticalAlign'top' | 'middle' | 'bottom''middle'Onde o texto fica dentro de uma caixa mais alta que suas linhas: uma placement.size.height fixa, ou uma caixa esticada por um vizinho ancorado. Quando as linhas são mais altas que a caixa (um numeral grande em uma caixa de altura fixa, uma lineHeight apertada), elas transbordam pelo lado que o alinhamento deixa livre, como faz o alinhamento flex do CSS: 'bottom' mantém o pé da caixa da última linha no pé da caixa e transborda por cima, 'middle' transborda por igual nas duas pontas, 'top' pelo pé. Até o postext 1.4, essas linhas sempre pendiam do topo, qualquer que fosse o alinhamento. Uma dropCap acompanha suas linhas (até o postext 1.4 ela ficava no topo de uma caixa em que 'middle' ou 'bottom' as tinha deslocado para baixo).
lineHeightnumber | Dimension1.2Entrelinha das linhas do elemento. Um número é um múltiplo de fontSize. Uma Dimension também é aceita, do mesmo jeito em que se escrevem todas as outras entrelinhas da configuração: em / rem é o mesmo múltiplo, e um comprimento absoluto (pt, mm, px…) é a distância entre linhas de base: define uma entrelinha de 15,5 pt, qualquer que seja o tamanho. Qualquer outra coisa (zero, um número negativo, uma dimensão malformada) assume o padrão. Até o postext 1.4, uma Dimension aqui tornava impossível medir a altura do design: uma abertura não reservava espaço nenhum, nem mesmo a sua minHeight, e o corpo passava por baixo do título.
letterSpacingDimension0Tracking: espaço extra avançado depois de cada caractere, espaços incluídos, exatamente como faz o letter-spacing do CSS. As larguras medidas crescem com ele, então uma caixa de largura automática continua justa. O tracking depois do último caractere de uma linha fica fora do alinhamento e da caixa de largura automática, de modo que um título com tracking centralizado fica centralizado nas suas letras, um alinhado à direita termina na borda e a última letra de uma linha justificada chega à borda (até o postext 1.4 eles ficavam meia unidade de tracking, ou uma inteira, à esquerda, e um elemento ancorado à direita de outro com tracking ficava uma unidade de tracking mais longe). Um valor negativo aperta as letras (um título de display a 36 pt costuma levar { value: -0.3, unit: 'pt' }) e as larguras diminuem do mesmo modo; canvas, HTML e PDF o pintam igual (até o postext 1.4 um valor negativo era aplicado como 0, sem aviso).
textTransform'none' | 'uppercase''none'Transformação de caixa aplicada ao texto resolvido, marcadores incluídos: o título de uma parte em maiúsculas no sumário.
boxElementBoxStyle—Fundo e borda opcionais desenhados atrás do texto: backgroundColor, borderColor, borderWidth, borderRadius e um padding por lado que amplia a caixa além do texto (veja Elementos de caixa para os campos).
dropCap{ lines, fontFamily, fontWeight, fontSize, color, gap }—Capitular: a primeira letra composta em tamanho grande ao lado das primeiras lines linhas (padrão 2), com família, peso e cor próprios, a gap de distância do texto. A letra assenta na linha de base da última linha que ocupa, e fontSize tem como padrão o tamanho que alinha o topo dela com as maiúsculas da primeira linha: o tamanho do texto mais lines − 1 entrelinhas, considerando as maiúsculas como 0,72 do tamanho da letra (uma família cujas maiúsculas sejam bem mais altas ou mais baixas que isso pede um fontSize próprio). Um texto com capitular quebra linhas qualquer que seja o seu overflow; com 'clip', suas linhas ainda são cortadas na borda de uma caixa de altura fixa. Uma color vinculada à paleta acompanha as paletas de parte e de seção, como o resto do design. No design de um título, a letra não reserva espaço abaixo do texto: a parte da caixa da sua linha que fica abaixo da linha de base não empurra o corpo para baixo. Uma letra que desce abaixo da linha de base (um Q ou um J em muitas famílias) pode então invadir o espaço sob o design: dê ao título uma marginBottom para isso. Até o postext 1.4, o tamanho padrão deixava a letra tão alta quanto todas as caixas de linha que ela ocupa, de modo que o topo dela ficava acima da primeira linha; um overflow diferente de 'wrap' descartava a letra sem aviso; uma paleta de seção ou de parte deixava a cor dela como estava; e uma letra tão profunda quanto o texto ao lado podia empurrar o corpo uma linha da grade para baixo. Configurações salvas antes mantêm o tamanho do 1.4, escrito como fontSize (veja Pacotes escritos pelo postext 1.4 ou anterior).
paragraphIndentDimension0Recuo de primeira linha de todos os parágrafos depois do primeiro. Uma quebra de linha no conteúdo (ou os dois caracteres \n, para texto que vem do valor de um atributo) separa parágrafos; quebras de linha consecutivas contam como uma.
hyphenatebooleanfalseQuando é true e o texto quebra linhas (overflow: 'wrap', ou uma dropCap, que sempre quebra), as palavras longas que ainda transbordariam depois de uma quebra de linha normal são divididas nos limites de sílaba (usando o idioma de hifenização ativo do documento), com um hífen condicional na quebra. Em um texto justificado (align: 'justify'), uma palavra que não cabe no restante de uma linha também é cortada na última divisão silábica que cabe, para preencher a linha.
inlineMarksbooleanfalseLê o texto resolvido (valores dos marcadores incluídos) como Markdown inline: bold, italic, ^superscript^, ~subscript~. Desativado, os sinais são impressos como foram escritos. No design de um título, então mantém os trechos de negrito, itálico, sobrescrito e subscrito do próprio título (Pneumocystis, CO~2~), com os demais caracteres escapados para serem impressos como foram escritos (desde o postext 1.19). Veja Marcas inline e contornos.
stroke{ width, color?, hollow? }—Contorno desenhado em volta das letras: width (uma Dimension, centrada nas bordas dos glifos), color (padrão: a cor do texto; uma capitular usa a própria cor) e hollow (true pinta só o contorno). Veja Marcas inline e contornos.
writingMode'horizontal-tb' | 'vertical-rl''horizontal-tb''vertical-rl' compõe o texto de cima para baixo, com as linhas da direita para a esquerda e os caracteres em pé: um cabeço descendo pela margem externa, um título vertical ao lado de um capítulo horizontal. Veja Elementos de texto verticais.
reservebooleantrueSó em designs de título: se o elemento conta para a altura que o título reserva no fluxo do texto. false para decoração que pode ficar sob o texto (um selo no pé da página, uma moldura, uma faixa lateral). Veja Altura reservada. Os designs de cabeçalho, de rodapé e de parte o ignoram.
marginFromBodyDimension6 ptDistância absoluta entre a borda do elemento voltada para o corpo e a borda do corpo. Independe dos outros elementos. Migrado para placement.offset.y.
marginFromEdgeDimension0 ptRecuo horizontal a partir da borda de conteúdo alinhada. Só se aplica quando align é 'left' ou 'right'. Migrado para placement.offset.x.
placementElementPlacementderivado de align + marginFromBody + marginFromEdgePosicionamento avançado (veja abaixo). Quando definido, tem precedência sobre os campos planos antigos.

Marcadores disponíveis:

  • {pageNumber}: número da página atual, a partir de 1.
  • {totalPages}: total de páginas do documento. Em um livro diagramado capítulo a capítulo (o Sandbox, buildBundle), cada capítulo é um documento, então este é o total do próprio capítulo.
  • {bookTotalPages}: total de páginas do livro inteiro: todos os capítulos, páginas em branco incluídas. Para um documento diagramado sozinho, é igual a {totalPages}. Veja Contagem de páginas do livro abaixo.
  • {title}, {subtitle}, {author}, {publishDate}: valores lidos de content.metadata. Metadados desconhecidos ou vazios viram uma string vazia (e geram um aviso no Sandbox).
  • {chapterTitle}: texto do H1 mais recente na página atual ou antes dela. Um H1 cujo estilo de título define runningChapter: false (uma prancha, um mapa) é ignorado.
  • {chapterTitleAtTop}, {chapterNumberAtTop}: título e número do capítulo vigente no topo da página, que diferem de {chapterTitle} e {chapterNumber} em uma página onde um novo capítulo começa abaixo de outro texto. Veja Capítulo no topo da página abaixo.
  • {partTitle}, {partNumber}: título e número da parte atual (a página :::part mais recente na página atual ou antes dela; as páginas em branco de paridade logo antes de uma página de parte já pertencem a ela). Vazios antes da primeira parte.
  • {firstMark.<key>}, {lastMark.<key>}: a primeira e a última palavra-guia da página: um título de um nível (h1–h6) ou uma entrada de um estilo de parágrafo. Veja Palavras-guia abaixo.

Contagem de páginas do livro

{bookTotalPages} imprime o número de páginas do livro inteiro, o total que o leitor vê em “página 12 de 348”. Conta páginas físicas, em branco incluídas, como {totalPages}, mas somando todos os capítulos:

  • Um documento diagramado sozinho (buildDocument sem continuation) é o livro inteiro: {bookTotalPages} é igual a {totalPages}.
  • buildBundle diagrama o livro, soma as páginas de todos os capítulos e diagrama mais uma vez com esse total, de modo que todos os capítulos imprimem o mesmo número. A contagem nunca move uma quebra de página, então uma rodada a mais resolve; uma configuração que não imprime {bookTotalPages} não paga nada por isso.
  • O Sandbox passa o total a cada capítulo assim que as páginas de todos os capítulos são conhecidas. Até lá, um capítulo imprime as páginas até o seu próprio fim. A exportação do livro inteiro na aba PDF conta as páginas que diagrama: quando um capítulo imprimiu outro total, porque suas páginas ainda não eram conhecidas, ela diagrama o livro mais uma vez com o total a que os capítulos chegaram.
  • Um host que diagrama os capítulos por conta própria passa o total como continuation.bookPageCount (também no primeiro capítulo). Sem isso, {bookTotalPages} conta as páginas até o fim do documento (continuation.pageIndexOffset mais as suas próprias páginas), o que só está certo no último capítulo.
footer: {
  elements: [{
    kind: 'text', id: 'folio', content: '{pageNumber} / {bookTotalPages}',
    fontSize: { value: 8, unit: 'pt' },
    placement: { anchor: { to: 'container', edge: 'top' }, size: { width: 'auto', height: 'auto' } },
  }],
}

configUsesPlaceholder(config, 'bookTotalPages') diz a um host se vale a pena calcular a contagem.

Palavras-guia: primeira e última marca

Um dicionário imprime no cabeço a primeira e a última entrada de cada página (“Aback – Anchor”); uma obra de referência imprime a primeira e a última seção. {firstMark.<key>} e {lastMark.<key>} as imprimem. A chave indica o que marca uma página:

  • h1 a h6: um título desse nível. A marca é o texto dele, sem o número.
  • Um id de estilo de parágrafo (entry): um parágrafo de um contêiner :::paragraphs{style="entry"}. A marca é o trecho em negrito que abre o parágrafo, a palavra-entrada, sem a pontuação final: **Aback.** Said of… marca Aback. Um parágrafo que não começa com texto em negrito não define marca.

{firstMark.<key>} é a primeira marca que começa na página e {lastMark.<key>}, a última. Uma página em que nenhuma marca começa (uma entrada longa que continua) imprime nos dois a marca vigente, a última antes dela. As páginas anteriores à primeira marca não imprimem nada; em um livro diagramado capítulo a capítulo, isso vale para a primeira marca do capítulo, já que as marcas não passam de um capítulo para o seguinte. Um título ou parágrafo dividido entre páginas marca só a página em que começa. A chave é escrita depois do ponto com letras, dígitos, _ e -, começando por uma letra ou _; uma chave desconhecida não imprime nada.

:::paragraphs{style="entry"}
**Aback.** Said of the sails when pressed back against the mast.
 
**Abaft.** Towards the stern, or behind a given point.
:::
header: {
  elements: [{
    kind: 'text', id: 'guide', content: '{firstMark.entry} – {lastMark.entry}',
    fontSize: { value: 8, unit: 'pt' },
    placement: { anchor: { to: 'container', edge: 'bottom' }, size: { width: 'auto', height: 'auto' } },
  }],
}

As palavras-guia são cabeços: são resolvidas nos espaços de cabeçalho e de rodapé (incluídos o header e o footer de um estilo de título) e não imprimem nada em designs de título, de parte e de sumário; nos designs de título e de parte, o painel Verificações as aponta como marcadores desconhecidos. Para mostrar a primeira entrada nas páginas pares e a última nas ímpares, use dois elementos, com parity: 'even' e parity: 'odd'. Um H1 cujo estilo de título define runningChapter: false não define marca h1.

Capítulo no topo da página

{chapterTitle} e {chapterNumber} nomeiam o último capítulo iniciado na página ou antes dela. Em um livro cujos capítulos correm seguidos, sem quebra de página entre eles, uma página que fecha um capítulo e começa o seguinte perto do pé leva o título do novo capítulo sobre um texto que ainda pertence ao anterior. {chapterTitleAtTop} e {chapterNumberAtTop} nomeiam, em vez disso, o capítulo vigente no topo da página, como fazem os romances com capítulos seguidos e muitas obras de referência:

  • uma página cujo primeiro bloco é o H1 de um capítulo nomeia esse capítulo;
  • qualquer outra página nomeia o capítulo que continua nela, mesmo quando um novo capítulo começa mais abaixo;
  • uma página em branco adicionada por paridade acompanha a página seguinte, e o separador dos modos always-*, a página anterior (veja Pertencimento das páginas em branco); quando a página depois de uma página em branco de paridade começa com o fim de um capítulo, e não com o H1 dele, a página em branco também fica com esse capítulo;
  • um H1 com runningChapter: false é ignorado.
header: {
  elements: [{
    kind: 'text', id: 'chapter', content: '{chapterTitleAtTop}', parity: 'odd', pages: 'body',
    fontSize: { value: 8, unit: 'pt' },
    placement: { anchor: { to: 'container', edge: 'bottom-right' }, size: { width: 'auto', height: 'auto' } },
  }],
}

Como as palavras-guia, os dois são cabeços: são resolvidos nos espaços de cabeçalho e de rodapé e não imprimem nada em designs de título, de parte e de sumário. {attr.<key>} sempre lê o último capítulo iniciado.

Alinhamento implícito pela borda

Quando o placement.anchor.to de um elemento de texto se refere a outro elemento por #id, a borda da âncora implica um alinhamento de texto padrão para as linhas quebradas:

  • right-of e align-left implicam align: 'left' no texto: as linhas quebradas correm para a direita a partir da âncora.
  • left-of e align-right implicam align: 'right': as linhas quebradas se apoiam no lado mais próximo do alvo da âncora.

O editor de títulos do Sandbox aplica esses alinhamentos implícitos automaticamente quando você muda a borda ou o alvo da âncora. Eles mantêm um texto quebrado em várias linhas visualmente ancorado no elemento a que se refere (assim, por exemplo, o “P” de um “Postext” quebrado fica alinhado na vertical sob o “I” de “Introduction”).

Marcas inline e contornos

Por padrão, um elemento de texto compõe o texto em uma só fonte: **, ^ e os demais sinais de Markdown são impressos como foram escritos. Com inlineMarks: true, o texto resolvido é lido como Markdown inline, com as mesmas marcas que o texto corrido aceita (veja Formato do documento → Formatação inline):

  • **bold** aplica peso 700 (ou o fontWeight do próprio elemento, quando for mais pesado); *italic* inverte a inclinação do elemento, de modo que a ênfase dentro de um elemento em itálico sai em redondo; ***both*** faz as duas coisas. As formas com sublinhado (__bold__, _italic_) também funcionam.
  • ^superscript^ e ~subscript~ são compostos a 58 % do tamanho, o sobrescrito elevado em um terço dele e o subscrito rebaixado em 0,15 dele; um subscrito e um sobrescrito que se tocam (T~0~^2^) são empilhados, como no corpo.
  • Uma barra invertida compõe o próprio sinal (\*, \_, \^, \~). Um link mantém o texto; as crases de código são descartadas.

As marcas são lidas depois que os marcadores são preenchidos, então um valor pode trazê-las: a linha de autores em um atributo de título recebe os números de afiliação como sobrescritos. Quebra de linha, justificação, os modos de reticências, dropCap e paragraphIndent funcionam com texto marcado; todos os trechos mantêm a cor do elemento.

{ "kind": "text", "id": "authors", "content": "{attr.authors}", "inlineMarks": true, "overflow": "wrap",
  "fontSize": { "value": 11, "unit": "pt" },
  "placement": { "anchor": { "to": "#title", "edge": "below" }, "offset": { "y": { "value": 6, "unit": "pt" } } } }
# Snow cover and river flow {authors="Ana Ruiz^1^, Luis Gil^2^ and Marta Sanz^1,3^"}

stroke desenha um contorno em volta das letras: width é a espessura do traço, centrado nas bordas dos glifos (metade fica dentro das letras, metade fora; a largura medida do texto não muda), color tem como padrão a cor do texto, e hollow: true deixa as letras sem preenchimento, para que só o contorno apareça: um número de display vazado, um título que se destaca sobre uma fotografia. O contorno é pintado sobre o preenchimento, do mesmo jeito no canvas, no HTML (-webkit-text-stroke) e no PDF (modo de renderização de texto 2, ou 1 quando vazado).

{ "kind": "text", "id": "year", "content": "1863", "fontFamily": "Bitter", "fontSize": { "value": 120, "unit": "pt" }, "fontWeight": 700,
  "color": { "hex": "#1d3557", "model": "hex" },
  "stroke": { "width": { "value": 1.5, "unit": "pt" }, "hollow": true },
  "placement": { "anchor": { "to": "page", "edge": "bottom-right" }, "offset": { "x": { "value": -15, "unit": "mm" }, "y": { "value": -20, "unit": "mm" } } } }

No PDF, os trechos em negrito e itálico incorporam as fontes correspondentes da família do elemento, então o provedor de fontes precisa fornecê-las.

Valores padrão do texto e armadilhas da ancoragem

Um elemento de texto que você mesmo escreve parte destes valores, alguns deles surpreendentes:

  • overflow é 'ellipsis-end'. Um texto largo demais para o espaço é cortado em uma linha com …. Defina overflow: 'wrap' para um endereço, uma linha de autoria ou qualquer título que possa ficar longo. Uma quebra de linha no conteúdo (uma quebra real, ou \n no modelo ou no valor de um atributo) começa uma nova linha em todos os modos; os modos de reticências cortam cada linha separadamente.
  • align é 'center' e verticalAlign é 'middle'. Um elemento de largura automática se ajusta à sua linha mais longa, então as linhas ficam centralizadas umas em relação às outras; defina align: 'left' para um bloco alinhado à esquerda (um elemento de largura automática ancorado em outro elemento alinha as linhas sozinho pelo lado da âncora, veja acima).
  • A fonte é EB Garamond 8 pt, preta, lineHeight 1.2, qualquer que seja a do texto corrido. Ao contrário das outras entrelinhas da configuração, a lineHeight de um texto de design costuma ser um múltiplo simples (1.2); uma Dimension também funciona (veja acima).

Um elemento sem placement.size.width (ou com 'auto') se dimensiona pelo seu texto, mas só dentro do espaço entre o ponto de âncora e a borda do contêiner para a qual ele cresce; para uma âncora top ou bottom, o dobro da distância até a borda mais próxima. O offset dele conta. Um deslocamento para longe dessa borda não custa nada: um cabeço ancorado em top-left com x negativo (pendurado na margem esquerda) não perde espaço, porque cresce para a direita. Um deslocamento em direção a essa borda reduz o espaço na mesma medida (o dobro, para uma âncora top ou bottom), e um que empurra a âncora além da borda não deixa espaço nenhum: uma âncora top-right cujo x fica abaixo de menos a largura do contêiner, ou uma âncora top deslocada para o lado em mais da metade dessa largura. Sem espaço, um modo de reticências não imprime nada e 'wrap' empilha um caractere por linha. Três saídas:

  • dar ao elemento uma size.width fixa: larguras fixas nunca são limitadas;
  • ancorá-lo em 'page' ou 'bleed', o que faz da página (ou da sangria) o quadro do seu espaço;
  • ancorá-lo na borda oposta do contêiner.

O contêiner de um cabeçalho vai do refile superior até o corpo, e o de um rodapé, do corpo até o refile inferior: no cabeçalho, as âncoras top-* são medidas a partir da borda de refile e as bottom-* a partir do corpo, e o contrário no rodapé (veja Quadro do contêiner, acima). Um cabeçalho ou rodapé nunca desloca o texto corrido e é pintado por cima dele, então um elemento empurrado para a área do corpo cobre o texto. Já uma faixa de abertura é pintada por baixo do texto corrido; quanto espaço ela ocupa no fluxo está descrito em Altura reservada.

#Elementos de texto verticais

Um elemento de texto com writingMode: 'vertical-rl' é composto na vertical em um espaço cujo texto é horizontal: os cabeços e fólios de qualquer livro, que ficam na folha, e todos os designs de uma página horizontal. Ele é diagramado como um texto horizontal em um quadro próprio girado um quarto de volta no sentido horário e depois girado de volta para a página:

  • A caixa dele fica onde o posicionamento a coloca. A altura é o comprimento de uma linha: size.height a define (ou 'auto', o comprimento do texto; 'fill', até a borda do contêiner), size.width define quantas linhas cabem na largura, e size.maxWidth limita o comprimento de uma linha.
  • align posiciona as linhas ao longo da caixa ('left' em cima), verticalAlign no sentido transversal ('top' à direita, onde fica a primeira linha), e o padding de uma caixa fica no lado para o qual foi escrito.
  • Os caracteres são medidos e pintados como em uma página vertical: os Han em pé, um em cada, a pontuação na sua forma vertical, as palavras latinas deitadas, os números curtos em uma célula (cjk.uprightDigits). O canvas, o PDF e o HTML o colocam no mesmo retângulo.
  • Com inlineMarks: true, as marcas de orientação destacam um trecho como no corpo: 第:tcy[3.0]回 compõe 3.0 em uma célula, :upright[GDP] põe as letras em pé, uma sob a outra, :sideways[…] gira um trecho; uma linha nunca quebra dentro de um deles. Um elemento horizontal os ignora.
  • Um elemento vertical não leva capitular.
  • No fluxo de uma página vertical (uma abertura, uma página de parte, o título de um boxe) o texto já corre para baixo, e writingMode não muda nada ali.

Os livros chineses verticais põem cabeços e fólios em um de três lugares (clreq §7.2; JLREQ §2.6 para o japonês):

ConvençãoOndeComo configurar
Cabeçalho e rodapé horizontaisAcima e abaixo da mancha, como nos livros horizontais; o mais comum.O cabeçalho e o rodapé como estão.
Margem externa (estilo 中缝, 邊峰 em Taiwan)Descendo pela margem externa: o título do capítulo ou do livro a partir de uns quatro caracteres abaixo do topo da mancha, o fólio terminando uns cinco acima do pé dela, em numerais chineses, a cerca de 80 % do tamanho do corpo.Dois elementos verticais ancorados em 'outer', abaixo.
Canto externo do péO fólio no pé da página, no canto externo (as regras de Taiwan para livros 中式).Um elemento horizontal no rodapé em 'bottom-left' com parity: 'odd' e outro em 'bottom-right' com parity: 'even', em um livro com encadernação à direita (o contrário em um com encadernação à esquerda).

Os cabeços na margem externa, como o Sandbox os adiciona (Cabeçalho › Cabeços na margem externa (vertical)), aqui para um corpo de 10 pt (o Sandbox os compõe a 80 % do tamanho do corpo):

{
  "page": { "pageNumbering": { "format": "trad-chinese-informal" } },
  "header": { "elements": [
    { "kind": "text", "id": "head", "content": "{chapterTitle}", "writingMode": "vertical-rl",
      "fontSize": { "value": 8, "unit": "pt" }, "overflow": "clip", "align": "left",
      "placement": { "anchor": { "to": "outer", "edge": "top" }, "offset": { "y": { "value": 4, "unit": "em" } } } },
    { "kind": "text", "id": "folio", "content": "{pageNumber}", "writingMode": "vertical-rl",
      "fontSize": { "value": 8, "unit": "pt" }, "overflow": "clip", "align": "left",
      "placement": { "anchor": { "to": "outer", "edge": "bottom" }, "offset": { "y": { "value": -5, "unit": "em" } } } }
  ] }
}

anchor.to: 'outer' (veja Posicionamento de elementos) é a margem externa de cada página, então os dois elementos descem pela borda esquerda de uma página ímpar e pela borda direita de uma página par em um livro com encadernação à direita (page.binding). {pageNumber} é impresso no formato de numeração de páginas: trad-chinese-informal dá 一百零三 na página 103, cjk-decimal dá 一〇三. As regras de cada espaço continuam valendo: pages: 'body' mantém o cabeço fora das aberturas de capítulo. Um pequeno ornamento entre o cabeço e o fólio (um rabo de peixe ︻, um fio) é um elemento comum ancorado no mesmo quadro.

Na VDT, um bloco vertical traz vertical (VDTDesignTextBlock.vertical: a região, os dígitos em pé e o eixo central de cada família); as linhas dele ficam no quadro girado do próprio bloco, xOffset para baixo a partir do topo da caixa, baselineY para a esquerda a partir da borda direita. Um trecho de uma linha marcada traz tcy ou orientation, como um segmento do corpo.

#Elementos de fio

Os elementos de fio desenham uma linha: horizontal, atravessando o espaço, ou vertical, descendo por ele.

PropriedadeTipoPadrãoDescrição
kind'rule'—Discriminador.
idstring—Id estável, único dentro do espaço, para referências anchor.to: '#id'.
direction'horizontal' | 'vertical''horizontal'Um fio horizontal se estende por placement.size.width ('fill' = até a borda do contêiner) e tem thickness de altura. Um fio vertical desce por placement.size.height ('fill' ou não definido = até a borda do contêiner) e tem thickness de largura: um divisor entre um cabeço e um fólio.
colorColorValue#000000Cor do traço.
thicknessDimension0.5 ptEspessura da linha. Um fio que a omite é desenhado com o padrão (até o postext 1.4 não pintava nada).
widthDimension | 'full''full''full' ocupa a área de conteúdo; uma Dimension limita a linha a um comprimento fixo, posicionado por align.
align'left' | 'center' | 'right''center'Alinhamento quando width não é 'full'.
marginFromBodyDimension6 ptDistância absoluta entre a borda do fio voltada para o corpo e a borda do corpo. Independe dos outros elementos.
marginFromEdgeDimension0 ptRecuo horizontal a partir da borda de conteúdo alinhada. Só se aplica quando width é uma Dimension fixa e align é 'left' ou 'right'.
parity'all' | 'odd' | 'even''all'Em quais páginas o fio aparece.
pages'all' | 'body' | 'opener' | 'part' | 'blank''all'Funções de página em que o fio aparece (veja o campo pages dos elementos de texto).
reservebooleantrueSó em designs de título: se o fio conta para a altura que o título reserva no fluxo do texto (veja Altura reservada).
placementElementPlacementderivado de align + marginFromBody + marginFromEdgePosicionamento avançado (veja Posicionamento de elementos). size.width / size.height definem o comprimento do fio; width: 'fill' é o antigo 'full'.

#Elementos de caixa

Os elementos de caixa pintam um retângulo de cantos arredondados dentro do espaço, úteis como fundo atrás do texto em aberturas de capítulo, barras laterais ou rodapés. Os elementos de caixa são posicionados exclusivamente pelo campo placement; não têm a forma abreviada plana antiga. Preenchimento, traço e raio dos cantos ficam no objeto aninhado style (ElementBoxStyle), como no exemplo JSON em “Posicionamento de elementos”.

PropriedadeTipoPadrãoDescrição
kind'box'—Discriminador.
idstring—Id estável, único dentro do espaço. Os elementos irmãos se ancoram nele com anchor.to: '#id'. O Sandbox atribui um ao criar o elemento.
style.backgroundColorColorValuetransparentCor de preenchimento. Defina como transparent para uma caixa só com contorno.
style.borderColorColorValuetransparentCor do traço.
style.borderWidthDimension0 ptEspessura do traço. Os traços são pintados no lado de dentro do retângulo delimitador da caixa, para que as dimensões externas não mudem: a borda externa do traço acompanha a borda da caixa, e uma caixa arredondada mantém o raio externo. Um traço tão largo quanto a caixa a preenche. Canvas, HTML e PDF o desenham igual (até o postext 1.4, o canvas e o PDF centravam o traço na borda, com metade dele fora da caixa).
style.borderRadiusDimension0 ptRaio dos cantos. Limitado à metade do lado menor no momento da renderização.
placementElementPlacement—Obrigatório. Veja “Posicionamento de elementos” abaixo.
parity'all' | 'odd' | 'even''all'Em quais páginas a caixa aparece.
pages'all' | 'body' | 'opener' | 'part' | 'blank''all'Funções de página em que a caixa aparece (veja o campo pages dos elementos de texto).
reservebooleantrueSó em designs de título: se a caixa conta para a altura que o título reserva no fluxo do texto (veja Altura reservada).

#Elementos de imagem

Um elemento image desenha um recurso bitmap ou SVG do documento: o logotipo da editora em uma folha de rosto, uma marca em um cabeço. Ele é dimensionado pelo seu placement.size: com um de width / height em 'auto' (o padrão), o outro lado acompanha a proporção da imagem; com os dois definidos, a imagem é ajustada dentro da caixa e centralizada. Um recurso ausente, ou que não seja imagem, não desenha nada.

{
  kind: 'image', id: 'logo', resourceId: 'logo-publisher',
  placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: { value: 64, unit: 'mm' }, y: { value: 233, unit: 'mm' } }, size: { width: { value: 83, unit: 'mm' }, height: 'auto' } },
}

resourceId aceita os mesmos marcadores que o content de um elemento de texto, de modo que um único design pode desenhar uma imagem diferente para cada título. Com resourceId: '{attr.vignette}' em um estilo de título, # Chapter I {style="opener" vignette="log"} desenha o recurso log e # Chapter II {style="opener" vignette="wig"} desenha wig: os capítulos compartilham o estilo em vez de terem um clone por imagem. Um cabeçalho ou rodapé lê os atributos do capítulo da página, como faz {attr.<key>} em um cabeço, e outros marcadores também funcionam ('map-{chapterNumber}'). Um id que resulta vazio, como o de um título sem o atributo, não desenha nada. Desde o postext 1.8.

PropriedadeTipoPadrãoDescrição
idstring—Identificador estável; outros elementos podem se ancorar nele como #id.
resourceIdstring—Id de um Resource bitmap ou SVG do documento. Pode conter marcadores, entre eles {attr.<key>}, preenchidos por título, parte ou página.
decorativebooleanfalseA imagem é só decoração (um ornamento, uma faixa): não passa texto alternativo para a saída, mesmo quando o recurso tem um (veja abaixo).
placementElementPlacement—Âncora, deslocamento e tamanho (veja Posicionamento de elementos). Um lado 'fill' vai até a borda do contêiner.
parity, pagescomo acima'all'Em quais páginas a imagem aparece.
reservebooleantrueSó em designs de título: se a imagem conta para a altura que o título reserva no fluxo do texto (veja Altura reservada).

O renderizador de PDF incorpora o recurso como uma figura (um SVG com matriz de impressão usa a matriz); o visualizador HTML o resolve por meio de resourceImageUrl.

Uma imagem desenhada por um design é conteúdo quando o recurso dela a descreve: o altText do recurso ou, na falta dele, a legenda como texto simples (um chip lido pelo rótulo, um :ref pelo seu text, quando tem um) vai para a VDT (VDTDesignImageBlock.altText), vira o alt do <img> no HTML e uma Figure com /Alt em um PDF etiquetado, lida logo depois do texto do seu design (a prancha de um capítulo depois do título do capítulo). Uma imagem cujo recurso não tem nenhum dos dois, e uma marcada como decorative, é decoração: alt="" com role="presentation", e um artefato no PDF. A imagem de um cabeço ou de um rodapé se repete em todas as páginas, então é elemento fixo da página, diga o recurso o que disser: sem altText na VDT, alt="" com role="presentation" no HTML, um artefato da página no PDF.

#Posicionamento de elementos

ElementPlacement é o modelo unificado de posicionamento usado por todos os tipos de elemento (texto, fio, caixa) dentro de qualquer espaço de design: cabeçalho da página, rodapé da página ou o espaço de design avançado de um nível de título. Três informações descrevem um posicionamento:

interface ElementPlacement {
  /** What this element anchors to and which edge of that target. */
  anchor: {
    to: 'container' | 'page' | 'bleed' | 'outer' | `#${string}`; // container = the slot; page = trim box; bleed = trim box + bleed; outer = the outer margin (header, footer); #id = another element
    edge: AnchorEdge;
  };
  /** Distance from the anchor point. */
  offset?: { x?: Dimension; y?: Dimension };
  /** Optional fixed width / height. Width also accepts 'fill' (span the slot).
   *  `maxWidth` caps an 'auto' width (text): the element still shrink-wraps its
   *  content, so elements anchored to it stay attached, but a long text wraps or
   *  ellipsizes there — a running head can reserve room for the label hanging
   *  off it instead of squeezing that label out. */
  size?: { width?: Dimension | 'fill' | 'auto'; height?: Dimension | 'fill' | 'auto'; maxWidth?: Dimension };
}

AnchorEdge aceita:

  • Bordas do contêiner (quando anchor.to é 'container', 'page' ou 'bleed'): top, top-left, top-right, bottom, bottom-left, bottom-right, left, right.
  • Bordas relativas a um elemento (quando anchor.to === '#someId'): right-of, left-of, below, above, align-top, align-bottom, align-left, align-right.

Cada borda relativa a um elemento põe um canto do elemento sobre um canto do elemento em que ele se ancora, e offset o desloca a partir dali:

  • right-of: o canto superior esquerdo sobre o canto superior direito do alvo (ao lado dele, topos alinhados); left-of: o canto superior direito sobre o canto superior esquerdo do alvo;
  • below: o canto superior esquerdo sobre o canto inferior esquerdo do alvo (embaixo dele, bordas esquerdas alinhadas); above: o canto inferior esquerdo sobre o canto superior esquerdo do alvo;
  • align-top e align-left: o canto superior esquerdo sobre o canto superior esquerdo do alvo. Os dois nomes dão o mesmo posicionamento: tanto a borda superior quanto a esquerda ficam alinhadas;
  • align-bottom: o canto inferior esquerdo sobre o canto inferior esquerdo do alvo;
  • align-right: o canto superior direito sobre o canto superior direito do alvo.

Uma borda relativa a um elemento usada com 'container', 'page' ou 'bleed', e uma borda de contêiner usada com '#id', são lidas como o canto superior esquerdo.

anchor.to: 'page' ancora o elemento na caixa de refile (a página física depois do corte) e 'bleed', na caixa de refile ampliada por cutLines.bleed em todos os lados (idêntica à caixa de refile enquanto as marcas de corte estão desativadas). Os dois quadros também passam a ser a referência para size: 'fill' e para a limitação automática de largura, de modo que uma faixa colorida pode ir de borda a borda, independentemente das margens da página:

{ "kind": "box", "id": "band", "placement": { "anchor": { "to": "bleed", "edge": "top-left" }, "size": { "width": "fill", "height": { "value": 6, "unit": "cm" } } }, "style": { "backgroundColor": { "hex": "#1d3557", "model": "hex" } } }

Com as marcas de corte ativadas, tudo o que um elemento pinta além da caixa de sangria é cortado (veja Marcas de corte).

anchor.to: 'outer' (espaços de cabeçalho e de rodapé) ancora o elemento na margem externa da página: da borda da mancha até a borda de refile, no lado oposto à lombada, e do topo da mancha até o pé. Ela fica à direita de uma página ímpar e à esquerda de uma página par em um livro com encadernação à esquerda, e o contrário em um com encadernação à direita (page.binding), de modo que um único elemento serve às duas páginas de uma página dupla: um cabeço descendo pela margem externa (veja Elementos de texto verticais). Em qualquer outro espaço, é lida como 'container'. O offset de um elemento de texto pode ser escrito em em, ems do seu próprio fontSize: quatro caracteres abaixo do topo da mancha é { "y": { "value": 4, "unit": "em" } }.

Dentro do espaço de design avançado de um título, os elementos ancorados na página e na sangria não aumentam a altura reservada para o título, a menos que se estendam abaixo da borda superior do título (uma faixa no alto da página fica atrás da abertura; uma faixa que desce abaixo do título empurra o texto corrido para baixo). Use advancedDesign.minHeight para reservar uma altura fixa de abertura em qualquer caso, e reserve: false em um elemento que não deve empurrar o texto de jeito nenhum. As regras completas estão em Altura reservada.

Cada elemento tem um id estável (atribuído automaticamente pelo Sandbox; você também pode defini-lo à mão). Os elementos ancorados em outros elementos formam um pequeno grafo de dependências que o motor resolve antes de medir, então um elemento pode se encadear em outro sem coordenadas manuais.

O formato antigo align + marginFromBody + marginFromEdge é interpretado na entrada e reescrito como um posicionamento no momento em que a configuração é resolvida, então as configurações existentes continuam funcionando sem mudanças.

#Texto do corpo

A propriedade bodyText controla a tipografia de todo o texto dos parágrafos.

Escala tipográficaHierarquia tipográfica do H1 até o texto pequeno, mostrando os tamanhos relativos de títulos, texto corrido e legendas.H1Título 132pxH2Título 224pxH3Título 320pxCorpoTexto corrido16pxPequenoLegenda / nota13px
Uma escala consistente mantém a hierarquia legível à primeira vista.
Escala de espaçamentoUma escala de espaçamento em degraus, com valores crescentes usados em margens, preenchimentos e intervalos.xs4 px×1sm8 px×2md16 px×4lg24 px×6xl40 px×102xl64 px×164 px
Os degraus de espaçamento criam um ritmo previsível na diagramação.
PropriedadeTipoPadrãoDescrição
fontFamilystring'EB Garamond'Família tipográfica do texto corrido. Qualquer Google Font, fonte do sistema ou família personalizada declarada em customFonts. Uma família, não uma pilha de fontes CSS (veja abaixo).
fontSizeDimension8 ptTamanho de fonte base do texto corrido.
lineHeightDimension1.5 emEspaçamento vertical entre as linhas. Unidades relativas (em, rem) acompanham o tamanho da fonte.
paragraphSpacingbooleanfalseQuando ativado, insere uma linha em branco (igual a lineHeight) entre parágrafos consecutivos, para uma separação no estilo editorial.
colorColorValue#000000Cor do texto.
boldColorColorValueCor principal (#295AA3)Cor aplicada aos trechos em negrito/destaque forte. É resolvida pela entrada main-color da paleta padrão, então mudar a cor da paleta recolore todos os trechos em negrito do documento.
italicColorColorValueCor principal (#295AA3)Cor aplicada aos trechos em itálico/ênfase. Mesmo padrão vinculado à paleta que boldColor.
referenceColorColorValueCor principal (#295AA3)Cor aplicada aos rótulos :ref inline (referências a recursos). Mesmo padrão vinculado à paleta que boldColor. Acompanha a paleta desde o postext 1.5; até o 1.4 ficava em #295AA3, qualquer que fosse a cor principal.
referenceBoldbooleantrueDesenha os rótulos :ref inline com a fonte em negrito. Uma referência mantém a ênfase do texto ao redor (dentro de … ela fica em negrito itálico com qualquer um dos valores); esta opção só acrescenta o negrito.
referenceItalicbooleanfalseDesenha os rótulos :ref inline em itálico. Uma referência dentro de texto em itálico fica em itálico com qualquer um dos valores.
emphasis'auto' | 'italic' | 'bold' | 'color' | 'overline''auto'Como … é composto: em itálico, na fonte em negrito e em boldColor, em redondo em italicColor, ou em redondo com um fio sobre as palavras. 'auto' é 'bold' em um documento escrito em alfabeto árabe e 'italic' em qualquer outro. Veja Texto árabe.
tashkil'keep' | 'strip' | 'strip-vowels''keep'Sinais vocálicos árabes: compostos como foram escritos, todos removidos, ou removidos exceto a shadda. Veja Texto árabe.
textAlign'left' | 'justify' | 'start' | 'end''justify'Alinhamento do texto. 'left' (ou 'start') é o lado em que a linha começa: a direita, em um parágrafo da direita para a esquerda (veja Direção do texto). O texto justificado distribui o espaçamento em cada linha para obter bordas regulares. As últimas linhas dos parágrafos justificados ficam em bandeira, na sua largura natural, exceto quando o Knuth-Plass aceitou uma última linha cheia demais contando com a compressão da cola; nesse caso, os espaços entre palavras são comprimidos para que a linha caiba exatamente na medida (a semântica de ajuste da cola do TeX, aplicada do mesmo jeito nos renderizadores canvas, HTML e PDF).
fontWeightnumber400Peso do texto normal (100–900).
boldFontWeightnumber700Peso do texto em negrito/destaque forte (100–900).
hyphenationHyphenationConfigativada, 'en-us'Configurações de hifenização automática. Veja abaixo.
firstLineIndentDimension1.5emRecuo aplicado à primeira linha de cada parágrafo (ou a todas as linhas exceto a primeira, quando o recuo deslocado está ativado).
hangingIndentbooleanfalseQuando ativado, o recuo é aplicado a todas as linhas exceto a primeira (recuo francês ou deslocado).
indentAfterHeadingbooleantrueQuando definido como false, o primeiro parágrafo logo depois de um título é composto sem recuo de primeira linha, uma convenção tipográfica comum em publicações científicas e em muitos estilos de livro. O mesmo vale para um parágrafo logo depois de uma linha :::space. Um boxe posicionado fora do texto entre os dois (na coluna lateral, span: 'side', flutuando no topo ou no pé de uma página, ou fixo) e uma figura flutuante são desconsiderados: na sua coluna, o parágrafo continua vindo depois do título e fica sem recuo. Um boxe posicionado no texto (placement: 'here') conta, e o parágrafo depois dele recebe recuo. Não tem efeito quando hangingIndent está ativado.
maxWordSpacingnumber2Limite superior do espaçamento entre palavras no texto justificado, expresso como multiplicador da largura normal do espaço. O Knuth-Plass mantém dentro dele todas as linhas que o parágrafo permite, hifenizando uma palavra ou distribuindo a folga pelas linhas vizinhas primeiro; uma linha que nenhum conjunto de quebras mantém dentro dele se estica além dele, e uma que passa de 3× o espaço normal é composta em bandeira. As linhas que excedem essa proporção são consideradas “frouxas”: veja Linhas que o algoritmo de quebra não consegue preencher, e maxJustifyTracking para deixar que elas recebam um pouco de tracking em vez disso.
minWordSpacingnumber0.6Limite inferior do espaçamento entre palavras no texto justificado, como multiplicador da largura normal do espaço.
maxJustifyTrackingnumber0Tracking máximo que uma linha justificada pode receber, em milésimos de em para mais ou para menos (a unidade do InDesign: 10 = 0,01 em por caractere), quando só os espaços entre palavras a esticariam além de maxWordSpacing ou a comprimiriam além de minWordSpacing. A parte do ajuste que passa do limite vai para as letras, de modo que os espaços de uma linha frouxa voltam a maxWordSpacing e uma linha apertada cabe em minWordSpacing. O Knuth-Plass o considera ao escolher as quebras, e só nas linhas que o espaçamento entre palavras sozinho levaria além dos limites: as demais, a última linha de um parágrafo (a menos que ela transborde), uma linha de uma só palavra e uma linha com um chip não recebem nenhum. A linha o registra como letterSpacing, e o canvas, o HTML e o PDF o pintam. Requer optimalLineBreaking. 0 o desativa. Veja Tracking como último recurso.
kashida'auto' | 'none''auto' em um documento em alfabeto árabe; senão, 'none'Justificação com kashida: uma linha justificada de texto em alfabeto árabe distribui a folga pelos espaços entre palavras (até um quarto da largura deles) e depois em kashidas, tatweels inteiros (U+0640) inseridos entre duas letras ligadas, nunca como espaçamento entre letras. O Knuth-Plass conta o alongamento de cada palavra como elasticidade. Nunca em palavras latinas, algarismos, títulos, linhas em bandeira ou na última linha de um parágrafo. Os tatweels são pintados, mas ficam fora do texto simples e do texto copiado. Veja Kashida no texto árabe.
kashidaPatterns'auto' | 'naskh' | 'simple' | 'nastaliq''auto'Quais ligações recebem kashida, e em que ordem: as regras clássicas do Naskh, as prioridades da Microsoft ou as regras do Naskh adaptadas ao Nastaʿlīq (segundo raqim-kashida). 'auto' lê a fonte do corpo: nenhuma em uma fonte Ruqʿa ou Dīwānī (Aref Ruqaa), regras do Nastaʿlīq em uma fonte Nastaʿlīq, Naskh nos demais casos.
kashidaPerWordnumber1Máximo de alongamentos em uma palavra.
kashidaMaxLengthnumber0.6Alongamento máximo em uma ligação, em ems; recebe tantos tatweels inteiros quantos couberem.
optimalLineBreakingbooleantrueUsa a quebra de linhas ótima de Knuth-Plass em vez do método guloso (a primeira que cabe). Produz um espaçamento entre palavras mais regular ao longo do parágrafo. Um parágrafo em chinês, japonês ou coreano (veja Tipografia do Leste Asiático), ou um com uma palavra mais larga que a coluna, continua sendo composto linha a linha; um parágrafo latino que cita algumas palavras CJK a mantém. O texto em bandeira também a usa com optimalRagged. Veja Hifenização e justificação.
optimalRaggedbooleantrueQuebra também com Knuth-Plass o texto corrido em bandeira: texto do corpo, citações e itens de lista alinhados à esquerda, à direita ou ao centro, e os estilos de parágrafo em bandeira, os corpos de boxe e os corpos das partes e dos estilos de seção. Os espaços entre palavras mantêm a largura. O algoritmo de quebra avalia quanto falta a cada linha para chegar à medida (uma linha 3 em mais curta custa o mesmo que uma linha justificada em maxWordSpacing), de modo que regulariza a borda em vez de encher cada linha antes da seguinte, e as regras de linhas curtas (avoidRunts, tightenRunts) e hyphenateAcrossColumns funcionam no texto em bandeira como no justificado. Com hyphenation.ragged, a zona continua decidindo quais sílabas podem terminar uma linha (veja Texto em bandeira). Títulos, legendas, notas, células de tabela e o sumário em bandeira continuam sendo compostos linha a linha. Requer optimalLineBreaking. false compõe o texto em bandeira linha a linha, como até o postext 1.4; as configurações salvas antes que compunham algum texto corrido em bandeira são lidas com esse valor (veja Pacotes escritos pelo postext 1.4 ou anterior).
breakAfterDashesbooleantruePermite que uma linha termine depois de um travessão ou meia-risca colado entre palavras: say—that’s, riddles.—I, Hamburg–Berlin, também quando a palavra depois do travessão está em outro estilo (see—and). O Knuth-Plass o trata como um espaço entre palavras, e a linha termina no travessão sem nada acrescentado. Nunca depois de um travessão que abre um aparte ou uma fala de diálogo (—dijo, said "—Hola, sagte »—Ich: um espaço, ou um espaço e aspas, antes do travessão; depois de aspas que fecham uma palavra, como em "no"—and, no alemão „nein“—und ou no francês « non »—et, a linha pode terminar), antes de pontuação (él—,), antes de aspas ou de um parêntese (thinking—" and, says—“no”, says—(no): aspas depois de um travessão costumam fechar a fala que o travessão interrompeu), dentro de uma sequência de travessões, nem dentro de um intervalo de números escrito com meia-risca (1914–1918). false mantém as quebras do 1.4: o Knuth-Plass nunca quebra depois de um travessão, e o algoritmo linha a linha do texto em bandeira formatado ou hifenizado só quebra entre duas letras; as configurações salvas antes cujo texto tem um travessão assim são lidas com esse valor (veja Pacotes escritos pelo postext 1.4 ou anterior). Vale para o texto corrido, títulos, listas, citações e boxes. Legendas, notas, células de tabela e o sumário mantêm as quebras do 1.4, e um parágrafo simples em bandeira composto linha a linha segue as regras próprias do pretext em qualquer caso.
breakAfterHyphensbooleantruePermite que uma linha termine depois do hífen de uma palavra composta, um hífen entre duas letras (well- · known, vencer- · se), em todos os parágrafos que o Knuth-Plass quebra. A linha termina no hífen e nada é acrescentado; a quebra tem o custo de uma sílaba. Nunca depois de um hífen junto a um algarismo ou a um sinal (COVID-19, -5 °C). Um parágrafo justificado sem formatação inline só quebra ali com duas letras de cada lado do hífen, para que nenhuma linha termine no e- de e-mail. false mantém as quebras do 1.4: um parágrafo justificado sem formatação inline nunca quebra ali, enquanto o mesmo parágrafo com uma palavra em itálico em qualquer lugar, um parágrafo em bandeira e um parágrafo composto linha a linha quebram; as configurações salvas antes cujo texto tem uma palavra composta são lidas com esse valor (veja Pacotes escritos pelo postext 1.4 ou anterior). Vale para o texto corrido, títulos, listas, citações e boxes. Veja Palavras compostas.
repeatHyphenbooleanfalseComeça também com um hífen a linha que vem depois de uma quebra no hífen de uma palavra composta: vencer- · -se, como pede a ortografia portuguesa, e léxico- · -semántico, como pedem as normas da Real Academia Espanhola desde 2010. O hífen repetido é medido e pintado com a sua linha, que o registra como repeatedHyphen; o plainStart e o sourceStart dela apontam para depois dele, de modo que os links, os cabeços e o Sandbox leem a palavra como foi escrita. O PDF o pinta sob um /ActualText que o deixa de fora, então o texto copiado ou extraído do PDF lê a palavra uma vez só. Um endereço web nunca recebe hífen repetido. Vale para o texto corrido, títulos, listas, citações e boxes; um parágrafo sem formatação que contém uma palavra composta passa então a ser quebrado pelo algoritmo que compõe o texto formatado.
blockquoteBlockquoteConfigveja abaixoComo as citações em bloco do Markdown (> …) são compostas: cor, itálico e recuos. Veja Citações.

#Citações

Uma citação em bloco (linhas que começam com >) usa a família, o tamanho, a entrelinha, os pesos, o alinhamento e a hifenização do corpo. bodyText.blockquote define o resto; sem definição, uma citação fica como era até o postext 1.4: cinza, em itálico, com o recuo de primeira linha do corpo e sem recuo lateral.

PropriedadeTipoPadrãoDescrição
colorColorValue#666666Cor do texto. Uma cor vinculada a uma entrada da paleta (paletteId) acompanha essa entrada, como em todo lugar; as cores de negrito, itálico e referência do corpo não se aplicam dentro de uma citação.
italicbooleantrueCompõe o texto em itálico. Um trecho … dentro dela volta ao redondo; com false, fica em itálico como em um parágrafo.
indentDimension0Recuo de todas as linhas a partir da borda esquerda da coluna ou do boxe. A medida diminui na mesma proporção, então as linhas justificadas terminam na borda direita. em é o tamanho do corpo.
firstLineIndentDimensiono do corpoRecuo da primeira linha de cada parágrafo citado, contado a partir de indent (com o hangingIndent do corpo, o recuo de todas as linhas exceto a primeira). Sem definição: bodyText.firstLineIndent.
bodyText: {
  firstLineIndent: { value: 1.5, unit: 'em' },
  // Upright verse in the body colour, set in by 2 em, no first-line indent.
  blockquote: { color: { hex: '#241f26', model: 'hex' }, italic: false, indent: { value: 2, unit: 'em' }, firstLineIndent: { value: 0, unit: 'em' } },
}

No Sandbox, estas opções formam o grupo Citações da seção Texto do corpo.

#Uma família por fontFamily

fontFamily (aqui e em todos os outros campos de família tipográfica: headings.fontFamily, tableStyle.bodyFontFamily, separatorFontFamily, o fontFamily de um estilo de chip ou de um elemento de design…) nomeia uma família. O canvas, a saída HTML e o PDF precisam compor com a mesma fonte, e o PDF incorpora uma fonte por família, sem cadeia de alternativas, então não há para onde uma pilha de fontes CSS recorrer. Uma pilha é composta na primeira família e reportada como aviso de configuração:

bodyText: { fontFamily: "'EB Garamond', Georgia, serif" } // set in EB Garamond

Carregue essa família antes de diagramar (veja Fontes personalizadas e o provedor de fontes em Gerar PDFs); se ela estiver ausente, o navegador mede com a fonte padrão dele, diga o resto da pilha o que disser. Uma vírgula entre aspas faz parte de um nome ('"Foo, Bar"' é uma família).

#Hifenização

Quando o alinhamento do texto é 'justify', a hifenização evita o espaçamento excessivo entre palavras, dividindo as palavras longas nos limites de sílaba. O motor usa os padrões TeX/Liang para encontrar os pontos de quebra naturais entre sílabas. Veja Hifenização e justificação para uma explicação detalhada. O texto em bandeira só é hifenizado quando você pede: veja Texto em bandeira abaixo.

PropriedadeTipoPadrãoDescrição
enabledbooleantrueSe a hifenização é permitida.
localeLocaleTaglocale do nível superior; senão, 'en-us'Regras do idioma para os limites de sílaba: um dos idiomas suportados abaixo, ou qualquer tag BCP 47 ('es-ES', 'pt-BR').
raggedbooleanfalseHifeniza também o texto em bandeira (alinhado à esquerda, à direita ou ao centro), dentro da zone. Veja Texto em bandeira.
zoneDimension3emZona de hifenização do texto em bandeira: uma palavra que não cabe só é dividida quando mandá-la inteira para a linha seguinte deixaria um vazio maior que este. em é relativo ao tamanho da fonte do próprio texto. Ignorada no texto justificado.
compoundsbooleantruePermite que o dicionário divida as palavras de um composto, uma palavra com um hífen entre duas letras (af-ter-dinner). false mantém essa palavra inteira, exceto no seu próprio hífen, onde a linha ainda pode terminar (after- · dinner), como faz o TeX. Um hífen condicional digitado na palavra continua quebrando, e um composto mais largo que a linha inteira continua sendo dividido. Vale para o texto corrido, títulos, listas, citações e boxes; legendas, notas, células de tabela e o sumário continuam dividindo os compostos. Veja Palavras compostas.

Idiomas suportados: 'en-us' (inglês), 'es' (espanhol), 'fr' (francês), 'de' (alemão), 'it' (italiano), 'pt' (português), 'ca' (catalão), 'nl' (holandês).

As subtags de região, escrita e variante são ignoradas na escolha dos padrões, assim como a caixa das letras e os separadores _: 'es-ES', 'es-MX' e 'es_419' hifenizam com 'es', 'pt-BR' com 'pt', e toda tag inglesa ('en', 'en-GB') com 'en-us', os únicos padrões ingleses incluídos, de modo que o texto britânico recebe quebras americanas. Um idioma sem padrões incluídos ('sv', 'pl', 'fi'…) é hifenizado com os padrões 'en-us', o que produz quebras erradas em vez de nenhuma; o motor o reporta uma vez por tag com um console.warn, e o Sandbox o lista no painel Verificações. Defina enabled: false para um documento assim; isso também silencia o aviso. Chinês, japonês e coreano (zh, ja, ko, qualquer que seja a região ou a escrita) não precisam de padrões nem geram aviso: um documento em um desses idiomas é composto sem hifenização. Para dividir as palavras latinas citadas nele, defina enabled: true e indique o idioma delas em locale ('en-us' para o inglês). enabled: true sem locale, ou com um idioma chinês, japonês ou coreano, deixa a hifenização desativada e avisa uma vez no console. matchHyphenationLocale(tag) devolve o idioma incluído que corresponde a uma tag (undefined quando não há nenhum), e HYPHENATION_LOCALES lista os incluídos. A configuração resolvida (doc.config.bodyText.hyphenation) indica em locale os padrões realmente usados e guarda em tag a tag que você informou, quando for diferente. O renderizador de PDF declara o idioma do documento a partir do locale do nível superior, e a partir desta tag só quando locale não está definido (veja Idioma do documento).

import { matchHyphenationLocale } from 'postext';
 
matchHyphenationLocale('es-MX'); // 'es'
matchHyphenationLocale('en-GB'); // 'en-us'
matchHyphenationLocale('sv');    // undefined: hifenizado com 'en-us', com um aviso no console

Palavras com menos de 5 caracteres nunca são hifenizadas. O motor exige pelo menos 2 caracteres antes e 3 caracteres depois de um ponto de quebra.

Texto em bandeira

Por padrão, só o texto justificado é hifenizado: com textAlign: 'left', e nos estilos de parágrafo alinhados à esquerda, ao centro ou à direita, toda palavra é mantida inteira, por mais irregular que a borda fique, exceto em um hífen condicional (U+00AD) digitado no texto. Defina hyphenation.ragged: true para hifenizar também o texto em bandeira.

Uma linha em bandeira nunca é esticada, então dividir toda palavra que não cabe encheria a borda de hífens. A zona de hifenização limita isso, como faz o compositor de linha única de um programa de editoração. Quando uma palavra não cabe no fim de uma linha, o motor olha o vazio que mandá-la inteira para a linha seguinte deixaria. Se esse vazio for maior que zone, a palavra é dividida na última sílaba que cabe; caso contrário, desce inteira. Uma zona em em é relativa ao tamanho da fonte do próprio texto. O padrão, 3em, só divide as palavras que deixariam uma linha visivelmente curta. Uma zona maior dá menos hífens e uma borda mais irregular; 0 divide toda palavra que não cabe. Na composição linha a linha, no máximo duas linhas seguidas terminam em sílaba. Hífens fixos (enseñanza-aprendizaje), junções de URL, hífens condicionais digitados no texto e palavras mais largas que a linha inteira quebram como sempre: a zona e o limite de duas linhas só regem as sílabas do dicionário.

A configuração vale para o documento inteiro. Aplica-se ao texto do corpo e às citações quando estão em bandeira, e a todo estilo de parágrafo e corpo de boxe em bandeira cuja hyphenation própria esteja ativada (por padrão, ela segue o hyphenation.enabled do corpo), de modo que hyphenation: false mantém inteiras as palavras de um estilo. Títulos, legendas, notas, células de tabela e o sumário não são hifenizados; ali, como em todo lugar, só uma palavra mais larga que a medida inteira é dividida. O texto de design (cabeços, aberturas, páginas de parte) segue a opção hyphenate de cada elemento de texto, que as aberturas de página inteira e as páginas de parte embutidas ativam, e usa também o dicionário do documento. O texto justificado ignora ragged e zone.

const config: PostextConfig = {
  locale: 'es',
  bodyText: {
    textAlign: 'left',
    // Um pouco mais de hifenização que o padrão de 3 em.
    hyphenation: { ragged: true, zone: { value: 2, unit: 'em' } },
  },
};

Com optimalRagged (o padrão), o texto corrido em bandeira é quebrado com Knuth-Plass, e a zona mantém o seu sentido ali: uma palavra só é dividida quando não cabe no restante da linha e mandá-la inteira para baixo deixaria vazio mais que a zona. Duas sílabas seguidas não são recusadas, mas custam o que custam dois hífens seguidos no texto justificado, então uma terceira é rara. Com optimalRagged: false, ou optimalLineBreaking: false, cada linha é preenchida antes da seguinte, como até o postext 1.4.

A hifenização do texto em bandeira diagrama os parágrafos com o algoritmo de quebra que os parágrafos com formatação inline (negrito, itálico, links, fórmulas) sempre usam, então ativá-la pode mudar algumas quebras além dos hífens. Em uma linha em bandeira, além dos espaços e das sílabas do dicionário, esse algoritmo quebra depois de um hífen fixo ou de um travessão entre duas palavras (largas—separadas; com breakAfterDashes, qualquer travessão colado entre palavras, também riddles.—I e riddles—*and*), nas junções de URL e entre ideogramas, e mantém junta uma palavra composta em vários trechos (**Nota**:, (*véase*). Sem a hifenização do texto em bandeira, um parágrafo em bandeira sem formatação é quebrado com Knuth-Plass quando optimalRagged está ativado (o padrão): nos espaços entre palavras, depois de um hífen fixo entre duas letras (meta- · analyses), como faz o outro algoritmo, e, com breakAfterDashes, depois de travessões colados. Na composição linha a linha, ele passa pelo algoritmo de quebra do pretext, que difere em três pontos: também pode quebrar antes de um travessão que fecha um aparte ou depois de um que o abre (él · — y), depois de uma barra ao hifenizar (km/ · h), e corta uma palavra mais larga que a linha em qualquer caractere, sem hífen, onde o outro algoritmo a divide primeiro em uma sílaba.

No Sandbox, a opção é Hifenizar texto em bandeira, abaixo do alinhamento de parágrafo da seção Texto do corpo, com a zona logo abaixo. Com o corpo justificado, a mesma opção fica junto das configurações de justificação, para os estilos de parágrafo e os boxes em bandeira.

#Idioma do documento

O locale de nível superior é o idioma do documento como um todo. Aceita os mesmos valores que hyphenation.locale e é o valor que esse campo usa quando não está definido, de modo que basta locale: 'es' para um livro em espanhol ser hifenizado em espanhol. Ele também escolhe o idioma das strings integradas de continuação de tabelas e de boxes divididos ((cont.) / Continued em vez de Continúa, veja Tabelas mais altas que a página e Marcas de um boxe dividido) e dos números de título escritos por extenso (Chapter One em vez de Capítulo uno, veja Números por extenso), e é o idioma marcado num PDF acessível. Sem definição, o motor assume 'en-us'; o Sandbox usa o idioma da interface e mostra o campo em Design › Sistema de escrita.

Ele escolhe também os tipos de recurso integrados. Quando resourceTypes não está definido, buildDocument numera e legenda com defaultResourceTypes(locale), então só locale: 'de' já dá Abbildung 1.1 e Tabelle 1.1. Quando locale não está definido, o idioma da hifenização ocupa o lugar dele, tanto para os tipos de recurso quanto para as strings das tabelas. Uma lista resourceTypes explícita sempre prevalece. As versões anteriores usavam os tipos em inglês qualquer que fosse o locale, a menos que você mesmo passasse defaultResourceTypes(locale); um documento que define locale e quer manter os rótulos em inglês passa resourceTypes: defaultResourceTypes('en'). Os livros do Sandbox e os pacotes levam a própria lista, então não são afetados.

Qualquer tag BCP 47 funciona aqui, como em hyphenation.locale: 'de-AT' recebe as strings em alemão. As strings integradas existem nos oito idiomas que a hifenização suporta, em chinês (em caracteres simplificados e tradicionais), em japonês e em árabe; qualquer outro idioma recebe as inglesas.

O chinês aceita zh, zh-Hans, zh-Hant, zh-CN, zh-SG, zh-TW, zh-HK, zh-MO e as formas longas (zh-Hant-TW), em maiúsculas ou minúsculas e com - ou _. As strings seguem a escrita, lida com Intl.Locale(tag).maximize(): zh, zh-CN e zh-SG são simplificado; zh-TW, zh-HK e zh-MO, tradicional. Já os padrões tipográficos que dependem do idioma seguem a região, como recomenda a clreq §1.2: CN, SG e MY contam como China continental, TW como Taiwan, HK e MO como Hong Kong, e uma tag sem região vale pela escrita (zh-Hant é Taiwan; zh e zh-Hans, a China continental). O japonês aceita ja, ja-JP, ja-Jpan e qualquer outra tag ja (isJapaneseLanguage(tag)); desde o postext 1.16 ele tem strings próprias e uma região própria, japan, cujos padrões tipográficos seguem a JLReq (veja Composição japonesa). Uma tabela de strings sem entrada em japonês dá inglês, nunca chinês. localeScript(tag), cjkRegionOf(tag), stringsKeyOf(tag) e sameContentLocale(a, b) dão essas leituras, e DOCUMENT_LANGUAGES lista os idiomas com strings integradas, cada um com o nome no próprio idioma (日本語 entre as entradas chinesas e العربية), como o seletor Idioma do documento do Sandbox os mostra.

IdiomaFigura: nome, plural, rótulo curtoTabela: nome, plural, rótulo curtoContinuação de tabela: continuedSuffix, continuesMarker
Inglês (en)Figure, Figures, Fig.Table, Tables, Tab.(cont.), Continued
Espanhol (es)Figura, Figuras, Fig.Tabla, Tablas, Tabla(cont.), Continúa
Francês (fr)Figure, Figures, Fig.Tableau, Tableaux, Tabl.(suite), À suivre
Alemão (de)Abbildung, Abbildungen, Abb.Tabelle, Tabellen, Tab.(Forts.), Wird fortgesetzt
Italiano (it)Figura, Figure, Fig.Tabella, Tabelle, Tab.(segue), Continua
Português (pt)Figura, Figuras, Fig.Tabela, Tabelas, Tab.(cont.), Continua
Catalão (ca)Figura, Figures, Fig.Taula, Taules, Taula(cont.), Continua
Neerlandês (nl)Figuur, Figuren, Fig.Tabel, Tabellen, Tab.(vervolg), Wordt vervolgd
Chinês simplificado (zh-Hans, zh, zh-CN)图, 图, 图表, 表, 表(续), 接下页
Chinês tradicional (zh-Hant, zh-TW, zh-HK)圖, 圖, 圖表, 表, 表(續), 接下頁
Japonês (ja, ja-JP)図, 図, 図表, 表, 表(続き), 次ページへ続く
Árabe (ar, ar-EG, ar-MA…)شكل, أشكال, شكلجدول, جداول, جدول(تابع), يتبع

O prefixo da legenda é o nome do tipo (Figura 1.1.). Os tipos chineses numeram dentro do capítulo com hífen, {h1}-{n} (图 1-1); com os ajustes de legenda labelNumberGap: '' e labelSeparator: ' ' a legenda fica 图1-1 标题 (veja Estilo de legendas). O índice remissivo também segue o idioma: 见 e 另见 antes de uma referência cruzada, 符号 e 数字 sobre os símbolos e os números em chinês simplificado; 見, 另見, 符號 e 數字 em tradicional. O japonês numera seus tipos da mesma forma e compõe as legendas como 図1-1 題 sem os dois ajustes, escreve as referências cruzadas como 第3章, 2.3節 e 12ページ, dá à bibliografia o título 参考文献, e seu índice imprime 記号 e 数字 sobre os símbolos e os números e escreve as remissões com uma seta, →夏目漱石 para ver e →夏目漱石、森鷗外も見よ depois das páginas para ver também, com os rótulos em pé. O árabe numera seus tipos da mesma forma, {h1}-{n} (شكل 2-3), escreve as referências cruzadas como الفصل 3, القسم 2-1 e ص 12, dá à bibliografia o título المراجع, e seu índice imprime انظر / انظر أيضًا, رموز e أرقام, com a vírgula e o ponto e vírgula árabes (، ؛).

Um documento em chinês, japonês ou coreano declara o idioma na saída HTML (lang na raiz .pt-doc, com zh-Hant-TW inteiro) e no canvas que ele pinta (ctx.lang, no Chrome 136 e posteriores), para que o navegador desenhe as formas de glifo da região: o Unicode unifica os caracteres han, e um mesmo ponto de código tem aspecto diferente numa fonte taiwanesa e numa japonesa. Os demais documentos não levam lang, como antes. O PDF declara /Lang a partir de locale em todo documento, marcado ou não, com a escrita e a região. Um documento num idioma escrito da direita para a esquerda (árabe, persa, urdu, hebraico…) também declara o idioma: as formas linguísticas da fonte (locl) e a fonte substituta o seguem.

const config: PostextConfig = {
  locale: 'es',
  bodyText: { textAlign: 'justify', hyphenation: { enabled: true } }, // hifeniza em espanhol
};

Algarismos do documento

O numerals de nível superior define os algarismos de todo número que o motor escreve: números de página e os rótulos de página do sumário, do índice e das referências a páginas; números de listas numeradas e de notas de rodapé; contadores de títulos e capítulos, {chapterNumber}; o {h1} e o {n} do número de uma figura e de uma referência a ela; {totalPages}, {bookTotalPages} e {numberDecimal}. 'latn' escreve 0–9, 'arab' os algarismos arábico-índicos ٠–٩ e 'arabext' os algarismos persas ۰–۹. Só o formato decimal muda (decimal, o arabic das listas ou um ajuste deixado no padrão), então um formato que o autor nomeia sai como foi nomeado: lower-roman continua i, ii, iii, e arabic-indic num documento latino ainda escreve ١, ٢, ٣. O texto do documento nunca é reescrito.

O padrão, 'auto', usa os algarismos de locale (defaultNumeralsFor(tag)): 'arab' para o árabe sem região ou com qualquer região fora do Magrebe (ar, ar-EG, ar-SA, ar-AE…), 'latn' para ar-MA, ar-DZ, ar-TN, ar-LY, ar-MR e ar-EH, 'arabext' para o persa (fa), o pashto (ps) e o urdu da Índia (ur-IN), e 'latn' para todo o resto, inclusive o urdu do Paquistão. O CLDR dá latn para um ar sem região e para ar-AE; os livros árabes do Machrek e do Golfo imprimem ٠–٩, e é isso que o Postext segue. Uma tag que nomeia seus algarismos os mantém: ar-MA-u-nu-arab. Uma página numerada com os algarismos do documento registra arabic-indic ou persian como pageNumberFormat, para que os rótulos de página do PDF mostrem os mesmos algarismos. Um valor desconhecido segue o idioma e é avisado como unknownNumerals.

Os números que o autor digita são lidos em qualquer um dos três sistemas: um item de lista ٣. começa em 3, e {startAt=٥}, :::numbering{startAt=٥}, :::space{lines=٢} e :::part{number="٣"} (para {numberDecimal}) leem o valor.

const config: PostextConfig = { locale: 'ar' };                     // ١، ٢، ٣
const maghreb: PostextConfig = { locale: 'ar-MA' };                 // 1, 2, 3
const forced: PostextConfig = { locale: 'ar', numerals: 'latn' };   // 1, 2, 3

Direção do texto

O direction de nível superior define a direção base do documento: 'ltr', 'rtl' ou 'auto' (o padrão), que é 'rtl' quando a escrita de locale vai da direita para a esquerda (árabe, persa, urdu, hebraico, siríaco, thaana, n'ko, adlam…, lida por directionOf(tag)) e 'ltr' nos demais casos. Um documento da direita para a esquerda é diagramado num quadro espelhado: as linhas começam à direita, a primeira coluna é a da direita, recuos, marcadores de lista, flutuantes, notas de rodapé e boxes ficam à direita, e page.binding: 'auto' o encaderna pela direita. A configuração resolvida só leva direction: 'rtl' num documento assim, de modo que um documento da esquerda para a direita se resolve como antes. Um valor desconhecido é lido como 'auto' e avisado como unknownConfigValue. O algoritmo bidirecional do Unicode (UAX #9) ordena os trechos de cada linha em qualquer direção: uma citação em árabe num livro em inglês se lê da direita para a esquerda no próprio lugar.

Dentro do documento, um título ou um contêiner ::: aceita {dir=ltr} ou {dir=rtl}, e os inline :ltr[…] e :rtl[…] isolam um trecho de texto (veja Direção do texto na marcação); um recurso de tabela aceita table.direction. Um bloco composto contra a direção do documento mantém o próprio lado de início: o recuo, os marcadores de lista e o final alinhado da última linha passam para o lado em que o texto dele começa.

Um ajuste que nomeia um lado se refere a um lado do texto ou do fluxo do corpo, nunca da folha, então um design feito para um livro em inglês continua funcionando quando o livro passa para o árabe. 'start' e 'end' são aceitos como nomes explícitos:

Ajuste'left' / 'right''start' / 'end'
textAlign do corpo, dos títulos, dos estilos de parágrafo, das partes, das notas de rodapé e do corpo dos boxes; align da legenda e da nota de legendaOs lados do texto: 'left' é o lado em que a linha começa, a direita de um parágrafo árabe, onde fica a última linha de um parágrafo justificado.Sinônimos de 'left' e 'right'. As configurações resolvidas levam 'left' / 'right'; as salvas mantêm o que foi escrito.
align de célula de tabelaOs lados do texto da célula, lidos na direção da tabela (table.direction).Sinônimos, lidos na direção da tabela.
placement.align (flutuantes, figuras estreitas)Os lados do fluxo do corpo: num livro da direita para a esquerda, 'left' é a direita da folha.Sinônimos.
stripe.side, icon.cornerSide e labelTab.position do boxe ('top-start', 'top-end')Os lados do fluxo do corpo, os mesmos para todos os boxes da página.A direção do próprio boxe (um :::callout{dir=ltr} num livro árabe começa pela esquerda da folha).
Posições do cabeçalho e do rodapé; elementos de design ancorados na folhaOs lados da folha.Só elementos de texto: o início e o fim da direction do próprio elemento.

placement.rotate mantém o sentido físico numa página espelhada: uma figura girada no sentido horário fica girada no sentido horário na folha. Um host que lê o layout encontra o quadro espelhado em cada página (VDTPage.flow com direction: 'rtl', pageIsMirrored(page)) e a ordem visual dos segmentos de cada linha em VDTLine.order; flowToPage e pageToFlow convertem entre o fluxo e a folha. Veja Composição árabe.

const arabic: PostextConfig = { locale: 'ar' };                        // da direita para a esquerda, encadernado pela direita
const english: PostextConfig = { locale: 'en', direction: 'rtl' };     // forçado; raramente é o que você quer

#Idiomas e escritas

O Postext compõe escritas alfabéticas da esquerda para a direita, e o chinês e o japonês na horizontal e na vertical. Composição chinesa e Composição japonesa explicam como são compostos e quais ajustes os controlam; as chaves estão em Tipografia do Leste Asiático, Escrita vertical e Encadernação. O que cada escrita recebe:

  • O chinês é composto pelo compositor CJK quando um parágrafo tem mais caracteres CJK do que espaços entre palavras: as linhas quebram entre caracteres segundo as regras de início e fim de linha de cjk.lineBreak (nenhuma linha começa com 。、」 ou ー, nenhuma termina com 「 ou (), mantêm inteiros —— e ……, um número com seus sinais e uma palavra latina, e uma linha justificada é espaçada entre os caracteres até a medida. A largura da pontuação, a pontuação pendente, o espaço entre han e latim, a grade de caracteres, os pontos de ênfase, as marcas de nome próprio e de título de obra, o rubi e as notas warichu seguem a região de locale, na linha horizontal ou na vertical (layout.writingMode: 'vertical-rl'). Um parágrafo latino que cita algumas palavras CJK mantém a quebra de linha ótima e pode quebrar ao lado delas; um colchete CJK, o ponto médio ou um sinal de largura total citado em texto latino (〈h〉, %) não muda nada.
  • O japonês passa pelo mesmo compositor com regras próprias desde o postext 1.16, as dos Requirements for Japanese Text Layout (JLReq) do W3C e da JIS X 4051: um locale ja dá a região japan, cujos valores automáticos definem os níveis de kinsoku da JLReq (kana pequenos e ー nunca abrem uma linha), pontuação de largura total com compressão de pares, um eme depois de ?!, o parêntese que abre um parágrafo na segunda metade do recuo, marcas de ênfase em forma de gergelim sobre o texto, títulos de obras entre 『』, furigana espaçados 1:2:1, contadores japoneses, notas e um índice ordenado pela leitura. As versões anteriores compunham o japonês com os padrões da China continental.
  • O coreano passa pelo mesmo compositor e é composto sem hifenização, mas com os padrões da China continental: as regras próprias dele (KLREQ) não estão implementadas. O texto coreano quebra entre sílabas e também nos espaços.
  • O árabe e as outras escritas da direita para a esquerda (persa, urdu, hebraico…) são compostos da direita para a esquerda: Composição árabe explica como. A direction do documento vem da escrita de locale, o algoritmo bidirecional do Unicode ordena as palavras latinas e os números dentro de cada linha, o livro é encadernado pela direita com a primeira coluna à direita, e todo número que o motor escreve usa os algarismos da região. Uma palavra que contém uma letra da escrita árabe nunca é hifenizada, espaçada nem cortada, e uma linha árabe justificada se estica nos espaços e com kashidas. Sinais vocálicos, ênfase, notas de rodapé e as strings do árabe estão em Texto árabe. O persa, o urdu e o hebraico recebem a direção, os algarismos e as regras de palavra inteira, mas nenhuma string integrada própria.

Há padrões de hifenização para oito idiomas (en-us, es, fr, de, it, pt, ca, nl); os tipos de recurso integrados e as strings de continuação existem nesses oito, em chinês, em japonês e em árabe. Chinês, japonês, coreano e os idiomas escritos da direita para a esquerda são compostos sem hifenização. Qualquer outro idioma é hifenizado com os padrões do inglês dos EUA, com um aviso no console, e recebe as strings em inglês. Para um documento assim, defina hyphenation.enabled: false e passe resourceTypes e as strings de continuação de tableStyle no idioma dele.

#Texto árabe

Estes ajustes servem ao texto em escrita árabe; nenhum muda um documento escrito em outra escrita. Composição árabe os explica junto com o resto de um livro árabe: direção, encadernação, algarismos, verso, sumário e índice.

  • Sinais vocálicos e entrelinha. Os sinais vocálicos de um texto vocalizado (fatḥa, kasra, shadda, tanwīn, o alef sobrescrito, os sinais corânicos) se empilham sobre e sob as letras, dentro da entrelinha, que nunca cresce por causa deles. Cada linha com sinais registra até onde vai a tinta deles (VDTLine.markInk), e o recorte de coluna dos renderizadores inclui os sinais da primeira e da última linha da coluna. Quando um sinal sobre uma palavra encosta nas letras ou nos sinais pendurados sob a palavra de cima, a composição avisa o parágrafo (arabicMarksExceedLeading, no painel Verificações do Sandbox). Só se comparam palavras que estão uma sobre a outra. Texto parcialmente vocalizado pede cerca de 1,7–1,85 em de lineHeight; verso totalmente vocalizado, 1,9–2,1 em.
  • Ênfase. A tipografia árabe não tem itálico, então num documento cujo locale se escreve em escrita árabe *…* sai em negrito por padrão (bodyText.emphasis: 'auto'). 'color' o compõe em redondo com italicColor, e 'overline' traça um fio sobre as palavras, o khaṭṭ fawqī dos livros árabes. O ajuste vale para todo texto composto com as fontes do corpo: parágrafos, listas, citações em bloco, estilos de parágrafo, corpo dos boxes, notas e títulos. Legendas, células de tabela, o sumário e o índice mantêm os próprios ajustes de itálico. Seja qual for a escolha, o motor nunca inclina letras árabes: as palavras árabes de um trecho em itálico ficam em pé e as palavras latinas mantêm o itálico. Num documento assim, a citação em bloco é em redondo por padrão.
  • Tashkīl. bodyText.tashkil: 'strip' tira os sinais vocálicos e corânicos do texto que o layout compõe, para uma edição sem vogais feita a partir de uma fonte vocalizada: fatḥa, ḍamma, kasra e seus tanwīn, sukūn, shadda, o alef sobrescrito (هٰذا vira هذا) e os sinais U+0656–U+065F e U+06D6–U+06ED. 'strip-vowels' mantém a shadda, como a maioria dos livros modernos a imprime. Hamza e madda ficam (أ إ آ são letras, também quando digitadas com sinais combinantes). O original mantém os sinais; as linhas, os títulos e o sumário são compostos sem eles, e cada caractere composto continua apontando para o seu lugar no original.
  • Notas de rodapé. footnotes.markerTemplate: '({n})' escreve as chamadas «(١)» nos algarismos do documento, numbering: 'page' recomeça a numeração em cada página e noteNumberPosition: 'inline' põe o número da própria nota na linha. O fio separador e os números das notas ficam no início da coluna, à direita num livro da direita para a esquerda.
  • Palavras inteiras. Uma palavra que contém uma letra da escrita árabe nunca é hifenizada, cortada ou espaçada, num livro árabe ou citada em outro. Um estilo que aplica letterSpacing a texto árabe é avisado (joiningScriptLetterSpacing), e uma palavra mais larga que a linha transborda dela e é avisada (unbreakableWordOverflow). Veja Composição árabe.
  • Kashida e verso. Uma linha árabe justificada se estica com kashidas além dos espaços (bodyText.kashida, veja Kashida no texto árabe), e um poema clássico é composto um bayt por linha, em dois hemistíquios de mesma largura, com :::verse (veja :::verse).
  • Índice. Um índice em árabe ordena alfabeticamente e ignora o artigo ال (index.ignoreArticle), os sinais vocálicos e os suportes da hamza; veja Índice remissivo.

#Órfãs, viúvas, linhas curtas e regras de manter junto

Veja Hifenização e justificação para a mecânica por trás destes deméritos. Esta seção é a referência das chaves de bodyText que os controlam.

Além da hifenização e dos limites de espaçamento, a configuração do texto do corpo expõe as regras flexíveis que evitam quebras de parágrafo estruturalmente desajeitadas. Todas entram no algoritmo de quebra de linhas de Knuth-Plass como deméritos: elas puxam o layout para quebras limpas sem nunca impor uma regra rígida. Defina os valores *Penalty como 0 para desativar, na prática, qualquer uma delas.

PropriedadeTipoPadrãoDescrição
avoidOrphansbooleantrueDesestimula que um parágrafo termine com menos de orphanMinLines linhas no alto da coluna seguinte.
orphanMinLinesnumber2Mínimo de linhas exigidas no alto da coluna seguinte quando um parágrafo é dividido. Só atua quando avoidOrphans é true.
orphanPenaltynumber1000Demérito somado quando a restrição de órfã é violada. Valores maiores inclinam o algoritmo mais fortemente contra órfãs; 0 desativa a penalidade.
avoidOrphansInListsbooleantrueCom true, os itens de lista também recebem proteção contra órfãs (não só os parágrafos). Só tem efeito quando avoidOrphans é true.
avoidWidowsbooleantrueDesestimula que um parágrafo comece com menos de widowMinLines linhas no pé da coluna atual.
widowMinLinesnumber2Mínimo de linhas exigidas no pé da coluna atual quando um parágrafo é dividido. Só atua quando avoidWidows é true.
widowPenaltynumber1000Demérito somado quando a restrição de viúva é violada. 0 desativa a penalidade.
avoidWidowsInListsbooleantrueCom true, os itens de lista também recebem proteção contra viúvas. Só tem efeito quando avoidWidows é true.
avoidRuntsbooleantrueDesestimula parágrafos que terminam com uma última linha muito curta, uma linha curta (runt), por exemplo uma única palavra curta sozinha. Vale também para texto alinhado à esquerda, com optimalRagged. Um parágrafo em chinês, japonês ou coreano não termina numa linha com um só caractere, sozinho ou com os sinais de fechamento (孤字): a linha de cima lhe cede o último caractere quando ainda pode ser justificada dentro do limite de tracking.
runtMinCharactersnumber20Limite para a última linha de um parágrafo, contado em espaços entre palavras, não em letras: a linha é curta quando é mais estreita que runtMinCharacters × normalSpaceWidth pixels. Um espaço entre palavras mede de um quarto a um terço de eme na maioria das fontes de texto, cerca de meia letra minúscula, então o padrão 20 pega últimas linhas com menos de 4 a 7 emes, mais ou menos 8 a 12 letras. Para pegar as últimas linhas com menos de cerca de N letras, use um valor perto de 2 × N.
runtPenaltynumber1000Equivalente de badness injetado na fórmula de demérito quadrático de Knuth–Plass (mesma escala da badness da linha, que satura em 10000). 0 desativa a penalidade.
gradedRuntPenaltybooleanfalseGradua a penalidade de linha curta conforme o quanto a última linha fica curta: uma última linha de largura w abaixo do limite t custa runtPenalty × (1 − w / t) em vez da penalidade inteira. Um final de duas palavras passa a custar menos que um de uma, e o algoritmo de quebra desce uma palavra quando uma linha de cima pode cedê-la ('…sallies of' / 'our minds.' em vez de '…sallies of our' / 'minds.'). Desligado por padrão: toda linha curta custa o mesmo, e o algoritmo mantém as linhas mais apertadas acima.
avoidRuntsInListsbooleantrueCom true, os itens de lista também recebem a penalidade de linha curta. Só tem efeito quando avoidRunts é true.
tightenRuntsbooleantrueQuando a penalidade não conseguiu evitar uma linha curta, compõe o parágrafo com uma linha a menos: os espaços entre palavras se apertam (nunca abaixo de minWordSpacing) e, se isso não bastar para recolher a linha, entra um pouco de tracking negativo. A versão mais curta é recusada, e a linha curta fica, quando esticaria uma linha justificada além de maxWordSpacing ou, se o parágrafo já tiver uma linha justificada mais frouxa, além dessa linha, ou quando deixaria mais linhas em bandeira (além de 3× o espaço normal) do que o parágrafo tinha. Os espaços entre palavras do texto alinhado à esquerda mantêm a largura, então ali só o tracking participa. Requer optimalLineBreaking e avoidRunts (e optimalRagged para texto alinhado à esquerda).
maxRuntTrackingnumber10O máximo de tracking que a correção de uma linha curta pode usar, em milésimos de eme (a unidade do InDesign: 10 = 0,01 em por caractere), aplicado como aperto. 0 deixa a correção só para o espaçamento entre palavras.
slackWeightnumber10Peso aplicado ao custo quadrático do “espaço de coluna não usado”. Valores maiores fazem o layout preferir encher bem as colunas; 0 desativa totalmente a pressão de folga.
keepColonWithListbooleantrueQuando um parágrafo termina com dois-pontos que introduzem diretamente uma lista, mantém a última linha, a dos dois-pontos, junto da lista: se colocar o parágrafo não deixar espaço para o primeiro item da lista começar na mesma coluna/página, a última linha (ou o parágrafo inteiro, se tiver uma só linha) passa para a coluna seguinte junto com a lista. Quanto espaço basta é definido por colonListRoom. Quando esta regra empurraria o parágrafo inteiro e há uma sequência de títulos logo antes dele na coluna, esses títulos também são levados adiante, para que headings.keepWithNext continue valendo.
colonListRoom'item' | 'line''item'O espaço que keepColonWithList pede sob a linha dos dois-pontos. 'item': o que as regras de órfãs e viúvas para listas deixariam do primeiro item no pé da coluna, uma linha quando ele pode ser dividido ali, ele inteiro quando elas o mantêm junto (um item de duas linhas, por exemplo). 'line': uma linha, como até o postext 1.4; um primeiro item que essas regras mantêm inteiro vai então sozinho para a coluna seguinte e deixa a linha dos dois-pontos no pé. Uma configuração salva antes do configVersion 6, num livro que introduz uma lista com dois-pontos, é lida com 'line' (veja Pacotes gravados pelo postext 1.4 ou anterior). Qualquer outro valor é lido como 'item'.
hyphenateAcrossColumnsbooleantruePermite que uma coluna ou uma página termine numa palavra hifenizada (o Hyphenate Across Column do InDesign). false quebra de novo o parágrafo que atravessa a quebra de coluna, para que a última linha dele na coluna termine numa palavra inteira; os espaços entre palavras das linhas acima absorvem a diferença, dentro de maxWordSpacing e minWordSpacing. É uma preferência: onde nenhuma quebra dentro desses limites o evita, o hífen fica. Tenta todas as quebras de coluna de um parágrafo: a primeira e as seguintes que caem onde termina uma coluna cheia, numa só requebra; e uma quebra posterior que cai em outro ponto (uma viúva mantida, um corte de faixa nivelado) em outra, a partir dessa coluna, que mantém nas suas quebras as linhas já compostas nas colunas anteriores e só quebra o resto. O corpo dos boxes não é afetado. Com optimalRagged, um parágrafo alinhado à esquerda é quebrado de novo da mesma forma: os espaços entre palavras mantêm a largura, então só os finais das linhas se movem. Cada requebra mede o parágrafo mais uma vez, por isso um livro com muitos hífens em fim de coluna é diagramado um pouco mais devagar. Requer optimalLineBreaking, e optimalRagged para texto alinhado à esquerda.
paragraphContainerSpacing'collapse' | 'add''collapse'O espaço sob um contêiner :::paragraphs que fecha num parágrafo, entre esse parágrafo e o bloco seguinte (veja O contêiner :::paragraphs). 'collapse': o maior entre o spaceBetween e o marginBottom do estilo e o espaçamento de parágrafo do texto em volta do contêiner (uma linha com paragraphSpacing; num boxe, o do boxe), fundido com o espaço que o bloco seguinte reserva acima de si, como entre dois parágrafos de texto corrido: um título sob uma bibliografia fica à distância do próprio marginTop, não dessa margem mais o espaçamento das entradas. 'add': como até o postext 1.4, só o espaço do estilo é posto sob a última linha antes do ajuste à grade, o espaço próprio do bloco seguinte é somado abaixo dele, e o espaçamento de parágrafo fica de fora, de modo que o parágrafo depois do contêiner podia ficar mais perto dele do que de qualquer outro. Uma configuração salva antes do configVersion 8 que declara um estilo de parágrafo, num livro que contém um contêiner assim, é lida com 'add' (veja Pacotes gravados pelo postext 1.4 ou anterior). Um marginBottom negativo puxa o bloco seguinte para cima em qualquer caso.

Sobre linhas curtas. Uma linha curta é a última linha de um parágrafo curta demais para parecer uma linha de texto de verdade, em geral uma ou duas palavras curtas isoladas no fim do parágrafo. Como a verificação se baseia no comprimento da linha em pixels em relação à largura do espaço normal, runtMinCharacters se adapta sozinho ao corpo da fonte atual. Uma palavra curta que, visualmente, é mais larga que runtMinCharacters × spaceWidth não tem problema; uma palavra mais estreita que isso (ou realmente sozinha) recebe a penalidade de linha curta. O limite conta espaços entre palavras, que têm cerca de metade da largura das letras: o padrão 20 corresponde a uma última linha de mais ou menos 8 a 12 letras. Toda linha curta custa a penalidade inteira, então entre dois finais abaixo do limite o algoritmo de quebra mantém as linhas mais apertadas acima; gradedRuntPenalty cobra cada um pelo que lhe falta, e o final mais longo vence. Para os curiosos de matemática: com o runtPenalty padrão de 1000, evitar uma linha curta supera qualquer conjunto de quebras alternativo que exija um esticamento do espaçamento entre palavras de até cerca de r≈2,15.

Flexíveis, não rígidas. Nenhuma destas regras pode impedir uma quebra: o motor sempre produz um layout. São deméritos: o algoritmo pondera badness, custo de hifenização, suavidade das classes de aptidão e estas penalidades estruturais numa única otimização global e escolhe o conjunto de quebras com o menor custo total. Se você precisa de uma garantia mais firme, aumente a penalidade; se um documento fica melhor com a penalidade mais branda, diminua-a.

#Títulos

A propriedade headings controla a tipografia de todos os níveis de título (H1–H6). Você pode definir padrões gerais que valem para todos os níveis e depois sobrescrever propriedades específicas por nível.

#Padrões gerais

PropriedadeTipoPadrãoDescrição
fontFamilystring'Open Sans'Família tipográfica de todos os títulos.
lineHeightDimension1.2 emEntrelinha dos títulos. Mais apertada que a do texto do corpo.
colorColorValueCor principal (#295AA3)Cor do texto dos títulos. Vinculada à entrada main-color da paleta padrão, então trocar a cor da paleta muda a cor de todos os títulos.
textAlign'left' | 'justify' | 'center' | 'right' | 'start' | 'end''left'Alinhamento de todos os níveis de título (não há valor por nível): alinhado à esquerda, justificado, centralizado ou alinhado à direita. Um título justificado põe a última linha alinhada à esquerda, como um parágrafo, então um título de uma linha fica como 'left'. Canvas, HTML e PDF posicionam as linhas da mesma forma, inclusive o prefixo numérico. A abertura padrão de um título span: 'page' sem design avançado também segue este valor (justificado fica alinhado à esquerda); um design avançado alinha os próprios elementos de texto com o align deles.
fontWeightnumber700Peso da fonte dos títulos (100–900).
marginTopDimension1.5 emEspaço acima dos títulos.
marginBottomDimension0.5 emEspaço abaixo dos títulos.
keepWithNextbooleantrueCom true, um título nunca é o último elemento de uma coluna ou página. Se o bloco seguinte não tiver espaço para pelo menos bodyText.widowMinLines linhas depois do título (ou uma linha quando bodyText.avoidWidows é false), o título é levado adiante para continuar junto do seu texto. Interage com bodyText.keepColonWithList: se essa regra precisar empurrar inteiro um parágrafo com dois-pontos, os títulos que o antecedem no fim da coluna vão junto em vez de ficarem isolados.
keepWithNextSpreadbooleanfalseCom keepWithNext, ainda permite que um título feche a última coluna de texto de uma página par, já que o texto dele abre então a página ímpar em frente, da mesma página dupla (JLReq §4.1.7). As páginas duplas são a página 1 sozinha, depois 2–3, 4–5…, contadas com pageIndexOffset, em livros encadernados pela esquerda e pela direita. Desde o postext 1.16.
keepWithNextSplit'rules' | 'fill''rules'Como o parágrafo sob um título se divide quando o título acaba no pé de uma coluna e empurrar o parágrafo inteiro deixaria o título para trás. 'rules': o máximo de linhas que cabem, desde que fiquem pelo menos bodyText.widowMinLines sob o título e pelo menos bodyText.orphanMinLines passem para a coluna seguinte; quando nenhuma divisão respeita as duas, o título segue com o parágrafo, e o equilíbrio de colunas preenche o espaço que ele deixa. 'fill': tantas linhas quantas couberem, por menos que passem adiante, então um parágrafo de quatro linhas com espaço para três se divide em 3 + 1. Até o postext 1.4 todo título se dividia assim, e as configurações salvas antes do configVersion 8 mantêm esse comportamento (veja Pacotes gravados pelo postext 1.4 ou anterior). Com avoidWidows ou avoidOrphans desligado, esse lado da regra deixa de valer.
snapToGridbooleantrueSe o fluxo volta a se ajustar à grade de linhas de base sob um título. Com true, o marginBottom do título é arredondado para cima até linhas inteiras da grade; com false, a margem exata é mantida e o texto sob o título pode ficar fora da grade até o próximo ponto de ajuste (o fim de uma lista, a cauda de um :::paragraphs, uma fórmula em destaque), como muitos livros fazem com uma linha e meia sob um título. Um nível (levels[].snapToGrid) ou um estilo de título pode definir o próprio valor; este é o que eles herdam.
inlineMarksbooleantrueSe um título lê as marcas na linha como um parágrafo lê: italic, bold, ^superscript^, ~subscript~, :smallcaps[…] e links. Um trecho em itálico inverte a inclinação do título, então sai em redondo num título em itálico; um trecho em negrito usa bodyText.boldFontWeight, ou o peso do próprio título quando este for mais pesado. O sumário também mostra os trechos em negrito e itálico; os cabeços e os marcadores do PDF imprimem só o texto. Com false, os marcadores são descartados e as palavras saem no estilo do próprio título, como até o postext 1.4. A abertura padrão de um título span: 'page' sem design também compõe os trechos em negrito, itálico, sobrescrito e subscrito; um design de título (uma faixa de abertura com design ou um advancedDesign na coluna) imprime como texto simples de qualquer forma, a não ser que o elemento de texto leia marcas na linha (inlineMarks: true): aí mantém os trechos em negrito, itálico, sobrescrito e subscrito do título (desde o postext 1.19). As configurações salvas antes disso cujos títulos têm marcas são lidas com false (veja Pacotes gravados pelo postext 1.4 ou anterior).
balancingColumnBalancingConfigativadoEquilíbrio vertical de colunas: espaço extra acima dos títulos para que as colunas terminem alinhadas com o pé da página. Veja abaixo.

Títulos centralizados para poemas ou os atos de uma peça, sem posição de design:

headings: {
  textAlign: 'center',
  levels: [{ level: 2, textTransform: 'uppercase' }],
}

Passar um objeto headings mantém todo padrão de nível que você não repetir, inclusive a quebra de página do H1 (veja Sobrescritas por nível).

#Equilíbrio de colunas

As editoras esperam que toda coluna comece no alto da página e termine alinhada com o pé. As regras de quebra (proteção contra órfãs e viúvas, títulos mantidos com o seu texto, figuras indivisíveis) deixam naturalmente colunas curtas, com uma ou mais linhas vazias da grade de linhas de base no pé. Com o equilíbrio ativado, o motor faz o que um compositor faria e aplica seus recursos na ordem de prioridade editorial:

  1. Um boxe que fecha a coluna: um boxe que termina uma coluna curta é empurrado para baixo exatamente pelo espaço que sobra sob o seu pé, de modo que a borda inferior cai na última posição da grade da página, alinhada com a última linha da coluna ao lado. Ele ocupa esse espaço mesmo quando é menor que uma linha, desde que nada passe para outra coluna. Por padrão, roda antes de qualquer outro recurso e ocupa a lacuna inteira, então um boxe que comenta o parágrafo acima dele pode acabar várias linhas abaixo; closingBox: 'last' deixa os títulos, os finais de lista e os outros recursos de espaçamento ficarem primeiro com as linhas inteiras, e o boxe só com o que sobrar, e closingBox: 'off' nunca o move.
  2. Imagens com área segura: uma imagem com área segura (Resource.safeArea, veja Formato do documento › Área segura) inserida na linha da coluna curta, ou flutuando no alto ou no pé dela só sobre essa coluna, cresce em linhas inteiras da grade, recortada dentro da área segura, até a coluna ficar cheia ou o recorte chegar à área segura. Uma imagem mais alta não deixa buraco na página, por isso roda antes de qualquer espaço ser somado; um flutuante no alto de uma coluna numa página ou faixa cujas cabeças de coluna ficam niveladas (veja abaixo) não cresce. Registrado como flexFigure. Não tem ajuste próprio: só cresce uma imagem cujo recurso marca uma área segura.
  3. Títulos: linhas inteiras da grade são somadas à margem superior dos títulos dentro da coluna curta. Quando são necessárias várias linhas e a coluna tem vários títulos, as linhas se distribuem entre eles, sempre com a maior parte para o título mais importante (um h2 recebe mais que um h3). Títulos no alto de uma coluna nunca recebem espaço extra, para que as colunas continuem começando no alto da página, exceto um título logo abaixo de uma figura ou tabela que encabeça a coluna numa página que continua: o espaço vai acima desse título, sob a figura.
  4. Finais de lista: quando os títulos não conseguem absorver a lacuna inteira, uma linha da grade é somada onde uma lista ou enumeração termina (espaço depois de uma lista soa natural), com um limite por final de lista.
  5. Parágrafos frouxos: como último recurso, um parágrafo da coluna é quebrado de novo com uma linha a mais (o \looseness=+1 do TeX), escolhendo o parágrafo mais longo para que o espaçamento extra entre palavras se dilua sem ser notado. A solução frouxa só é aceita quando toda linha fica abaixo de bodyText.maxWordSpacing: a cor tipográfica nunca passa do limite que você já configurou. Requer bodyText.optimalLineBreaking.

A última coluna de uma página só é equilibrada quando a página continua naturalmente na seguinte: a página de fechamento de um capítulo pode, legitimamente, terminar curta. Uma página assim, e uma faixa de fechamento cortada por igual por trailing, também mantém niveladas as cabeças de coluna: nenhuma linha é somada sob uma figura ou tabela que encabeça uma das colunas (o recurso de depois do flutuante), e um título ou um boxe que abre uma coluna sob essa figura fica no alto dela, diga o que disser stretchAfterFloats, de modo que uma coluna nunca começa mais abaixo que a vizinha só para alinhar os pés.

PropriedadeTipoPadrãoDescrição
enabledbooleantrueSe os pés das colunas são equilibrados.
maxLinesPerHeadingnumber4Máximo de linhas extras da grade que podem ser somadas acima de um único título.
stretchAfterListsbooleantruePermite linhas extras da grade onde uma lista termina, quando os títulos não conseguem absorver a lacuna inteira.
maxLinesAfterListnumber1Máximo de linhas extras da grade depois de um único final de lista.
stretchAfterFloatsbooleantruePermite linhas extras da grade sob uma figura ou tabela que encabeça a coluna curta (um flutuante no alto), depois do recurso de final de lista, para que o texto abaixo desça em vez de a coluna terminar curta. Nunca numa página que não continua (a página de fechamento de um capítulo) nem numa faixa de fechamento cortada por igual por trailing: ali as cabeças de coluna ficam niveladas e a última coluna pode terminar uma linha mais curta. O primeiro bloco sob a figura pode ser o resto de um parágrafo começado na página anterior: ele desce do mesmo jeito, pela linha que as regras de quebra deixaram livre no pé da coluna (uma linha que a regra de viúvas deixou vazia, ou um espaço de parágrafo sem lugar para texto depois dele). Defina como false para manter o texto logo abaixo da figura.
maxLinesAfterFloatnumber1Máximo de linhas extras da grade sob um único flutuante no alto.
looseParagraphsbooleantrueÚltimo recurso: quebra de novo parágrafos de uma coluna curta com uma linha mais frouxa (uma linha a mais cada um), dentro de bodyText.maxWordSpacing.
maxLooseParagraphsnumber2Quantos parágrafos de uma mesma coluna curta podem ganhar uma linha, do mais longo para o mais curto.
trackParagraphsbooleantrueQuando o espaçamento entre palavras sozinho não consegue ganhar a linha, um parágrafo frouxo pode também usar o menor tracking positivo (espaçamento entre letras) que consiga.
maxTrackingnumber10Limite superior desse tracking, em milésimos de eme por caractere (10 = 0,01 em).
trailingbooleantrueNivela a faixa de fechamento de um capítulo e do documento: quando o fluxo termina antes de a página ficar cheia (numa abertura de capítulo, num :::part, num boxe placement: 'fixed' que fecha o capítulo ou no fim do documento) com as colunas desiguais, elas são cortadas por igual, com um limite de faixa de ceil(Σ used / N / grid) linhas, resolvido depois que os recursos acima acertaram as páginas anteriores, para que uma bibliografia curta termine na mesma altura em todas as colunas em vez de encher a primeira e deixar a última meio vazia. O corte respeita as regras que uma faixa sem corte respeita: quando um bloco que não pode ser dividido nele (uma cauda de parágrafo que os mínimos de órfãs e viúvas mantêm inteira, um boxe que se mantém junto) passaria do corte, e perderia as últimas linhas, já que os renderizadores recortam a coluna pela sua caixa, ou quando um título fecharia uma coluna enquanto o texto dele abre a seguinte (com headings.keepWithNext, o padrão), o corte desce uma linha, até três vezes, e senão é abandonado. Um bloco que não consegue começar nas linhas que o corte deixa sob uma figura que encabeça uma coluna (um parágrafo precisa ali do seu mínimo de órfãs) passa para a coluna seguinte da faixa, como faria a partir de uma coluna sem corte, e a figura fica sozinha na coluna. Colunas cujos pés diferem em uma linha da grade ou menos ficam como estão: uma faixa de fechamento cuja última coluna termina uma linha mais curta é o acabamento habitual, e cortá-la só moveria uma linha. O corte vai para a faixa em que foi medido, e o mesmo vale para o corte que um boxe de página inteira pede no meio da página: até o postext 1.4, quando o primeiro texto da página de fechamento tinha sido oferecido antes a uma faixa sem espaço para ele (uma página que um flutuante ocupa inteira, a faixa estreita que um boxe de página inteira deixa no pé da página anterior), o corte era gasto nessa faixa e a página de fechamento mantinha todas as linhas na primeira coluna. Colunas de larguras diferentes (um layout de coluna e meia com texto nas duas) são cortadas por área, com a altura de cada coluna ponderada pela largura, já que uma linha da coluna estreita comporta menos texto. Só atua quando enabled é true.
beforeSpanbooleantrueNivela a faixa que um bloco de página inteira deixa para trás: quando um boxe span: 'page' não cabe sob as colunas atuais nem depois de um corte por igual, e precisa passar para a página seguinte ou se dividir (calloutStyles[].keepTogether: false), as colunas que ele interrompe são cortadas por igual, com o mesmo limite de fechamento que uma faixa de fechamento recebe, em vez de a primeira encher a página e a última terminar curta. A página fica como uma quebra explícita, para que os recursos acima não estiquem de novo a última coluna até o pé da página; o boxe, ou a parte dele que cabe, fica então sob as colunas niveladas. Só atua quando enabled é true.
closingBox'first' | 'last' | 'off''first'Quando um boxe que fecha uma coluna curta ocupa o espaço sob o seu pé (recurso 1, registrado como trailingCallout). 'first': antes de qualquer outro recurso, como até o postext 1.4; o boxe ocupa a lacuna inteira, mesmo várias linhas, e os títulos acima dele não recebem nada. 'last': depois dos recursos de título, final de lista, fórmula em destaque e flutuante, que ficam primeiro com as linhas inteiras; o boxe desce com o texto acima dele e depois ocupa só o que sobrou, em geral uma fração de linha, de modo que o pé dele ainda encontra a última posição da grade e ele fica perto do texto que comenta. 'off': nunca; o boxe mantém o espaço sob o seu pé, embora os outros recursos ainda possam descê-lo em linhas inteiras quando somam espaço acima dele, e a fração de linha que sobra sob ele fica. Numa página de fechamento, onde o boxe é o único recurso que alinha o seu pé com a coluna ao lado, 'off' o deixa onde o fluxo o pôs. Qualquer outro valor é lido como 'first'. Um boxe que fecha a única coluna de texto da sua faixa (uma página de uma coluna que termina antes, com o bloco seguinte abrindo a página seguinte) nunca é movido: não há coluna ao lado com a qual alinhar (desde o postext 1.19).

Qual recurso atuou

O layout registra cada recurso aplicado no bloco em que o aplicou: block.balancing no VDT que buildDocument devolve. Uma prova, um teste ou um relatório pode dizer por que uma coluna termina alinhada, e quais colunas nenhum recurso conseguiu fechar. Os blocos que o equilíbrio não tocou não levam balancing, e um documento construído com enabled: false não tem nenhum.

interface VDTBalancing {
  levers: BalanceLever[]; // em geral um: um parágrafo depois de uma lista pode ficar com a linha do final de lista e também ganhar uma linha
  spaceAbove: number;     // px que os recursos de espaçamento somaram acima do bloco (0 quando só atuou looseParagraph ou flexFigure)
  bodyGrowth?: number;    // flexFigure: px que a imagem cresceu, recortada dentro da área segura
  extraLines?: number;    // looseParagraph: linhas que o parágrafo ganhou
  tracking?: number;      // looseParagraph: o tracking que as ganhou, em milésimos de eme (0 = só espaçamento entre palavras)
}
type BalanceLever = 'trailingCallout' | 'flexFigure' | 'heading' | 'listEnd' | 'afterDisplay' | 'afterFloat' | 'looseParagraph';
RecursoRegistrado emO que fez
trailingCallouto bloco de moldura do boxeUm boxe que fecha a coluna desceu exatamente pelo espaço sob o seu pé (spaceAbove; uma fração de linha é aceitável).
flexFigureo bloco da imagem (na linha) ou o flutuanteUma imagem com área segura composta bodyGrowth px mais alta, em linhas inteiras da grade, recortada dentro da área segura. O bodySource do bloco do recurso é a parte mostrada e bodyFlex.delta, o mesmo crescimento.
headingo títuloLinhas inteiras da grade acima do título, até maxLinesPerHeading.
listEndo primeiro bloco depois da listaUma linha da grade onde uma lista termina, até maxLinesAfterList.
afterDisplayo bloco depois da fórmula ou do boxeUma linha da grade sob uma fórmula em destaque ou um boxe.
afterFloato primeiro bloco da colunaUma linha da grade entre uma faixa de flutuantes que encabeça a coluna e o texto dela, até maxLinesAfterFloat.
looseParagrapho parágrafoO parágrafo quebrado de novo com extraLines a mais, com o menor tracking que o conseguiu (tracking; block.letterSpacing é o mesmo valor em px).

Os cortes por igual ficam registrados nas colunas que cortam: column.bandCapped é true em toda coluna cortada por igual (a faixa que um boxe de página inteira deixa, uma faixa de fechamento), e column.trailingCap marca também a faixa de fechamento de um capítulo ou do documento (trailing).

import { buildDocument } from 'postext';
 
const doc = buildDocument(content, config);
for (const page of doc.pages) {
  page.columns.forEach((column, i) => {
    const levers: string[] = column.blocks.flatMap((block) => block.balancing?.levers ?? []);
    if (column.trailingCap) levers.push('closing band cut level');
    else if (column.bandCapped) levers.push('band cut level');
    if (levers.length > 0) console.log(`page ${page.index + 1}, column ${i + 1}: ${levers.join(', ')}`);
  });
}
// page 3, column 1: heading, heading
// page 3, column 2: listEnd, looseParagraph

#Sobrescritas por nível

Cada nível de título pode sobrescrever os padrões gerais pelo array levels. Só fontSize (além do breakBefore do H1, comentado abaixo) muda por padrão; todas as outras propriedades herdam dos ajustes gerais de título.

NívelCorpo padrãobreakBefore padrão
H118 pt{ enabled: true, parity: 'always-odd' }
H215 pt{ enabled: false, parity: 'any' }
H312 pt{ enabled: false, parity: 'any' }
H410 pt{ enabled: false, parity: 'any' }
H59 pt{ enabled: false, parity: 'any' }
H68 pt{ enabled: false, parity: 'any' }

O padrão do H1 reproduz a diagramação de capítulos de um livro: todo título de nível superior abre numa página nova à direita (ímpar), com uma página em branco separadora obrigatória depois do capítulo anterior. Sobrescreva-o em levels[0].breakBefore se o seu documento for mais simples que um livro.

O breakBefore de um nível é mesclado campo a campo sobre esse padrão, e um objeto headings que não o menciona o mantém. { parity: 'odd' } no H1 mantém a quebra e muda só a paridade; { enabled: false } deixa os capítulos seguirem em sequência:

headings: {
  fontFamily: 'Merriweather',                               // o H1 continua quebrando para um recto novo (always-odd)
  levels: [{ level: 1, breakBefore: { parity: 'odd' } }],    // …ou: um recto, sem página em branco obrigatória
}
 
headings: { levels: [{ level: 1, breakBefore: { enabled: false } }] } // capítulos em sequência

Mudou no postext 1.5. Até o postext 1.4, qualquer objeto headings desligava a quebra do H1 a menos que levels[0].breakBefore fosse repetido, e um breakBefore parcial completava o campo que faltava com o padrão sem quebra. Por isso, uma configuração escrita em código para a 1.4 com um objeto headings e sem quebra no H1 agora abre todo capítulo num recto novo, com versos em branco onde for preciso. Para manter os capítulos em sequência, dê um campo à entrada H1 de levels:

headings: { fontFamily: 'Merriweather', levels: [{ level: 1, breakBefore: { enabled: false } }] } // como a 1.4 diagramava

Onde a configuração ficou salva, o motor consegue perceber e faz isso por você: o Sandbox, nos livros e configurações que salvou na época (veja Sandbox → Persistência), e openBundle / readBundle, num pacote .postext gravado pelo postext 1.4 ou anterior (veja Pacotes gravados pelo postext 1.4 ou anterior). Uma configuração que você mesmo salvou pode passar por migrateConfig(config) de postext/bundle, que grava as quebras como a 1.4 as diagramava (pinLegacyHeadingBreaks): enabled: false num H1 que não tinha quebra, e parity: 'any' ao lado de um enabled: true que não indicava paridade, no H1 e nos estilos de título. Ele também fixa o tamanho das fórmulas, o espaço em volta das figuras na linha (também nos boxes), as marcas na linha dos títulos, o tamanho das capitulares, o espaço sob uma linha com dois-pontos que introduz uma lista, as linhas que o corte de um boxe deixa de um parágrafo ou de um item de lista, as quebras de linha em travessões, a quebra linha a linha do texto alinhado à esquerda, a divisão de um parágrafo sob um título, as quebras de linha no hífen de uma palavra composta e o espaço sob os contêineres :::paragraphs, como a 1.4 os compunha. Passe o markdown do livro como terceiro argumento, { content }, e ele deixa de fora a fixação das fórmulas quando o texto não tem nenhum $, as das lacunas quando nenhuma linha insere um recurso (a da lacuna nos boxes quando nenhuma o faz dentro de um boxe), a das marcas quando nenhum título tem marcas, a dos dois-pontos quando nenhuma lista vem depois de uma linha terminada em dois-pontos, a do corte de boxes quando o texto não abre nenhum :::callout, a dos travessões quando nenhum travessão está colado entre palavras, a da divisão sob título quando o texto não tem títulos, a dos contêineres quando o texto não abre nenhum contêiner :::paragraphs, e a das palavras compostas quando nenhum hífen está entre duas letras; a da quebra do texto alinhado à esquerda só vai para uma configuração que deixa algum texto corrido alinhado à esquerda, e a dos contêineres só para uma que declara um estilo de parágrafo (veja Pacotes gravados pelo postext 1.4 ou anterior). Para fixar só as quebras, chame pinLegacyHeadingBreaks(config).

As sobrescritas por nível aceitam as mesmas propriedades que os padrões gerais (fontSize, lineHeight, fontFamily, color, fontWeight, marginTop, marginBottom, snapToGrid) e mais estes campos exclusivos de nível (um estilo de título também os aceita):

PropriedadeTipoPadrãoDescrição
italicbooleanfalseCompõe o título em itálico. Aplicado por cima de fontWeight.
textTransform'none' | 'uppercase''none'Põe o título em maiúsculas (um prefixo de numeração fica como foi escrito, assim como um chip e o rótulo de um :ref nele). Preserva o comprimento para que o mapa de origem do editor continue 1:1: caracteres cuja forma maiúscula se expande (ß → SS) ficam como estão. O título transformado também alimenta o marcador {titleText} dos designs avançados e das aberturas de capítulo. Os marcadores do PDF mantêm o título como foi escrito (Author contributions, não AUTHOR CONTRIBUTIONS), do jeito que o text-transform do CSS não mexe no próprio texto (até o postext 1.4 eles pegavam as maiúsculas).
letterSpacingDimension0Tracking depois de cada glifo do título, inclusive os espaços e o prefixo de numeração, como o letter-spacing do CSS. Um valor positivo espaça as letras (maiúsculas compostas com textTransform: 'uppercase' costumam pedir um pouco: { value: 0.12, unit: 'em' }), um negativo aperta um corpo de destaque. Um valor em em é relativo ao fontSize do nível. As linhas do título são medidas com ele, então quebram onde termina o texto espaçado, e canvas, HTML e PDF o pintam igual. Uma linha centralizada ou alinhada à direita é posicionada pelas letras: o tracking depois do último glifo fica de fora, como no texto de design, para que um título centralizado se alinhe com a abertura padrão de um título span: 'page'. Um nível desenhado pelo seu advancedDesign o ignora, como os outros campos tipográficos daqui: cada elemento de texto do design tem o próprio letterSpacing. Um estilo de título também o define, para os títulos que o usam. Até o postext 1.4 os títulos não tinham tracking, e a chave era descartada sem aviso.
lineSpannumbernão definidoLinhas ocupadas (行取り, JLReq §4.1.6): o título é composto numa faixa com esse número de linhas do corpo, com os caracteres centralizados nela, no lugar de marginTop e marginBottom; 3 é um título 3行取り. A faixa começa numa linha do corpo quando o título se ajusta à grade, e o texto depois dele continua na grade, nos dois modos de escrita e com cjk.grid; um título cujas linhas precisem de mais ocupa o número inteiro seguinte. O centro é o dos caracteres (a linha de base menos o centro ideográfico da fonte), como a JLReq o mede, não o das caixas de linha. Não se aplica a aberturas (span: 'page'), títulos desenhados por um advancedDesign, títulos ocultos nem títulos dentro de boxes. Um estilo de título pode definir 0 para desligar o do seu nível. Desde o postext 1.16.
indentDimension0Recuo do título a partir do início da linha (字下げ), sendo em o corpo do texto, como os livros japoneses contam os recuos de títulos em caracteres do corpo (JLReq §4.1.3): { value: 6, unit: 'em' } é 6字下げ qualquer que seja o corpo do título. A medida diminui na mesma quantidade, e um título centralizado se centraliza no restante. Desde o postext 1.16.
jidorinumbernão definidoEspaçamento uniforme (字取り): um título de uma linha mais estreito que esse número de emes do próprio corpo é espaçado por igual até exatamente essa largura, então 3 compõe 序章 como 序 章. Títulos mais largos e títulos de várias linhas ficam como estão. {jidori=N} depois do texto de um título define um título só, e {jidori=0} desliga o do seu nível. Desde o postext 1.16.
numberingTemplatestring''Modelo do número automático do nível. Um token … imprime o contador corrente desse nível de título, opcionalmente formatado por um sufixo ( romano maiúsculo, romano minúsculo, / alfabético, com zeros à esquerda, por extenso (twenty-one), como ordinal (twenty-first), em numerais chineses (japoneses num documento em japonês: 第百一章), algarismo por algarismo, em numerais financeiros e circulado, ou qualquer nome de Grafias dos formatos de numeração), e qualquer outro texto é literal ('Chapter . ', '.', '第回' para 第一百二十回; uma barra invertida escapa uma chave literal). Um token cujo contador ainda está vazio desaparece junto com o separador vizinho. Vazio (o padrão) significa sem número automático. O número gerado é anteposto ao título no fluxo, alimenta o marcador de uma posição de design avançado (onde o prefixo em si não é anteposto) e é impresso no sumário. Veja Números por extenso para as palavras, e Estilos de título para um modelo próprio em alguns títulos de um nível.
numberSeparatorstring' 'O que fica entre o número e o título: na coluna, na abertura padrão de um nível span: 'page', nos cabeços que imprimem a linha do título e nos marcadores do PDF. O separador do nível 1 também une o número e o título de uma parte na página de parte padrão e na linha de parte padrão do sumário. Os títulos de capítulo chineses usam um espaço ideográfico ou nada (' ': 第一回 甄士隱夢幻識通靈). O sumário mantém a própria coluna de número (toc.levels[].numberGap). Um estilo de título pode definir o seu. Quando um título é partido em dois com , como os títulos em dístico dos romances chineses, as formas de uma linha só (a coluna, o sumário, os cabeços) unem as metades com um espaço ideográfico quando os dois lados são caracteres chineses ou japoneses, e com um espaço nos demais casos.
numberPosition'before' | 'replace''before'Onde fica o número gerado. 'before': antes do título, unido por numberSeparator. 'replace': o número é o título inteiro e o título escrito na fonte não é impresso, então # Night com numberingTemplate: 'الليلة {1:ordinal-feminine}' imprime الليلة الثانية. O sumário lista o número como título da entrada (sem coluna de número), e os cabeços (, ) e os marcadores do PDF também o leem; fica vazio num título assim. Só para títulos numerados com modelo (o do nível ou o do estilo deles); qualquer outro mantém o título. Um estilo de título pode definir o seu, 'before' para manter os títulos que o nível substituiria.
breakBeforeHeadingBreakBeforeConfigH1: { enabled: true, parity: 'always-odd' }
H2–H6: { enabled: false, parity: 'any' }
Força uma quebra de página antes de todo título deste nível. parity: 'odd' / 'even' restringe também o lado da página dupla em que o título abre: uma página em branco de preenchimento é inserida quando necessário (e continua contando na numeração das páginas). 'always-odd' / 'always-even' garantem além disso pelo menos uma página em branco separadora obrigatória entre o conteúdo anterior e o novo título (a separadora pertence ao capítulo anterior; qualquer preenchimento de paridade a mais pertence ao novo). Quando o título é o primeiríssimo bloco do documento e a primeira página ainda está vazia, a paridade não é aplicada: o título cai na página 1, como escrito. Um campo não definido mantém o padrão do nível: o { parity: 'odd' } do H1 continua quebrando.
hiddenbooleanfalseUm título estrutural: não imprime nada nem ocupa espaço na coluna ou num boxe (nem texto, nem margens, nem faixa de abertura), mas faz todo o resto que um título faz. O breakBefore dele continua abrindo página, ele abre a seção do seu estilo, conta (a menos que o estilo diga numbered: false) e é listado por :::toc, nomeado pelos cabeços e vira marcador no PDF. Serve para uma dedicatória, uma página de epígrafe ou um colofão de que o sumário e os marcadores do leitor precisam, mas que a página não mostra. Defina-o num estilo de título em vez de num nível inteiro; um título o sobrescreve com / .
headings: {
  fontFamily: 'Merriweather',
  levels: [
    // Predefinição canônica de livro: capítulos numa página à direita (ímpar).
    { level: 1, fontSize: { value: 24, unit: 'pt' }, breakBefore: { enabled: true, parity: 'odd' } },
    { level: 2, fontSize: { value: 18, unit: 'pt' }, italic: true },
  ]
}

snapToGrid também funciona por nível. Sem definição, um nível segue headings.snapToGrid; definido, o sobrescreve. Assim, um documento pode pôr os H2 a uma linha e meia do texto, fora da grade até o próximo ponto de ajuste, enquanto os H3 arredondam o espaço sob eles para linhas inteiras da grade. Um estilo de título também pode defini-lo, para os títulos que o usam.

headings: {
  marginBottom: { value: 1.5, unit: 'em' },
  levels: [
    { level: 2, snapToGrid: false }, // exatamente 1,5 em sob cada H2
    { level: 3 },                    // herda headings.snapToGrid: true
  ],
}

#Quebra antes

breakBefore é independente dos controles de numeração: ativá-lo força uma quebra de página, mas o contador numérico só recomeça quando você insere explicitamente uma diretiva :::numbering. As páginas em branco de paridade contam como páginas reais na sequência e recebem cabeçalhos e rodapés segundo as regras normais de par e ímpar.

Um :::pagebreak logo antes de um título assim não substitui a quebra dele: o título continua aplicando a paridade depois da página que a diretiva abriu, o que pode somar uma página em branco. Veja Estilos de título para um título que deve começar logo depois de uma quebra manual.

Valores de paridade

ValorComportamento
'any' (padrão)Sem restrição de paridade. O título simplesmente abre na página seguinte.
'odd'Garante que o título abra numa página ímpar (à direita). Uma única página em branco só é inserida quando a página seguinte natural é par.
'even'O mesmo, mas para uma página par (à esquerda).
'always-odd'Garante pelo menos uma página em branco separadora obrigatória entre o conteúdo anterior e o novo título e depois garante a paridade ímpar. Útil quando todo capítulo deve começar numa página dupla nova.
'always-even'O mesmo, mas para uma página par.

A quem pertencem as páginas em branco

As páginas em branco inseridas por breakBefore levam cabeçalhos com o título do capítulo conforme o motivo pelo qual foram inseridas:

  • As páginas inseridas para cumprir uma restrição de paridade ('odd', 'even' ou a parte de paridade de 'always-*') pertencem ao capítulo seguinte. O marcador {chapterTitle} do cabeçalho delas resolve para o título do novo capítulo, porque a página em branco só existe para levar o novo capítulo à paridade certa.
  • A separadora obrigatória inicial inserida por 'always-odd' / 'always-even' pertence ao capítulo anterior. É uma pausa deliberada de fim de capítulo, então o cabeçalho {chapterTitle} ainda mostra o título do capítulo antigo.

Os cabeços e a paleta de uma seção com estilo (veja Estilos de título) seguem as mesmas duas regras nas páginas em branco.

Exceção do início do documento

Quando o primeiríssimo bloco do documento é um título com breakBefore ativado (ou a fonte abre com :::pagebreak), a paridade não é aplicada enquanto a primeira página ainda está vazia. O título cai na página 1, como escrito, qualquer que seja a paridade configurada, de modo que um documento que começa com um # Chapter 1 configurado com parity: 'odd' não herda uma página em branco inicial espúria. Depois que algum conteúdo foi colocado, a paridade é aplicada normalmente.

#Largura e design avançado

Cada nível de título aceita dois campos adicionais que controlam como o título é renderizado como abertura de capítulo na largura da página inteira.

PropriedadeTipoPadrãoDescrição
span'column' | 'page''column'Com 'page', o título é tratado como abertura de capítulo, e seu design avançado (se ativado) é ligado à página como uma faixa de abertura acima do corpo. Combine com breakBefore.enabled: true para que a abertura sempre comece uma página nova. Sem um design próprio, o título é pintado por uma abertura padrão em toda a área de conteúdo, na tipografia e na entrelinha do nível, com seus trechos em negrito, itálico e sobrescrito ou subscrito (headings.inlineMarks), e a faixa é medida nessa largura. A faixa comporta todas as linhas que a abertura pinta: quando a abertura ocupa mais linhas que a medida do próprio título (um título justificado cujos espaços se encolheriam para caber numa linha, uma quebra forçada ), a faixa também as ocupa. Até o postext 1.4 a faixa era medida com o título quebrado na largura da coluna, de modo que um título que cabe numa linha da área de conteúdo ocupava uma faixa de duas linhas de altura, com a linha centralizada nela. As outras colunas começam abaixo da faixa, como a coluna do próprio título. O texto que abre uma delas começa onde começaria o texto logo abaixo da abertura, seja o que for que siga a abertura na sua própria coluna: a marginBottom da abertura abaixo do seu título ou do seu design, levada à próxima linha da grade quando o nível se ajusta a ela. Um título, uma fórmula em destaque ou uma linha do sumário que abre uma delas fica alinhado com o primeiro título ou a primeira fórmula em destaque abaixo da abertura, margens incluídas como lá; quando a coluna da abertura continua com outra coisa, começa onde começa esse texto. Um título oculto abaixo da abertura não conta, e o espaço que o balanceamento de colunas acrescenta acima do título abaixo da abertura não se repete nas outras colunas. O que essas colunas ajustam à grade cai na grade da página. Até o postext 1.4 o primeiro bloco delas ficava no pé da faixa: fora da grade quando a faixa terminava entre duas linhas, e colado à faixa, sem margem, quando um título seguia a abertura (uma linha mais alto que agora quando a faixa terminava na grade); um título ali também perdia a margem superior que o título da primeira coluna mantinha.
spanBreakbooleantrueSe um título span: 'page' começa uma página nova. true abre a página seguinte (o lado é escolhido por breakBefore). false, com breakBefore.enabled: false, abre o título onde o texto chega, como faz um boxe na largura da página: as colunas acima dele terminam niveladas (a faixa é balanceada quando headings.balancing.trailing está ligado), o design do título ou a abertura padrão é composto na área de conteúdo abaixo delas, e o texto continua em todas as colunas abaixo: um segundo artigo de um boletim sob o fim do primeiro. Quando o espaço restante não comporta o título e o mínimo de linhas contra viúvas abaixo dele, o título abre a página seguinte, como com true. O papel da página continua sendo o que lhe dá o seu primeiro bloco. Sem efeito sobre um título span: 'column'. Também num estilo de título. Desde o postext 1.19.
advancedDesignHeadingAdvancedDesignConfigEspaço de composição livre para este nível. Com enabled, os elementos do espaço compõem a abertura. Use dentro de um elemento de texto para renderizar o texto do título; use , etc. para inserir o número formatado do título.
advancedDesign.minHeightDimension—Altura mínima reservada para o título no fluxo da coluna. O título ocupa max(design content bottom, minHeight), depois a marginBottom do título abaixo dele (do seu estilo de título, do seu nível ou de headings.marginBottom; 0,5 em do tamanho do título por padrão), e a soma é arredondada para cima até a grade de linhas de base quando os títulos se ajustam a ela. Assim, uma abertura pode empurrar o texto do corpo para baixo (ou tomar a página inteira) mesmo quando seus elementos são baixos ou estão ancorados nos quadros da página ou da sangria acima do título. Para uma faixa com exatamente minHeight de altura, defina essa marginBottom como 0 e faça de minHeight um número inteiro de linhas da grade. Vale sempre que enabled for true, mesmo com o espaço vazio. Veja em Altura reservada o que conta como base do conteúdo do design.

Uma abertura tão alta quanto a página. Quando a altura reservada passa do pé da coluna (uma capa cuja minHeight é a altura da página, uma imagem ou um boxe ancorado na página ou na sangria que desce até o refile), a abertura toma o resto da página: o texto que vem depois dela começa na página seguinte, em todas as colunas, tanto num layout de duas colunas quanto num de coluna e meia. Por isso uma capa não precisa de um :::pagebreak depois dela (um não faz mal: não acrescenta página em branco). Um título de coluna (span: 'column') cujo design é mais alto que sua coluna toma essa coluna, e o texto começa no topo da seguinte. O bloco do título desce então até o pé da coluna que ele toma, e seu design é composto contra essa faixa: os elementos ancorados no topo ficam onde estão ancorados, e os que acompanham a faixa (ancorados no meio ou no pé dela, ou com altura 'fill') se limitam ao espaço que o título toma, como se limitam à altura reservada quando ela cabe (até o postext 1.4 o bloco mantinha a altura do seu texto, de modo que esses elementos eram compostos contra uma faixa da altura de um título, e o texto seguinte continuava por baixo do design). Elementos fixos da página que não devem tomar a página (uma tarja na margem externa ao longo de toda a página, um ornamento no pé da página) ficam melhor no design do cabeço ou do rodapé: ancorados em 'page' ou 'bleed' e exibidos com pages: 'opener', são pintados na página de abertura e não reservam espaço no corpo.

Exemplo: uma abertura de capítulo minimalista que mostra “Chapter N” acima do título:

{
  "headings": {
    "levels": [
      {
        "level": 1,
        "span": "page",
        "breakBefore": { "enabled": true, "parity": "always-odd" },
        "advancedDesign": {
          "enabled": true,
          "slot": {
            "elements": [
              {
                "kind": "text",
                "id": "chapterLabel",
                "placement": {
                  "anchor": { "to": "container", "edge": "top" },
                  "offset": { "y": { "value": 48, "unit": "pt" } },
                  "size": { "width": "fill" }
                },
                "content": "Chapter {numberRoman}",
                "fontSize": { "value": 10, "unit": "pt" },
                "align": "center",
                "overflow": "ellipsis-end"
              },
              {
                "kind": "text",
                "id": "chapterTitle",
                "placement": {
                  "anchor": { "to": "#chapterLabel", "edge": "below" },
                  "offset": { "y": { "value": 12, "unit": "pt" } },
                  "size": { "width": "fill" }
                },
                "content": "{titleText}",
                "fontSize": { "value": 24, "unit": "pt" },
                "fontWeight": 700,
                "align": "center",
                "overflow": "wrap",
                "hyphenate": true
              }
            ]
          }
        }
      }
    ]
  }
}

Marcadores de título disponíveis dentro do espaço de design de um nível:

  • {titleText}: o texto simples do título (sem o prefixo de numeração). Uma quebra forçada no título (\\) é aqui uma quebra de linha, tanto numa faixa de abertura quanto num design de coluna (até o postext 1.4 um design de coluna imprimia um espaço ali, embora sua altura fosse medida com a quebra). As linhas em que o título oculto se quebra na sua coluna são reunidas de novo no título tal como foi escrito: uma palavra quebrada depois do seu próprio hífen mantém o hífen, sem espaço depois dele; uma palavra que a coluna dividiu volta a ficar inteira, sem o hífen que a quebra acrescentou; um grupo unido por um espaço inseparável que era mais largo que a coluna e foi separado nesse espaço recupera o espaço inseparável (Capítulo XVIII, não CapítuloXVIII); e cada uma das outras quebras devolve o seu espaço. Até o postext 1.4 toda quebra de linha virava um espaço, de modo que um título quebrado num hífen imprimia Word- Book, e uma quebra forçada num título assim se perdia.
  • {number}: o número formatado segundo o numberingTemplate do nível.
  • {numberDecimal}, {numberRoman}, {numberRomanLower}, {numberAlpha}, {numberAlphaLower}: o contador do título (a contagem corrente do seu nível, seja o que for que o modelo imprima) em outros formatos de numeral: o terceiro capítulo dá 3, III, iii, C, c, com ou sem numberingTemplate. Um título não numerado (um estilo com numbered: false) os deixa vazios.
  • {numberWords}, {numberWordsLower}, {numberOrdinalWords}, {numberOrdinalWordsLower}: o mesmo contador por extenso, com maiúscula inicial ou em minúsculas: Three / three, Third / third (veja Números por extenso).
  • {numberHan}: o mesmo contador em numerais chineses, na escrita do locale do documento: uma abertura composta com 第{numberHan}回 imprime 第十二回 no décimo segundo capítulo, enquanto o sumário mostra 12.
  • {chapterNumber}, {chapterTitle}, {pageNumber}, {totalPages}, {bookTotalPages}, {title}, {subtitle}, {author}, {publishDate}: marcadores de metadados compartilhados.
  • {attr.<key>}: um atributo escrito na própria linha do título (# Title {author="I. Zango Martín"}), que recorre ao atributo do H1 do capítulo atual. Atributos ausentes resultam numa string vazia, sem aviso.

{chapterNumber} imprime o que os cabeços imprimem para o capítulo: o número do H1 quando o seu nível (ou estilo) tem um numberingTemplate; caso contrário, o ordinal do capítulo (1, 2…, continuando depois dos capítulos diagramados antes deste); e nada num capítulo não numerado ou num estilo cujo modelo é ''. O design de um título lê o capítulo ao qual o título pertence: um título de nível 1, o seu próprio; um título inferior, o último título de nível 1 antes dele. Isso vale também quando dois capítulos se encontram numa mesma página, embora os cabeços dessa página imprimam o capítulo posterior. A altura que uma abertura reserva é medida com esse mesmo valor. Até o postext 1.4 ela era medida com o prefixo de número do título (vazio sem modelo), de modo que um design cuja altura dependesse de {chapterNumber} podia ser pintado mais alto que o espaço que ocupava; e o design imprimia o capítulo da página, de modo que o primeiro de dois capítulos que dividiam uma página mostrava o número do segundo.

Os marcadores de contador acompanham o livro através de capítulos diagramados um de cada vez (os contadores que continuationAfter() repassa), e o atributo startAt de um título os reinicia (veja Formato do documento → Atributos de título). Uma abertura que diz Chapter III acima de um título numerado 3. no fluxo e no sumário:

{
  level: 1,
  span: 'page',
  numberingTemplate: '{1}.',
  advancedDesign: {
    enabled: true,
    slot: { elements: [
      { kind: 'text', id: 'label', content: 'Chapter {numberRoman}', fontSize: { value: 10, unit: 'pt' }, overflow: 'ellipsis-end',
        placement: { anchor: { to: 'container', edge: 'top-left' }, size: { width: 'fill', height: 'auto' } } },
      { kind: 'text', id: 'title', content: '{titleText}', fontSize: { value: 24, unit: 'pt' }, overflow: 'wrap',
        placement: { anchor: { to: '#label', edge: 'below' }, size: { width: 'fill', height: 'auto' } } },
    ] },
  },
}

Altura reservada

Um título com design avançado ocupa espaço na coluna como qualquer bloco: o texto do corpo que vem depois dele começa abaixo desse espaço. O espaço é a maior de três alturas, todas medidas para baixo a partir do topo do título (o topo da área de conteúdo, numa abertura que começa a sua página):

  1. o próprio texto do título, composto na tipografia do nível (fica oculto sob o design, mas mantém suas linhas);
  2. a base do conteúdo do design: a borda inferior mais baixa entre os elementos que contam (veja abaixo);
  3. advancedDesign.minHeight.

A marginBottom do título é somada depois (do seu estilo de título, do seu nível ou de headings.marginBottom; 0,5 em por padrão), e o resultado é arredondado para cima até a grade de linhas de base quando os títulos se ajustam a ela. Numa abertura (span: 'page'), a mesma faixa fica livre em todas as colunas da página. Num título de coluna que cabe na sua coluna, o design é composto na caixa do título, que tem exatamente essa altura.

Quais elementos contam. Todos os elementos do design contam (textos, fios, caixas e imagens; até o postext 1.4 um elemento image nunca contava, de modo que o texto podia começar por cima de uma imagem da faixa, a menos que minHeight o afastasse), exceto:

  • os elementos com reserve: false: decoração que pode ficar sob o texto;
  • os elementos que acompanham a própria faixa: ancorados na linha do meio do contêiner (left, center, right) ou na linha de baixo (bottom-left, bottom, bottom-right), com altura 'fill' em relação ao contêiner (uma caixa ou um fio vertical sem altura o preenche por padrão), e qualquer elemento ancorado num deles. O contêiner é a faixa reservada, então esses elementos ficam no pé dela ou a atravessam: um fio sob a faixa, um painel colorido atrás do título. Eles acompanham a altura; nunca a definem. Um texto entre eles que mantém sua própria altura ainda precisa de espaço: um título ancorado no pé da faixa deixa a faixa pelo menos tão alta quanto o título, de modo que o título nunca começa acima do topo do título oculto. Com minHeight: 36mm e o título ancorado em bottom-left, a caixa do título tem 36 mm mais a sua marginBottom, arredondada para cima até a grade, e o título fica no pé dessa caixa, logo acima do texto que vem depois; sem minHeight, um título que ocupa mais linhas que o texto oculto deixa a faixa tão funda quanto o título. Até o postext 1.4 um título assim era pintado para cima, sobre o texto acima do título oculto. Caixas, fios e imagens que acompanham a faixa não impõem esse piso, então um painel ancorado no pé pode subir acima do título.

Os elementos ancorados na página e na sangria contam pelo quanto descem abaixo do topo do título. Uma faixa no alto da página que termina acima do título não conta nada; uma imagem sangrada que passa dele empurra o texto até a sua borda inferior. O mesmo acontece com tudo o que fica baixo na página: um selo a 25 mm do pé da página, uma faixa lateral de altura inteira ou uma moldura reservam a página até a sua borda inferior, e o texto em geral começa na página seguinte. Marque essa decoração com reserve: false (ela continua sendo pintada e outros elementos ainda podem se ancorar nela) e dê ao título o espaço de que ele precisa com os seus elementos de texto ou com minHeight:

{
  "kind": "image", "id": "seal", "resourceId": "seal", "reserve": false,
  "placement": {
    "anchor": { "to": "page", "edge": "bottom-right" },
    "offset": { "x": { "value": -25, "unit": "mm" }, "y": { "value": -25, "unit": "mm" } },
    "size": { "width": { "value": 30, "unit": "mm" } }
  }
}

Um elemento ancorado num elemento que não reserva espaço continua contando, a menos que também seja marcado (uma legenda posta no selo precisa do seu próprio reserve: false).

Onde é pintado. O design de uma abertura (span: 'page') é desenhado antes do corpo, então a decoração que não reserva nada fica sob o texto. O design de um título de coluna é desenhado junto com o bloco do título (por cima dos blocos acima dele na coluna, por baixo dos que vêm depois) onde quer que esteja posicionado acima do pé da sua coluna: também nas margens laterais, na margem superior, na sangria e nas colunas vizinhas. O pé da coluna o corta, no canvas e no PDF, porque o fluxo termina ali (veja Mais alto que a coluna, abaixo). Até o postext 1.4 o canvas e o PDF também o cortavam no topo da sua coluna, de modo que uma faixa ancorada no topo da página ou da sangria entrava nas margens laterais, mas parava na margem superior. Decoração ancorada no pé da página fica melhor numa abertura, ou no design do rodapé com pages: 'opener'.

Mais alto que a coluna. Quando o espaço passa do pé da coluna (uma minHeight tão alta quanto a página, uma imagem ou uma moldura que desce até o refile), o título toma o resto da sua página (uma abertura, em todas as colunas) ou da sua coluna (um título de coluna), e o texto que vem depois dele começa na página ou na coluna seguinte. Seu bloco vai então até o pé da coluna, nunca reduzido à altura do seu texto, de modo que a faixa contra a qual o design é composto é o espaço que ele toma (minHeight incluída, até o pé). Um design mais alto que a própria página (uma linha fina longa numa página de tela pequena) continua sendo cortado no pé da página (um design de coluna, no da sua coluna): o Sandbox o lista no painel Verificações como Design do título cortado. A diagramação não emite aviso por isso (uma capa que toma a sua página é o caso comum e não perde nada), mas qualquer aplicação pode fazer a mesma verificação no layout pronto: collectHeadingDesignCuts(doc) devolve um { kind: 'headingDesignCut', pageIndex, level, where, overflowPx, sourceStart, sourceEnd } para cada título cujo texto de design fica além do pé do refile da página (where: 'page', uma abertura) ou da sua coluna (where: 'column'), e formatWarning descreve cada um. Veja Uma abertura tão alta quanto a página em Largura e design avançado.

A coluna lateral. Num layout de coluna e meia cuja coluna lateral recebe flutuantes (sideColumnRole: 'floats'), um elemento do design de um título de coluna que fica na coluna lateral (um numeral de capítulo ancorado na página, na coluna da margem externa da abertura de um livro didático) mantém a pilha lateral longe dele. Cada figura, tabela ou boxe span: 'side' que a página compõe depois do título mantém um espaço entre flutuantes livre em relação a cada um desses elementos: fica onde a pilha o coloca quando cabe acima do elemento e, caso contrário, vai para baixo dele, ou espera a página seguinte quando o resto da coluna lateral não o comporta ali. Assim, um numeral no alto do canal mantém a pilha inteira abaixo dele, enquanto um número de seção pendurado na margem ao lado de um título mais abaixo na página deixa o alto do canal para as figuras que a página cita (a figura marginal continua no topo da sua página). O que a coluna lateral já contém quando o título é posicionado não se move: uma figura empilhada antes na página que desce até o elemento do título fica onde está, sob o elemento, então um design cujo elemento fica na coluna lateral no meio da página pede que as suas figuras sejam citadas depois do título. Os elementos com reserve: false deixam a coluna lateral livre, como deixam o texto. Uma abertura (span: 'page') não precisa de nada disso: a sua faixa é reservada em todas as colunas, inclusive na lateral. Até o postext 1.4 uma figura lateral citada numa abertura assim era composta no alto da coluna lateral, por cima do numeral.

Capas. Um título de capa que preenche a sua página (minHeight tão alta quanto a página, ou uma imagem de página inteira no seu design) manda, portanto, o texto que vem depois dele para a página seguinte por si só, em layouts de uma ou de várias colunas. Um :::pagebreak logo depois dele é opcional e não faz mal: uma quebra de página numa página ainda vazia não faz nada, então nunca acrescenta uma página em branco. Ele só é necessário quando o design da capa para antes do pé da página e o texto ainda assim deve começar numa página nova.

# Annual report 2026 {style="cover"}
 
:::pagebreak
 
# Letter from the chair

#Números por extenso

Dois sufixos de modelo de numeração escrevem um contador por extenso, no idioma do documento (o locale de nível superior ou, na falta dele, o idioma de hifenização; veja Idioma do documento): words para o cardinal e ordinal para o ordinal. A caixa do sufixo define a caixa das palavras, como A / a faz com as letras:

TokenInglês (21)Espanhol (21)Chinês (21)
twenty-oneveintiuno二十一
Twenty-oneVeintiuno二十一
TWENTY-ONEVEINTIUNO二十一
twenty-firstvigesimoprimero第二十一
Twenty-firstVigesimoprimero第二十一
TWENTY-FIRSTVIGESIMOPRIMERO第二十一

Inglês, espanhol, chinês e árabe são escritos por extenso; qualquer outro idioma usa as palavras em inglês, como fazem as strings de continuação de tabela embutidas. O chinês escreve os numerais informais de simp-chinese-informal ou trad-chinese-informal, conforme a escrita do locale (一万 / 一萬), com 第 antes de um ordinal; os caracteres Han não têm caixa, então as três grafias de um sufixo imprimem o mesmo. O inglês segue o uso americano (one hundred five, sem and). O espanhol usa formas masculinas, como se numera um capítulo ou um libro (capítulo primero, tercero, veintiuno), e escreve os ordinais de 13 a 29 numa só palavra, como prefere a RAE (decimotercero, vigesimoprimero). Os cardinais são escritos por extenso até 999 999, e os ordinais em espanhol até 999; números maiores saem em algarismos.

Os números em árabe concordam em gênero com o substantivo que contam, então o sufixo aceita um modificador: -feminine (ou -f) para um substantivo feminino, -masculine (-m, o padrão) para um masculino; -classical escreve as centenas مائة, como fazem Bulaq e a maioria das edições egípcias, em vez do moderno مئة. {1:ordinal} escreve o ordinal definido no nominativo que um título usa: الفصل {1:ordinal} dá الفصل الأول, الفصل الحادي عشر, الفصل الحادي والعشرون; الليلة {1:ordinal-feminine} dá الليلة الأولى, الليلة الحادية عشرة, الليلة الحادية والعشرون, الليلة المئتان, e acima de cem a fórmula clássica de “depois”: الليلة الخامسة والأربعون بعد الثلاثمئة, الليلة الحادية بعد الألف. {1:words} escreve o cardinal (واحد وعشرون; feminino إحدى عشرة, واحدة وعشرون). Os ordinais são escritos por extenso até 9 999 e os cardinais até 99 999; os modificadores se combinam ({1:ordinal-f-classical}), e os outros idiomas os ignoram. O árabe não tem maiúsculas, então a caixa do sufixo não muda nada. Os marcadores de design {numberWords} e {numberOrdinalWords} escrevem as formas masculinas; para uma abertura no feminino, ponha o ordinal no modelo do nível e imprima-o com {number}.

Num design de título, {numberWords} / {numberWordsLower} e {numberOrdinalWords} / {numberOrdinalWordsLower} escrevem o contador do título da mesma forma, de modo que a abertura pode dizer Chapter One enquanto o sumário mostra 1. O textTransform: 'uppercase' de um elemento de texto dá as maiúsculas:

// Romance em espanhol: "CAPÍTULO PRIMERO" acima do título, "1." no sumário.
{ level: 1, numberingTemplate: '{1}.', span: 'page',
  advancedDesign: { enabled: true, slot: { elements: [
    { kind: 'text', id: 'n', content: 'Capítulo {numberOrdinalWordsLower}', textTransform: 'uppercase', /* … */ },
    { kind: 'text', id: 't', content: '{titleText}', /* … */ },
  ] } } }

#Listas com marcadores

A propriedade unorderedLists controla como são renderizadas as listas com marcadores (-, *, +) e as listas de tarefas do GFM (- [ ], - [x]). São aceitos até cinco níveis de aninhamento.

#Padrões das listas com marcadores

PropriedadeTipoPadrãoDescrição
fontFamilystringherda bodyText.fontFamilyFonte usada no texto dos itens.
colorColorValueCor principal (#295AA3)Cor do texto e do marcador dos itens. Vinculada à entrada main-color da paleta padrão.
fontWeightnumber700Peso do texto dos itens (100–900). Os marcadores herdam esse peso, a menos que seja substituído por nível.
italicbooleanfalseRenderiza o texto dos itens em itálico.
bulletCharstring'•'Glifo usado como marcador.
bulletFontSizeDimension1 emTamanho do glifo do marcador. As unidades relativas acompanham o tamanho da fonte do corpo.
gapDimension0.5 emEspaço horizontal entre o marcador e o texto do item.
indentDimension0 emRecuo base do nível 1. Os níveis mais profundos partem do início do texto do nível pai, a menos que sejam substituídos (veja abaixo).
bulletVerticalOffsetDimension0 emAjuste fino da posição vertical do marcador. Valores negativos sobem o marcador; valores positivos o descem.
marginTop / marginBottomDimension1.5 emEspaço antes e depois da lista como um todo.
itemSpacingDimension0 emEspaço vertical extra inserido entre os itens, além da entrelinha. Em volta de uma lista aninhada num item de outra, vale o espaçamento da lista externa dos dois lados, antes do primeiro item da lista aninhada e depois do último (até o postext 1.4 o item depois de uma lista aninhada recebia o espaçamento da lista aninhada).
snapTopToGridbooleanfalseArredonda para cima o espaço acima da lista para que o primeiro marcador fique na grade de linhas de base, como o texto sob um título; marginTop passa então a ser um mínimo. O fim de uma lista devolve o fluxo à grade de qualquer forma, então, com itemSpacing em 0, todos os itens se alinham com o texto da coluna ao lado. Desligado por padrão, como até o postext 1.4: uma marginTop que não é um número inteiro de linhas deixa os itens fora da grade até a lista terminar. As listas dentro de boxes, cujo interior fica fora da grade, não são afetadas.
hangingIndentbooleantrueQuando ativado, as linhas quebradas se alinham com o primeiro caractere do texto, e não sob o marcador.
levelsUnorderedListLevelConfig[]—Substituições por profundidade para os níveis 1–5. Veja abaixo.

#Extensões para listas de tarefas

Os itens de tarefa do GFM (- [ ] …, - [x] …) são renderizados como itens com marcador, com um glifo de caixa de seleção no lugar do marcador. Os campos a seguir se aplicam só aos itens de tarefa:

PropriedadeTipoPadrãoDescrição
taskCheckboxCharstring'☐'Glifo usado para tarefas não marcadas.
taskCheckedCharstring'☑'Glifo usado para tarefas concluídas.
taskCompletedStrikethroughbooleantrueDesenha um risco sobre o texto das tarefas concluídas.
taskCompletedColorColorValueherda a cor do itemCor opcional aplicada ao texto das tarefas concluídas. Quando omitida, usa-se a cor normal do item.

#Substituições por nível (listas com marcadores)

Cada entrada de levels se refere a uma profundidade (1–5) e pode substituir qualquer um dos seguintes:

PropriedadeTipoDescrição
bulletCharstringGlifo do marcador nesta profundidade.
fontFamilystringFamília tipográfica dos itens nesta profundidade.
fontSizeDimensionTamanho do glifo do marcador nesta profundidade.
colorColorValueCor do item.
fontWeightnumberPeso do item.
italicbooleanLiga ou desliga o itálico.
indentDimensionRecuo explícito do marcador nesta profundidade. Veja a regra de cascata abaixo.
verticalOffsetDimensionAjuste fino vertical do marcador nesta profundidade.

Cascata de recuos. O nível 1 sempre começa no valor geral de indent (por padrão 0 em: os marcadores ficam presos à borda da coluna). Nos níveis 2–5, se você deixar indent indefinido, o motor põe o marcador no início do texto do nível anterior (recuo do pai + largura do marcador + gap). Defina um indent explícito num nível para interromper a cascata e fixar essa profundidade onde quiser.

unorderedLists: {
  bulletChar: '—',
  gap: { value: 0.4, unit: 'em' },
  hangingIndent: true,
  levels: [
    { level: 2, bulletChar: '·' },
    { level: 3, bulletChar: '◦', color: { hex: '#666666', model: 'hex' } },
  ],
}

#Listas numeradas

A propriedade orderedLists controla as listas numeradas (1., 2) etc.). São aceitos até cinco níveis de aninhamento, e cada profundidade pode usar um formato de número diferente.

#Padrões das listas numeradas

PropriedadeTipoPadrãoDescrição
fontFamilystringherda bodyText.fontFamilyFonte usada no texto dos itens e no número.
colorColorValueCor principal (#295AA3)Cor do texto e do número dos itens. Vinculada à entrada main-color da paleta padrão.
fontWeightnumber700Peso do texto dos itens e dos números (100–900).
italicbooleanfalseRenderiza o texto dos itens em itálico.
numberFormatOrderedListNumberFormat'arabic'Estilo do número: 'arabic', 'lower-alpha', 'upper-alpha', 'lower-roman', 'upper-roman'. As grafias das outras configurações também funcionam ('decimal', 'roman-lower', 'i'…; veja Grafias dos formatos de numeração); um valor desconhecido numera em algarismos arábicos e é relatado.
prefixstring''Texto composto antes do número, no estilo do separador: com '(' aqui e ')' como separador, uma lista chinesa fica (一), (二). Quando o separador é desenhado como um trecho à parte, o prefixo também é, logo antes do número.
separatorstring'.'Caractere posto entre o número e o texto, normalmente '.' ou ')'.
separatorFontFamilystringherda fontFamilyFonte do separador. Quando algum estilo do separador difere do estilo do número, o separador é desenhado como um trecho à parte depois do número (alinhado à direita); por exemplo, 1 em Optima Bold preto seguido de • em DIN Pro Bold azul.
separatorFontWeightnumberherda fontWeightPeso do separador (100–900).
separatorItalicbooleanherda italicRenderiza o separador em itálico.
separatorColorColorValueherda colorCor do separador. As referências à paleta são respeitadas.
separatorGapDimension0 emEspaço entre o número e o separador. O texto do item continua começando a gap do separador.
numberFontSizeDimension1 emTamanho do número.
gapDimension0.5 emEspaço horizontal entre o número e o texto do item.
indentDimension0 emRecuo base do nível 1; os níveis mais profundos partem do início do texto do nível pai, a menos que sejam substituídos.
numberVerticalOffsetDimension0 emAjuste fino da posição vertical do número.
marginTop / marginBottomDimension1.5 emEspaço antes e depois da lista como um todo.
itemSpacingDimension0 emEspaço vertical extra entre os itens. Em volta de uma lista aninhada num item de outra, vale o espaçamento da lista externa dos dois lados, antes do primeiro item da lista aninhada e depois do último (até o postext 1.4 o item depois de uma lista aninhada recebia o espaçamento da lista aninhada).
snapTopToGridbooleanfalseArredonda para cima o espaço acima da lista para que o primeiro número fique na grade de linhas de base, como o texto sob um título; marginTop passa então a ser um mínimo. O fim de uma lista devolve o fluxo à grade de qualquer forma, então, com itemSpacing em 0, todos os itens se alinham com o texto da coluna ao lado. Desligado por padrão, como até o postext 1.4: uma marginTop que não é um número inteiro de linhas deixa os itens fora da grade até a lista terminar. As listas dentro de boxes, cujo interior fica fora da grade, não são afetadas.
numberWidth'run' | 'level''run'A largura da coluna de números de um item, que define onde o seu texto começa; os números ficam alinhados à direita nela. 'run': o número mais largo da própria sequência do item, isto é, os itens de uma mesma profundidade sem nada além de itens mais profundos entre eles. Uma figura, um parágrafo ou um boxe entre dois itens inicia uma nova sequência, então ii) depois de uma tabela pode começar o texto um pouco mais à direita que i) antes dela, e uma lista de nove itens compõe o texto mais à esquerda que uma lista de doze. 'level': o número mais largo na profundidade do item em todo o documento (o capítulo, num livro), de modo que todas as listas, e todas as partes de uma lista interrompida, começam o texto no mesmo lugar, como já fazem os recuos dos níveis mais profundos.
hangingIndentbooleantrueAs linhas quebradas se alinham com o primeiro caractere do texto, e não sob o número.
levelsOrderedListLevelConfig[]—Substituições por profundidade para os níveis 1–5.

#Substituições por nível (listas numeradas)

Cada entrada de levels pode substituir numberFormat, prefix, separator, fontFamily, fontSize, color, fontWeight, italic, indent, verticalOffset e o estilo do separador (separatorFontFamily, separatorFontWeight, separatorItalic, separatorColor, separatorGap); vale a mesma cascata de recuos das listas com marcadores. O estilo do separador de um nível herda o estilo do número desse mesmo nível, a menos que a configuração do separador para a lista inteira seja informada.

Alinhamento à direita. O pipeline mede o número formatado mais largo de uma sequência e recua todos os itens dessa sequência para que os números se alinhem pela borda direita. Numa lista de dez itens renderizada como 1. – 10., os números de um dígito recebem preenchimento à esquerda para que o separador fique na mesma coluna.

orderedLists: {
  numberFormat: 'arabic',
  separator: '.',
  levels: [
    { level: 2, numberFormat: 'lower-alpha' },
    { level: 3, numberFormat: 'lower-roman', separator: ')' },
  ],
}

Isso produz a mistura aninhada clássica:

1. First item
   a. Sub-item
      i) Deep note
   b. Sub-item
2. Second item

A hierarquia dos documentos chineses (GB/T 15834—2011, Anexo B.3) tem cinco níveis: 一、, depois (一), depois 1., depois (1) e depois ①:

orderedLists: {
  levels: [
    { level: 1, numberFormat: 'simp-chinese-informal', separator: '、' },
    { level: 2, numberFormat: 'simp-chinese-informal', prefix: '(', separator: ')' },
    { level: 3, numberFormat: 'arabic', separator: '.' },
    { level: 4, numberFormat: 'arabic', prefix: '(', separator: ')' },
    { level: 5, numberFormat: 'circled-decimal', separator: '' },
  ],
}

#Matemática

A propriedade math controla como são analisadas e renderizadas as fórmulas LaTeX entre os delimitadores $...$ (em linha) e $$...$$ (em destaque). O motor por trás é o MathJax (o pacote mathjax-full) no modo de saída SVG, rasterizado no canvas e incorporado como glifos escaláveis no PDF.

interface MathConfig {
  enabled?: boolean;        // Renderiza o LaTeX. Com false, os trechos passam como TeX literal.
  fontSizeScale?: number;   // × o tamanho do texto ao redor (o tamanho do corpo nas fórmulas em destaque).
  color?: ColorValue;       // Cor das fórmulas; herda a cor do corpo se omitida.
  marginTop?: Dimension;    // Espaço acima dos blocos de fórmula em destaque.
  marginBottom?: Dimension; // Espaço mínimo abaixo; o ajuste à grade de linhas de base pode aumentá-lo.
  indentAfterDisplay?: boolean; // Recua um parágrafo que vem depois de uma fórmula em destaque.
  keepWithLeadIn?: boolean; // Mantém uma fórmula em destaque com a linha que a introduz.
  equationNumbering?: {      // Numera as fórmulas em destaque que levam um \label.
    enabled?: boolean;           // padrão true
    numberingTemplate?: string;  // '{n}'; '{h1}.{n}' numera por capítulo
    resetOn?: ResourceCounterReset; // 'never' | 'h1' … 'h6'
    counterFormat?: ResourceCounterFormat; // 'decimal'
    format?: string;             // '({n})': o que a fórmula e \eqref imprimem
  };
}
PropriedadeTipoPadrãoDescrição
enabledbooleantrueCom false, os trechos $...$ e $$...$$ continuam sendo analisados (então os avisos de delimitador não fechado continuam aparecendo), mas são renderizados como o seu código TeX literal. Útil quando o conteúdo contém cifrões de propósito ou quando você quer desativar por completo a renderização de fórmulas.
fontSizeScalenumber1.0Multiplicador aplicado ao tamanho do texto ao redor antes da renderização: um em da fonte TeX da fórmula é esse tamanho × fontSizeScale, ou seja, bodyText.fontSize numa fórmula em destaque e nas fórmulas em linha do texto do corpo, e o tamanho do bloco que as contém nas fórmulas em linha de um título, de um estilo de parágrafo, de uma legenda ou do corpo de um boxe. 1,0 iguala o texto ao redor; valores entre 0,9 e 1,1 são comuns quando a fonte matemática parece um pouco maior ou menor que a fonte do texto. Mudou no postext 1.5: até a 1.4 as fórmulas saíam cerca de 13% maiores que isso (veja abaixo).
colorColorValueherda a cor do corpoCor da fórmula renderizada. Omita para herdar bodyText.color. Defina explicitamente quando quiser as fórmulas numa cor diferente da do texto, por exemplo igual ao destaque de um título.
marginTopDimension0.8emEspaço acima de um bloco de fórmula em destaque. Ignorado nas fórmulas em linha.
marginBottomDimension0.8emEspaço abaixo de um bloco de fórmula em destaque. É um mínimo: o ajuste à grade pode aumentá-lo para que a próxima linha de base caia numa linha da grade (quer page.baselineGrid desenhe a grade, quer não).
indentAfterDisplaybooleantrueRecua a primeira linha de um parágrafo que vem depois de uma fórmula em destaque, como em qualquer outro parágrafo. false compõe sem recuo todo parágrafo logo depois de uma fórmula em destaque, como continuação da frase que a fórmula interrompeu (“onde L é…”). Uma fórmula escrita dentro de um parágrafo (sem linha em branco acima nem abaixo dela) é sempre seguida sem recuo: o texto abaixo do seu $$ de fechamento continua esse parágrafo e nunca recebe recuo (veja Fórmulas matemáticas).
keepWithLeadInbooleanfalseMantém uma fórmula em destaque na coluna da linha que a introduz: a penalidade predisplay do TeX. Quando a fórmula não cabe abaixo da última linha do parágrafo anterior, essa linha vai para a coluna ou página seguinte junto com a fórmula; quando as linhas deixadas para trás seriam menos que bodyText.widowMinLines (menos que uma quando bodyText.avoidWidows está desligado), um parágrafo que começa nessa coluna passa inteiro adiante (com os títulos que fecham a coluna acima dele, conforme headings.keepWithNext). A linha levada fica sozinha no alto da coluna seguinte, seja o que for que a regra de órfãs peça. Com false só a fórmula passa adiante, e a linha que a introduz pode fechar a coluna acima ou ficar sobre uma figura que encabeça a seguinte.
equationNumbering{ enabled, numberingTemplate, resetOn, counterFormat, format }true, '{n}', 'never', 'decimal', '({n})'Como são numeradas as fórmulas em destaque que levam um \label (veja Equações numeradas abaixo). numberingTemplate, resetOn e counterFormat funcionam como os de um tipo de recurso: '{h1}.{n}' com resetOn: 'h1' numera (2.1), (2.2)… por capítulo, e '{h1}.{h2}.{n}' com 'h2', por seção. format é o número tal como a fórmula e \eqref o imprimem, com {n} no lugar do número: '[{n}]' compõe [3]. enabled: false não numera nada: um \label é descartado, e uma referência a ele imprime a sua página.
math: {
  enabled: true,
  fontSizeScale: 1.0,
  color: { hex: '#295AA3', model: 'hex' },
  marginTop: { value: 1, unit: 'em' },
  marginBottom: { value: 1, unit: 'em' },
}

Tamanho das fórmulas, alterado no postext 1.5. O MathJax dá a caixa de uma fórmula em ex, e um ex da sua fonte TeX equivale a 0,442 em. Até o postext 1.4 o motor o tomava como meio em, de modo que toda fórmula era composta cerca de 13% maior que bodyText.fontSize × fontSizeScale. Agora as fórmulas saem no tamanho documentado, e as linhas e páginas com fórmulas são recompostas. Uma configuração escrita em código para a 1.4 mantém o tamanho de fórmula da 1.4 se passar por pinLegacyMathSize, de postext/bundle, uma única vez, tal como foi escrita:

import { pinLegacyMathSize } from 'postext/bundle';
 
config = pinLegacyMathSize(config); // as fórmulas, e o espaço em volta das fórmulas em destaque, como a 1.4 as compunha

Outras mudanças de regras na 1.5 também podem mexer nas suas páginas: as quebras dos títulos, o espaço em volta de uma figura em linha (no texto corrido e nos boxes), as marcas em linha dos seus títulos, o tamanho das suas capitulares, o espaço reservado sob uma linha terminada em dois-pontos para a lista que ela introduz, as linhas que o corte de um boxe deixa de um parágrafo ou de um item de lista, as quebras de linha depois de um travessão, a quebra do texto em bandeira, a divisão de um parágrafo sob um título, as quebras de linha depois do hífen de uma palavra composta e o espaço sob um contêiner :::paragraphs. migrateConfig, executado uma vez com o markdown que a configuração diagrama, fixa as que esse texto precisa, o tamanho das fórmulas incluído (veja Pacotes escritos pelo postext 1.4 ou anterior):

import { migrateConfig } from 'postext/bundle';
 
config = migrateConfig(config, undefined, { content: markdown }); // quebras de títulos, fórmulas, espaços em linha, marcas de títulos, capitulares, linhas com dois-pontos, cortes de boxes, quebras após travessão, quebras de compostos, texto em bandeira, divisões sob um título e espaço dos contêineres como a 1.4 os compunha

Isso segura o que essas mudanças de regras moveriam, mas não preserva todas as páginas da 1.4. A versão 1.5 também corrige bugs de layout, e uma correção não tem fixação: uma configuração antiga a recebe como uma nova, então uma página afetada por ela ainda pode mudar. Entre elas: um título na largura da página sem design próprio é medido na largura da página e composto na entrelinha do seu nível; nada é reservado sob a linha de base de uma capitular num design de título; uma capitular toma a cor de paleta da seção e é composta mesmo num texto de design cujo overflow não é 'wrap', que então quebra linhas; um parágrafo num boxe pinta o tracking com que foi medido; um boxe dividido mantém a coluna do ícone em todos os fragmentos; uma linha de boxe que esticaria os espaços além de 3× é composta em bandeira, como no texto corrido; o recurso de parágrafo frouxo nunca compõe uma linha justificada mais larga do que maxWordSpacing permite; um boxe flutuante mantém uma marginBottom (numa faixa superior) ou uma marginTop (numa faixa inferior) maior que o espaço entre flutuantes; um texto de design centralizado ou alinhado à direita com tracking (um cabeço, o título de uma abertura) é posicionado pelas suas letras, sem o tracking depois da última; sob uma abertura na largura da página, o texto que abre a segunda coluna começa onde começaria o texto logo abaixo da abertura, também quando um título segue a abertura; os pontos do sumário param antes do número de página numa fonte cujo kerning afasta uma sequência de pontos; um cabeço lê um título tal como foi escrito, sem espaço onde uma das suas linhas termina depois de um hífen ou de um travessão ou dentro de uma palavra cortada por largura (MEDIOAMBIENTALES, não MEDIOAMBIENTALE S), e o {titleText} de um design de título o lê do mesmo jeito (thousand-colour, não thousand- colour); um texto ancorado no pé ou no meio da faixa de um design de título mantém a faixa alta o bastante para contê-lo, de modo que ele não sobe mais sobre o texto acima do título; uma palavra mais larga que a sua linha, cortada junto a um hífen que ela própria tem, é cortada depois desse hífen e não recebe um segundo; o item depois de uma lista aninhada noutra recebe o itemSpacing da sua própria lista, não o da lista aninhada; e o resto de uma palavra cortada por ser mais larga que a sua linha mantém os seus próprios pontos de quebra, de modo que um endereço web continua quebrando nas suas junções, e uma palavra composta nos seus hifens, em vez de nas sílabas do dicionário.

pinLegacyMathSize multiplica a escala por 1,1312 (0,5 ÷ 0,442) e divide pelo mesmo fator as margens das fórmulas em destaque em em. Só a escala não basta para um livro com fórmulas em destaque: as margens delas são medidas do tamanho da própria fórmula, então cresceriam os mesmos 13% e empurrariam para baixo o texto abaixo delas. Escritos por extenso para as margens padrão, os três valores são:

math: {
  fontSizeScale: 1.131,
  marginTop: { value: 0.7072, unit: 'em' },
  marginBottom: { value: 0.7072, unit: 'em' },
} // fórmulas do tamanho que a 1.4 compunha, com o espaço que a 1.4 deixava em volta

Quando se sabe onde a configuração estava guardada, o motor faz isso por você: openBundle / readBundle num pacote .postext escrito antes da 1.5 (veja Pacotes escritos pelo postext 1.4 ou anterior), e o Sandbox nos livros, na cópia de trabalho e nos arquivos postext-config.json salvos naquela época (veja Sandbox → Persistência). Eles são lidos por meio de migrateConfig, que fixa o tamanho (pinLegacyMathSize): fontSizeScale passa a ser a escala guardada (1 quando não definida) × 1,1312, e uma margem de fórmula em destaque em em ou rem (uma medida do tamanho da própria fórmula) é dividida pelo mesmo fator, de modo que o espaço em volta de uma fórmula em destaque fica como a 1.4 o deixava (os 0,8 em padrão passam a 0,7072 em). Uma margem numa unidade de página (pt, mm…) fica como está, assim como uma configuração com enabled: false ou uma cujo livro não tem nenhum $. O livro antigo é então diagramado como a 1.4 o diagramava, e a sua seção math mostra o tamanho em que é composto. Para compor esse livro no tamanho atual, redefina esses valores: Fórmulas → Escala de tamanho, Margem acima (destaque) e Margem abaixo (destaque) no Sandbox, ou em código:

math: { ...config.math, fontSizeScale: 1, marginTop: undefined, marginBottom: undefined } // tamanho e margens atuais

Equações numeradas. Uma fórmula em destaque com número ocupa a sua medida (a coluna, ou a largura interna do boxe em que está): a equação fica centralizada e o seu número, alinhado à direita na linha da equação, em cada linha numerada de um align. O número vem de um \label{eq:x} (desde o postext 1.19): as fórmulas rotuladas, e as linhas rotuladas de um align, gather, alignat, flalign ou eqnarray, são numeradas em ordem de leitura com equationNumbering, a menos que uma linha diga \nonumber ou \notag. Ambientes como equation não são numerados por si sós: uma fórmula sem rótulo não tem número. \tag{…} imprime o rótulo que você der, entre parênteses, e \tag*{…}, tal como foi escrito; nenhum dos dois é contado, e um \label ao lado de um deles lhe dá um nome. Uma equação numerada mais larga que a sua medida transborda para a direita, como qualquer fórmula em destaque. (Até o postext 1.4 uma fórmula com \tag não era desenhada; até a 1.18 só \tag numerava uma fórmula.)

\eqref{eq:x} no texto imprime o número no seu format, (3), e \ref{eq:x}, o número sozinho; o mesmo fazem :ref{id="eq:x"} e @eq:x (veja Referências cruzadas), e dentro de uma fórmula \eqref o imprime como texto. Num livro diagramado capítulo por capítulo, o contador continua a partir do capítulo anterior: continuationAfter o leva em LayoutContinuation.statementCounters (equation), e o esboço do livro dá a cada rótulo o seu número (OutlineEntry.numberLabel), de modo que uma referência a uma equação de outro capítulo a imprime.

math: {
  equationNumbering: { numberingTemplate: '{h1}.{n}', resetOn: 'h1' }, // (1.1), (1.2)… (2.1)
}

O resolvedor e o redutor seguem o padrão das outras seções:

import {
  DEFAULT_MATH_CONFIG,
  resolveMathConfig,
  stripMathDefaults,
} from 'postext';
 
const resolved = resolveMathConfig(config.math);
const minimal  = stripMathDefaults(config.math);

#Inicializar o motor de fórmulas

O MathJax é carregado sob demanda, não junto com o resto do motor. Quando você diagrama na thread principal, inicialize-o antes de construir um documento que tenha fórmulas:

import { buildDocument, initMathEngine, renderPage } from 'postext';
 
await initMathEngine(); // carrega o MathJax uma vez; as chamadas seguintes resolvem na hora
const doc = buildDocument({ markdown: 'Euler: $e^{i\\pi}+1=0$.' }, config);
document.body.append(renderPage(doc.pages[0], doc));
  • Até o motor rodar, as fórmulas são marcadores de posição. Cada uma é diagramada como uma caixa cinza de tamanho estimado. Se um documento com fórmulas for diagramado sem que initMathEngine() tenha sido chamado alguma vez, o console mostra um aviso dizendo isso.
  • O worker de layout o inicializa por você. Uma construção em postext/worker chama initMathEngine() por conta própria quando o markdown contém um $.
  • Diagramar agora e de novo quando estiver pronto. isMathReady() diz se o motor está rodando. onMathReady(fn) chama fn assim que ele estiver (na hora, se já estiver) e devolve uma função que cancela a chamada. Um editor pode mostrar os marcadores de posição de imediato e diagramar de novo quando o MathJax chegar; nenhum aviso é impresso enquanto initMathEngine() está em andamento.
  • Falha. initMathEngine() rejeita quando o MathJax não pode ser carregado, e uma chamada posterior tenta de novo.
  • Qualquer bundler, Node ou uma CDN. O MathJax vem dentro do pacote como um único módulo pré-empacotado (cerca de 1,8 MB antes da compressão, baixado só por initMathEngine). O simples import { initMathEngine } from 'https://esm.sh/postext' funciona; não é preciso ?bundle. O pacote mathjax-full só é necessário para compilar o postext, então instalar o postext não o instala. O MathJax e o analisador mhchem que ele inclui têm licença Apache-2.0: os avisos deles e o texto da licença acompanham o módulo, em dist/math/THIRD_PARTY_LICENSES.txt.
  • Um motor por página. O motor e o seu cache de fórmulas renderizadas são compartilhados por tudo o que importa postext no mesmo realm JavaScript (veja Estado global compartilhado numa página).

Para a gramática do lado do documento ($...$, $$...$$, como escapar um cifrão literal), veja Formato do documento.

#Notas de rodapé

A propriedade footnotes define onde ficam as notas citadas com [^id], como são numeradas e qual é a sua aparência. A marcação é descrita em Formato do documento.

interface FootnotesConfig {
  placement?: 'column' | 'chapterEnd' | 'spread'; // No pé da coluna que cita, depois do capítulo, ou ao lado do texto de uma página dupla.
  numbering?: 'chapter' | 'document' | 'page' | 'column' | 'spread'; // Recomeça a cada capítulo, segue corrida, ou recomeça a cada página / coluna / página dupla.
  numberFormat?: string;      // decimal, lower-roman, circled-decimal (①)…
  symbols?: string[];         // Os símbolos de nota de numberFormat: 'symbols'.
  markerPosition?: 'auto' | 'superscript' | 'inline' | 'side' | 'right'; // Elevado, na linha de base, ao lado da palavra, ou à direita de uma linha vertical.
  markerSize?: Dimension;     // Tamanho do marcador em linha, lateral ou à direita; em é o texto ao redor.
  numberGap?: 'en' | 'em';    // Espaço depois do número da própria nota.
  chapterEndAlign?: 'foot' | 'text';   // chapterEnd: notas no pé da coluna ou logo abaixo do texto.
  fontSize?: Dimension;       // Tamanho das notas; em é o tamanho do corpo.
  lineHeight?: Dimension;     // Entrelinha das notas; em é o tamanho da nota.
  color?: ColorValue;         // Cor das notas; a cor do corpo quando não definida.
  textAlign?: TextAlign;      // O alinhamento do corpo quando não definido.
  hangingIndent?: Dimension;  // Recuo das linhas seguintes de uma nota.
  spaceBetween?: Dimension;   // Espaço entre duas notas.
  spaceAbove?: Dimension;     // Espaço entre o texto e o fio; em é o tamanho do corpo.
  spaceBelowRule?: Dimension; // Espaço entre o fio e a primeira nota.
  separator?: {
    enabled?: boolean;        // Desenha o fio.
    width?: number;           // Comprimento do fio, uma fração da largura da coluna.
    lineWidth?: Dimension;    // Espessura do fio.
    color?: ColorValue;       // A cor das notas quando não definida.
  };
}
PropriedadeTipoPadrãoDescrição
placement'column' | 'chapterEnd' | 'spread''column' (livros japoneses em escrita vertical: 'chapterEnd')'column' compõe cada nota no pé da coluna que contém a linha que a cita, sob um fio curto; num layout de uma coluna, é o pé da página. 'chapterEnd' compõe todas as notas de um capítulo depois do seu último bloco, em ordem de citação. 'spread' (傍注, só texto vertical) compõe as notas citadas nas duas páginas de uma página dupla no fim da sua página ímpar, a página da esquerda de um livro com lombada à direita, sob o fio: as notas citadas na página par esperam por ela; uma nota que deixaria a página ímpar com menos de uma linha de texto fica no pé da página par, com as que vêm depois dela; e um capítulo que termina numa página par mantém ali as notas que estavam esperando. Em texto horizontal recai em 'column', com um aviso unknownConfigValue. Desde o postext 1.16.
numbering'chapter' | 'document' | 'page' | 'column' | 'spread''chapter' (livros japoneses em escrita horizontal: 'page'; com placement: 'spread': 'spread'; com numberFormat: 'symbols' no pé da coluna: 'page')'chapter' recomeça em 1 sob cada título de nível 1 e no início de cada documento. 'document' segue corrida pelo documento e, num livro diagramado capítulo por capítulo, de um capítulo para o seguinte (continuationAfter leva o último número como continuation.footnoteNumber). 'page' recomeça em 1 a cada página e 'column' a cada coluna, contando as notas onde o layout as compõe (as colunas de uma página em ordem de leitura): o habitual 页下注 de um livro chinês. O documento é diagramado, numerado onde as suas notas caíram e diagramado de novo até os números se manterem (no máximo três construções a mais). Os dois se aplicam a notas no pé da coluna: com placement: 'chapterEnd' as notas são numeradas por capítulo. 'spread' recomeça a cada página dupla (páginas 2–3, 4–5…), em ordem de citação, para placement: 'spread'.
numberFormatstring'decimal'Como os números são escritos, em qualquer grafia que as configurações de numeração aceitam: 'decimal', 'lower-roman', 'lower-alpha', 'circled-decimal' (ou '①'), 'cjk-decimal', '一'… O marcador e o número que abre a nota usam esse formato. circled-decimal escreve em decimal os números acima de 50. Um nome desconhecido numera em decimal, com um aviso unknownNumberFormat. 'symbols' (ou '*') marca as notas com símbolos de referência, symbols em sequência: * † ‡ § ‖ ¶, depois dobrados (** †† ‡‡…) e triplicados; as notas são então contadas de novo a cada página, a menos que numbering seja definido. Desde o postext 1.19.
symbolsstring[]['*', '†', '‡', '§', '‖', '¶']A sequência que numberFormat: 'symbols' escreve, dobrada e depois triplicada quando se esgota. Os arquivos latinos do Google Fonts e do Fontsource trazem * † § ¶ (a adaga em latin-ext), mas não ‡ nem ‖, que o PDF então desenha como o glifo .notdef da fonte, com um aviso missingGlyph: com uma fonte assim, deixe-os de fora (['*', '†', '§', '¶']) ou componha as notas numa fonte que os tenha. Strings vazias são descartadas, e uma lista vazia mantém o padrão. Desde o postext 1.19.
markerPosition'auto' | 'superscript' | 'inline' | 'side' | 'right''auto''superscript' eleva o marcador no texto e o número que abre a nota, em tamanho reduzido. 'inline' os compõe na linha de base: o marcador em markerSize, o número da nota no tamanho da nota; em texto vertical, um marcador circulado em linha fica em pé numa célula própria. 'right' compõe um marcador reduzido rente ao lado direito de uma linha vertical, como os livros japoneses verticais compõem (1); em texto horizontal é um sobrescrito. 'side' (合印) compõe um marcador pequeno ao lado da palavra marcada, do lado do seu rubi, terminando onde termina o último caractere da palavra; não ocupa espaço na linha, que quebra e justifica como se ele não estivesse ali, e um espaço entre linhas estreito demais para ele é relatado como rubyExceedsLeading. 'auto' é em linha para circled-decimal, 'right' num livro japonês vertical e sobrescrito nos demais casos. De qualquer forma, o marcador fica junto do caractere anterior e nunca abre uma linha.
markerSizeDimension1em; 0.6em para 'side', 0.7em para 'right'Tamanho de um marcador em linha, lateral ou à direita; em é o tamanho do texto ao redor (0.75em é uma redução comum). Sem efeito num marcador sobrescrito.
numberGap'en' | 'em''en' (notas japonesas depois do capítulo: 'em')O espaço depois do número que abre uma nota: um espaço meia-risca ou um eme inteiro da nota (um espaço ideográfico quando o número ou a nota contém texto CJK), nunca esticado nem quebrado. Desde o postext 1.16.
markerTemplatestring'{n}' (livros japoneses em escrita vertical: '({n})')Como se escreve o número de uma nota, com {n} no lugar dele, no formato de número e nos algarismos do documento: '({n})' dá os marcadores entre parênteses dos livros árabes, «(١)». Escreve tanto o marcador no texto quanto o número que abre a nota. Um modelo sem {n} é lido como o padrão.
noteNumberPosition'auto' | 'superscript' | 'inline''auto'Onde fica o número que abre a nota: elevado ou na linha, no tamanho da nota. 'auto' segue markerPosition. Os livros árabes elevam o marcador no texto e compõem o número da própria nota na linha.
chapterEndAlign'foot' | 'text''foot'Com placement: 'chapterEnd': 'foot' compõe no pé da coluna as notas que a fecham, com as linhas que sobram entre o texto e as notas, como ficam as notas no pé da coluna. 'text' as compõe logo abaixo do texto.
fontSizeDimension0.8emTamanho do texto das notas. em e rem são o tamanho do corpo. As notas usam a família e os pesos do corpo.
lineHeightDimension1.25emEntrelinha do texto das notas; em é o tamanho da nota. As notas ficam fora da grade de linhas de base: elas se empilham a partir do pé da coluna, e o texto acima delas continua na grade.
colorColorValuecor do corpoCor do texto das notas.
textAlignTextAlignalinhamento do corpoAlinhamento do texto das notas.
hangingIndentDimension0Recuo da segunda linha em diante de uma nota, para que se alinhem depois do seu número.
spaceBetweenDimension0Espaço entre duas notas.
spaceAboveDimension0.5emEspaço entre a última linha do texto e o fio; em é o tamanho do corpo. Com 'chapterEnd' e chapterEndAlign: 'text', spaceAbove + spaceBelowRule é o espaço entre o texto e a primeira nota.
spaceBelowRuleDimension0.4emEspaço entre o fio e a primeira nota.
separator.enabledbooleantrueDesenha o fio acima das notas de cada coluna. Com false, os espaços acima continuam.
separator.widthnumber0.3Comprimento do fio como fração da largura da coluna (0–1), a partir da borda esquerda da coluna.
separator.lineWidthDimension0.5ptEspessura do fio.
separator.colorColorValuecor das notasCor do fio.
footnotes: {
  fontSize: { value: 7.5, unit: 'pt' },
  lineHeight: { value: 9.5, unit: 'pt' },
  hangingIndent: { value: 0.8, unit: 'em' },
  separator: { width: 0.25, lineWidth: { value: 0.4, unit: 'pt' } },
}

Livros japoneses. Um documento japonês (locale: 'ja', desde o postext 1.16) dá aos campos que deixa sem definir os valores do JLReq §4.2. Um livro vertical compõe as notas depois do capítulo (placement: 'chapterEnd', 後注), numeradas por capítulo, com marcadores ({n}) à direita da linha (markerPosition: 'right', algarismos em pé por cjk.uprightDigits); um horizontal as compõe no pé da página, numeradas por página, com marcadores sobrescritos. Os dois desenham o fio com um terço da medida (separator.width: 1/3), e as notas depois do capítulo recebem numberGap: 'em' e um recuo deslocado de dois emes. Um valor explícito prevalece e é mantido ao salvar (stripFootnotesDefaults compara com os padrões do próprio documento); documentos em chinês, árabe e línguas latinas se resolvem como antes. footnoteDocumentDefaults(locale, writingMode, placement?) devolve esses valores. Escreva o marcador antes de um 。 de fim de frase (先生[^1]。): ele fica junto do caractere anterior, e 。 nunca abre uma linha. Veja Composição em japonês › Notas.

Como as notas são compostas no pé da coluna:

  • Nota e citação dividem uma coluna. Antes de posicionar uma linha, o layout soma a altura das notas que essa linha cita pela primeira vez (e o fio, na primeira nota da coluna). Uma linha cujas notas não cabem abaixo dela vai para a coluna seguinte com o resto do seu parágrafo, conforme as regras de órfãs e viúvas. A área de texto da coluna diminui na altura das notas, então o balanceamento de colunas e a faixa final de um capítulo contam só o texto.
  • Várias notas numa coluna se empilham em ordem de citação sob um único fio. Uma nota citada de novo mais adiante mantém o seu número e não é composta outra vez.
  • Flutuantes inferiores. Uma figura que ocupa o pé de uma coluna depois que as notas dela foram compostas fica acima delas; as notas compostas depois da figura ficam acima dela.
  • Boxes. Uma nota citada dentro de um boxe (em linha, flutuante ou fixo) vai para o pé da coluna em que o texto depois do boxe continua, em geral a mesma coluna. Um boxe que fecha o documento deixa as suas notas no pé da coluna em que o texto terminou.
  • Limites. Uma nota nunca é dividida: uma nota mais alta que uma coluna transborda dela. Os marcadores em legendas, células de tabela e títulos não são lidos (saem impressos como foram escritos).
  • Saída. Canvas, HTML e PDF pintam as notas, os marcadores (um número sobrescrito) e o fio. No PDF cada marcador tem um link para a sua nota, e um PDF etiquetado compõe cada nota como um elemento Note com um /ID único listado na /IDTree da árvore de estrutura (PDF/UA-1). As notas são VDTBlocks com footnoteNote definido, em page.floats; os fios ficam em page.footnoteAreas.
  • Avisos. undefinedFootnote (um marcador sem definição: o número é impresso sobre uma nota vazia) e unusedFootnote (uma definição que nenhum marcador cita: não é composta).

O resolvedor e o redutor seguem o padrão das outras seções:

import { DEFAULT_FOOTNOTES_CONFIG, resolveFootnotesConfig, stripFootnotesDefaults, footnoteDocumentDefaults } from 'postext';
 
resolveFootnotesConfig(config.footnotes, 'ja', 'vertical-rl'); // os campos não definidos recebem os padrões japoneses verticais

#Referências cruzadas

A propriedade crossRefs define as palavras que uma referência cruzada imprime em volta de um número ou de uma página, e o estilo que um :ref assume quando não define nenhum. Cada modelo contém {n} onde vai o número; um modelo sem ele recebe o número depois de um espaço inseparável ("§" imprime § 3.2). Um modelo não definido segue o idioma do documento: chapter / section / p. em inglês, capítulo / sección / pág. em espanhol, 第{n}章 / 第{n}节 / 第{n}页 em chinês, e assim por diante em francês, alemão, italiano, português, catalão e neerlandês.

interface CrossRefsConfig {
  chapter?: string;  // Palavras em volta do número de um título de nível 1: "chapter {n}".
  section?: string;  // Em volta do número de qualquer outro título: "section {n}".
  page?: string;     // Em volta de um número de página: "p. {n}".
  defaultStyle?: 'default' | 'number' | 'title' | 'page'; // Um :ref sem style=.
}
PropriedadeTipoPadrãoDescrição
chapterstringconforme o idiomaUma referência a um título de nível 1: "chapter {n}". O número de um título cujo modelo já escreve a palavra (Chapter {1}, 第{1:一}章) é impresso como está.
sectionstringconforme o idiomaUma referência a um título de nível 2 a 6: "section {n}", "§ {n}".
pagestringconforme o idiomaUma referência de página (style=page): "p. {n}", "page {n}".
defaultStyle'default' | 'number' | 'title' | 'page''default'O que um :ref a um título ou a uma âncora imprime sem style=. 'default': um título numerado, pela sua palavra e número; um não numerado, pelo seu título; uma âncora, pelo seu texto. Uma referência que define style o mantém, e as referências a figuras e tabelas não são afetadas.

As referências recebem a cor, o peso e a inclinação de todas as referências (bodyText.referenceColor, referenceBold, referenceItalic).

#Citações

A propriedade citations escolhe o estilo de citação e a aparência das citações e da bibliografia. A marcação é descrita em Citações e bibliografia; o estilo é aplicado pelo pacote postext-citeproc.

interface CitationsConfig {
  style?: string;          // 'apa', 'ieee', 'chicago-notes-bibliography'… ou 'custom'
  customStyle?: string;    // um estilo CSL completo (XML .csl), usado com style: 'custom'
  locale?: string;         // locale CSL; o idioma do documento quando não definido
  link?: boolean;          // as citações têm link para as suas entradas
  marker?: 'style' | 'brackets' | 'parentheses' | 'superscript' | 'corner';
  collapseRanges?: boolean;
  notes?: 'footnote' | 'warichu';
  numbering?: 'book' | 'chapter';
  bibliography?: {
    title?: string;        // não definido: a palavra do idioma do documento; '' ou ' ': nenhum
    scope?: 'book' | 'chapter';
    auto?: boolean;
    fontSize?: Dimension;
    lineHeight?: Dimension;
    hangingIndent?: Dimension;
    entrySpacing?: Dimension;
    labelWidth?: Dimension;
    labelAlign?: 'left' | 'right';
    doi?: 'link' | 'text' | 'hide';
    includeUncited?: boolean;
    groupByLanguage?: boolean;
  };
}
PropriedadeTipoPadrãoDescrição
stylestring'apa'O id de um estilo incluído (veja Estilos) ou 'custom'. O estilo decide o que as citações e as entradas dizem: nomes, datas, ordem, pontuação e se as citações são notas.
customStylestring—Um estilo CSL completo, o XML de um arquivo .csl, usado quando style é 'custom'. O Sandbox o carrega de um arquivo.
localestringidioma do documentoO locale CSL em que o estilo escreve as suas palavras (en-US, es-ES, zh-CN, zh-TW, ja-JP…). Um documento japonês é lido como ja-JP: as citações narrativas unem dois autores com と e abreviam mais autores com ほか.
linkbooleantrueUma citação tem link para a sua entrada na bibliografia (link no PDF, âncora no HTML, clique no Sandbox).
marker'style' | 'brackets' | 'parentheses' | 'superscript' | 'corner''style'Como um estilo numérico marca uma citação: como o estilo a escreve, [1], (1), um sobrescrito ou 〔1〕 (em pé no texto vertical). Um localizador vem depois do número.
collapseRangesbooleantrueNúmeros consecutivos como intervalo: 1–3 num marcador próprio, [2]–[4] no do IEEE. false os mantém separados.
notes'footnote' | 'warichu''footnote'Onde um estilo de notas compõe as suas citações: notas de rodapé (posicionadas e numeradas como footnotes diz) ou notas em duas linhas dentro da linha (夹注).
numbering'book' | 'chapter''book'Citações ao longo do livro, ou cada capítulo por si (cada documento, e cada título de nível 1 depois de uma citação): um estilo numérico numera cada capítulo a partir de 1, e uma obra citada em dois capítulos recebe o número de cada capítulo; um estilo de notas escreve uma obra por completo na primeira citação em cada capítulo. Pensado para bibliography.scope: 'chapter', cujas listas então recebem os números do capítulo.
bibliography.titlestringconforme o idiomaTítulo acima da lista, um parágrafo em negrito. Em branco: nenhum. Um título seu vai acima de :::bibliography.
bibliography.scope'book' | 'chapter''book'Uma lista com todas as obras que o livro cita, ou uma por capítulo com as obras que ele cita. Num documento de vários capítulos, cada H1 inicia uma nova lista de capítulo; com auto, um capítulo que não posiciona nenhum :::bibliography recebe a sua lista no fim.
bibliography.autobooleantrueCompõe a lista depois do texto (o último capítulo, numa lista do livro inteiro) quando nenhum :::bibliography a posiciona.
bibliography.fontSizeDimension0.9emTamanho das entradas; em é o tamanho do corpo.
bibliography.lineHeightDimensionentrelinha do corpoEntrelinha das entradas.
bibliography.hangingIndentDimension2emRecuo das linhas seguintes de uma entrada não numerada.
bibliography.entrySpacingDimension0.3emEspaço entre duas entradas.
bibliography.labelWidthDimensionrótulo mais longoLargura da coluna em que ficam os números de uma lista numerada: o texto de cada entrada começa a essa distância, tanto na primeira linha quanto nas seguintes, de modo que 9. e 10. dividem a coluna. O rótulo continua fazendo parte do texto da entrada.
bibliography.labelAlign'left' | 'right''left'Onde um rótulo fica na sua coluna: junto à borda esquerda dela ou junto ao texto (9. e 10. terminam juntos).
bibliography.doi'link' | 'text' | 'hide''link'DOIs e URLs como links, como texto simples ou omitidos.
bibliography.includeUncitedbooleanfalseLista todas as referências, citadas ou não (como nocite: "@*").
bibliography.groupByLanguagebooleanfalseAs obras em chinês, japonês e coreano primeiro, depois as demais. Só para estilos autor-data e autor-página: uma lista numerada mantém a ordem dos seus números.

#Tipografia do Leste Asiático

A propriedade cjk define como se compõe o texto em chinês, japonês e coreano: que convenções regionais ele segue, onde suas linhas podem quebrar, que largura ocupa a pontuação e se ela fica pendente, o espaço entre han e latim, a grade de caracteres da mancha e como se imprimem as marcas de ênfase, as linhas laterais, as leituras em rubi, as notas warichu e as marcas de kanbun do texto. Todos os campos são opcionais, e 'auto' segue a região do idioma do documento (locale), de modo que um livro com locale: 'zh-Hant' quebra as linhas e compõe a pontuação à maneira de Taiwan, e um com locale: 'ja' à maneira japonesa, sem definir mais nada. Composição chinesa e Composição japonesa explicam as regras por trás desses ajustes, com duas configurações completas cada uma. Os campos e valores japoneses existem desde o postext 1.16.

interface CjkConfig {
  region?: 'auto' | 'mainland' | 'taiwan' | 'hongkong' | 'japan'; // Convenções regionais.
  lineBreak?: 'auto' | 'none' | 'basic' | 'gb' | 'strict'
    | 'ja-very-strict' | 'ja-strict' | 'ja-loose';      // Que sinais não podem abrir nem fechar uma linha.
  punctuationWidth?: 'auto' | 'fullwidth' | 'kaiming' | 'lineEndHalf' | 'halfwidth';
  compressAdjacent?: 'auto' | boolean;      // Dois sinais vizinhos ocupam 1.5 em.
  trimLineStart?: 'auto' | boolean;         // Parênteses na borda da linha perdem a metade externa.
  hangingPunctuation?: 'auto' | 'none' | 'allow' | 'force';
  spaceAfterQuestion?: 'auto' | boolean;    // Um eme depois de ?! dentro do parágrafo (Japão).
  paragraphStartBracket?: 'auto' | 'indent' | 'half' | 'flush'; // Parêntese que abre um parágrafo com recuo.
  wordBreak?: 'normal' | 'keep-all';        // keep-all: quebrar só nos espaços (分かち書き, coreano).
  latinSpacing?: Dimension;                 // Entre han e latim; padrão 0.25 em.
  uprightDigits?: 0 | 2 | 3 | 4;            // Texto vertical: números numa célula em pé; padrão 2.
  grid?: { enabled?: boolean; charsPerLine?: number; linesPerPage?: number; show?: boolean };
  emphasis?: 'auto' | 'italic' | 'dots';    // O que *…* faz com os caracteres CJK.
  emphasisMark?: {                          // A marca que *…* e :dots[…] desenham quando não a indicam.
    style?: 'auto' | 'dot' | 'circle' | 'sesame';
    fill?: 'auto' | 'filled' | 'open';
    position?: 'auto' | 'over' | 'under';
  };
  bookTitleMark?: 'auto' | 'brackets' | 'wavy' | 'none'; // O que :book[…] imprime.
  bookTitleBrackets?: 'auto' | { open: string; close: string }[]; // Os parênteses, do mais externo para dentro.
  annotationColor?: ColorValue;             // Pontos e linhas de nome/título; padrão a cor do texto.
  ruby?: {
    fontFamily?: string; fontSize?: Dimension; color?: ColorValue;
    position?: 'auto' | 'over' | 'under' | 'right';
    overhang?: 'auto' | 'none' | 'kana' | 'any'; // Quanto uma leitura pode avançar sobre os vizinhos.
    align?: 'auto' | 'center' | 'jis' | 'start'; // Como se espaça uma leitura mais curta que a base.
    smallKana?: 'keep' | 'full';                 // Kana pequenos nas leituras como escritos, ou em tamanho cheio.
  };
  warichu?: { fontSize?: Dimension; color?: ColorValue; open?: string; close?: string };
  kunten?: { fontSize?: Dimension; color?: ColorValue; placement?: 'inline' | 'interlinear' }; // Marcas de kanbun.
}
PropriedadeTipoPadrãoDescrição
region'auto' | 'mainland' | 'taiwan' | 'hongkong' | 'japan''auto'As convenções que o texto segue, conforme as regiões descritas nos Requirements for Chinese Text Layout (clreq) do W3C. 'auto' lê a região de locale, ou de bodyText.hyphenation.locale quando locale não está definido: zh, zh-Hans, zh-CN e zh-SG dão 'mainland'; zh-Hant e zh-TW dão 'taiwan'; zh-HK e zh-MO dão 'hongkong'; ja e toda etiqueta ja-* dão 'japan', cujas convenções seguem os Requirements for Japanese Text Layout (JLReq) do W3C; qualquer outro idioma dá 'mainland'. A região escolhe os padrões de todos os campos 'auto' abaixo. 'japan' definido à mão compõe qualquer documento à maneira japonesa; os textos embutidos e os padrões das notas e do índice remissivo continuam seguindo o idioma.
lineBreak'auto' | 'none' | 'basic' | 'gb' | 'strict' | 'ja-very-strict' | 'ja-strict' | 'ja-loose''auto'Que sinais não podem abrir nem fechar uma linha (clreq §6.1.1 e Apêndice C.3 do JLReq; veja as tabelas abaixo). 'auto': 'gb' para a China continental, 'basic' para Taiwan e Hong Kong, 'ja-very-strict' para o Japão. Qualquer nível pode ser escolhido em qualquer documento.
punctuationWidth'auto' | 'fullwidth' | 'kaiming' | 'lineEndHalf' | 'halfwidth''auto'A largura com que se compõem os sinais de largura cheia (veja Largura da pontuação). 'auto': 'kaiming' para a China continental, 'fullwidth' para Taiwan, Hong Kong e Japão (no Japão com as regras do JLReq para pares, fins de linha e pontos médios).
compressAdjacent'auto' | boolean'auto'Dois sinais que se encontram (。」, 》(, :“) cedem o meio eme de branco entre eles, e o par ocupa 1,5 eme em vez de 2. 'auto': ativado para a China continental, Hong Kong e Japão, desativado para Taiwan.
trimLineStart'auto' | boolean'auto'Um parêntese ou aspa de abertura que inicia uma linha cede seu meio eme anterior, e um de fechamento que termina uma linha cede o meio eme posterior. 'auto': ativado para a China continental, Hong Kong e Japão, desativado para Taiwan. No Japão, um sinal de fechamento no fim da linha conserva seu meio eme até que a linha precise dele (veja Largura da pontuação).
hangingPunctuation'auto' | 'none' | 'allow' | 'force''auto'Se um sinal de pausa ou de fim de frase pode ficar pendente além do fim da linha (veja Pontuação pendente). 'auto': 'allow' no Japão (só 、。,.), 'none' nas demais regiões. Até o postext 1.15 o padrão era 'none', que é o que 'auto' continua sendo fora do Japão.
spaceAfterQuestion'auto' | boolean'auto'Um eme de espaço depois de um ? ou ! de largura cheia (também ‼⁇⁈⁉, e depois de uma chamada de nota colada a eles) dentro de um parágrafo, como o japonês compõe (JLReq §3.1.6): não antes de um parêntese de fechamento ou de outro sinal desses, não no fim do parágrafo, e suprimido no fim da linha. Um U+3000 ou um espaço digitado pelo autor nesse ponto passa a ser esse espaço. Ele nunca estica nem encolhe. 'auto': ativado no Japão, desativado nas demais regiões.
paragraphStartBracket'auto' | 'indent' | 'half' | 'flush''auto'Como se compõe um parêntese de abertura que inicia um parágrafo com recuo de primeira linha (JLReq §3.1.5): 'indent' mantém o recuo e dá ao parêntese seu eme depois dele (padrão ①); 'half' faz o parêntese ceder o branco anterior ao glifo, que passa a ocupar a segunda metade de um recuo de um eme, e o texto começa onde começa o texto dos outros parágrafos (padrão ③); 'flush' põe o parêntese na borda, sem recuo (天付き). Um parágrafo sem recuo ou com recuo deslocado não é alterado. 'auto': 'half' no Japão; nas demais regiões o parêntese é composto como em qualquer início de linha, como antes.
wordBreak'normal' | 'keep-all''normal'Onde uma linha quebra entre dois caracteres. 'keep-all' quebra só num espaço (U+0020, U+3000) e junto à pontuação onde lineBreak permite (depois de 、。」, antes de 「), nunca entre duas letras (kana, kanji, hangul, latinas): serve para kana escrito com espaço entre as frases (分かち書き), como nos livros ilustrados e nas cartilhas, e para o coreano. Uma frase mais longa que a linha é quebrada por dentro, como 'normal' a quebraria. Um estilo de parágrafo pode definir o seu (wordBreak). Desde o postext 1.16.
latinSpacingDimension{ value: 0.25, unit: 'em' }O espaço entre um caractere han e uma letra ou algarismo latino vizinho, em emes do corpo do texto CJK ou em qualquer medida; 0 o desativa (veja Espaço entre han e latim).
uprightDigits0 | 2 | 3 | 42Texto vertical: um número de no máximo essa quantidade de algarismos fica em pé numa única célula (tate-chu-yoko), a menos que esteja numa frase latina, cujas palavras ele então acompanha deitado; 0 o desativa (veja Números em texto vertical).
grid{ enabled?, charsPerLine?, linesPerPage?, show? }desativadoA mancha em caracteres por linha e linhas por página (veja Grade de caracteres).
emphasis'auto' | 'italic' | 'dots''auto'O que a ênfase do Markdown (…) faz com os caracteres chineses e japoneses: 'dots' põe uma marca de ênfase junto a cada um, como faz :dots[…] (com a forma e o lado de emphasisMark), e as letras latinas dentro da mesma ênfase mantêm o itálico; 'italic' os inclina, o que uma fonte CJK só consegue simular. 'auto': 'dots' quando o idioma do documento é chinês ou japonês, 'italic' nos demais casos (veja Marcas, rubi e warichu).
emphasisMark{ style?, fill?, position? }'auto' em cada umA marca que … e um :dots[…] sem atributos desenham: style 'dot', 'circle' ou 'sesame' (﹅); fill 'filled' ou 'open' (auto: vazado para o círculo); position 'over' (à direita do texto vertical) ou 'under' (à esquerda dele). Auto: no Japão um gergelim cheio sobre o texto, e à direita dele no texto vertical (JLReq §3.3.9); nas demais regiões um ponto sob o texto, e à direita dele no texto vertical, como antes. Os atributos da própria marca prevalecem.
bookTitleMark'auto' | 'brackets' | 'wavy' | 'none''auto'O que :book[…] imprime: parênteses em volta do título (bookTitleBrackets), a linha ondulada de título de obra sob ele, ou o título sem nada. 'auto': parênteses para a China continental e o Japão, a linha ondulada para Taiwan e Hong Kong.
bookTitleBrackets'auto' | { open, close }[]'auto'Os parênteses de :book[…] com 'brackets', do mais externo para dentro; um título aninhado mais fundo do que a lista alcança usa o último par. 'auto' (ou uma lista vazia): 『』 e depois 「」 no Japão, 《》 e depois 〈〉 nas demais regiões.
annotationColorColorValuea cor do textoCor dos pontos de ênfase e das linhas de nome próprio e de título de obra; o padrão das cores do rubi e do warichu.
ruby{ fontFamily?, fontSize?, color?, position?, overhang?, align?, smallKana? }meio corpo, 'auto'As leituras de :ruby[…] e {紅樓|hóng|lóu}: fonte (por padrão a do texto), corpo (por padrão { value: 0.5, unit: 'em' } do texto; zhuyin a 60 % disso), cor e onde ficam quando o rubi não indica ('auto': zhuyin à direita de cada caractere, pinyin e kana sobre o texto no texto horizontal e à direita dele no texto vertical). overhang: quanto uma leitura mais longa que sua base pode avançar sobre os vizinhos: 'kana' um caractere de rubi sobre kana, ー e o lado vazio de um sinal, meio sobre um parêntese de abertura, nada sobre kanji (JLReq §3.3.8); 'any' meio caractere de rubi sobre qualquer vizinho sem leitura; 'none' nada; auto 'kana' no Japão, nas demais regiões um quarto do corpo do rubi sobre qualquer vizinho sem leitura, como antes. align: como se espaça uma leitura mais curta que sua base: 'jis' 1:2:1 (meia unidade nas pontas, uma entre os caracteres), 'center' sem espaçamento e centralizada, 'start' a partir do início da base; auto 'jis' no Japão, 'center' nas demais regiões. smallKana: 'full' compõe em tamanho cheio os kana pequenos das leituras ('keep', o padrão, os mantém como escritos). O mode e o align do próprio rubi prevalecem.
warichu{ fontSize?, color?, open?, close? }meio corpo; parênteses () no Japão, nenhum nas demais regiõesAs notas em duas linhas de :warichu[…]: o corpo da nota (por padrão meio eme, para que as duas linhas preencham o eme da linha), sua cor e os parênteses compostos no corpo do texto antes da primeira linha e depois da última (o open / close da própria nota prevalecem). No Japão, um open ou close vazio ('') compõe a nota sem esse parêntese.
kunten{ fontSize?, color?, placement? }meio corpo, 'inline'As marcas de kanbun de :kunten[…] (返り点, 送り仮名, 竪点): seu corpo (por padrão { value: 0.5, unit: 'em' }, JIS X 4051 §5.5), sua cor (por padrão annotationColor, senão a do texto) e placement: 'inline' compõe as marcas de retorno depois do seu caractere, ocupando meio eme, como a JIS as compõe; 'interlinear' põe todas as marcas no espaço entre as linhas, sem ocupar avanço. Igual em todas as regiões (veja Marcas, rubi e warichu).
NívelNunca no início de uma linhaNunca no fim de uma linha
noneNada: uma linha pode quebrar entre dois caracteres quaisquer, como fazem os jornais de Taiwan e de Hong Kong.Nada.
basicSinais de pausa e de fim de frase 、,;:。!?.‼⁇⁈⁉; aspas de fechamento ” ’ 」 』 e parênteses e colchetes de fechamento )〕]}】〗》〉; conectores – ~ ~ e um travessão — isolado entre duas palavras; pontos médios · ‧ ・; marcas de iteração 々〻ゝゞヽヾ e ー; unidades de número % ‰ ° ℃ % e os quadrados de unidade ㎡ ㎏ ㏄.Aspas de abertura “ ‘ 「 『 e parênteses e colchetes de abertura (〔[{【〖《〈; símbolos de moeda ¥ $ € £.
gbbasic, mais a barra / / (GB/T 15834—2011 §5.1.9).basic, mais a barra.
strictgb, mais o travessão duplo —— ⸺ e as reticências …… ⋯⋯.gb.

Os níveis japoneses seguem as três convenções do Apêndice C.3 do JLReq. Em todos eles nenhuma linha termina com um parêntese ou uma aspa de abertura, um símbolo de moeda fica com seu número (exceto em ja-loose), a barra não tem regra, e ——, ―― e …… podem abrir uma linha mas nunca se separam (nem 〳〵):

NívelNunca no início de uma linha
ja-very-strictParênteses e aspas de fechamento; 、,。.; os hífens ‐ – ゠ 〜 ~; ?!‼⁇⁈⁉; ・:;; as marcas de iteração ゝゞヽヾ〻 e 々; ー; os kana pequenos ぁぃぅぇぉっゃゅょゎゕゖ ァィゥェォッャュョヮヵヶ ㇰ–ㇿ (e suas formas de meia largura); as unidades % ℃. O padrão da JIS X 4051.
ja-strictja-very-strict, menos os kana pequenos, ー e 々 (os livros em geral, segundo o JLReq).
ja-looseSó parênteses e aspas de fechamento, 、, e 。. (jornais).

Em todos os níveis:

  • —— e …… formam uma unidade de dois emes que nunca se separa; duas unidades assim seguidas podem se separar entre si.
  • Um número conserva seus sinais e sua unidade (¥5,999, 50%, 50%, 120㎡), também com um espaço no meio (−3 ℃, 50 %), e uma palavra latina fica inteira: o texto ocidental entre caracteres CJK é composto como um trecho que nenhuma linha quebra por dentro, a não ser que o trecho sozinho seja mais largo que a linha (nesse caso ele é dividido numa sílaba, com hífen, ou no último caractere que cabe; um endereço web, nas suas junções). Os quadrados de unidade ㎡ ㎏ ㎞ ㏄ (U+3371–337A, U+3380–33DF, U+33FF) pertencem ao trecho do seu número e não tornam CJK um parágrafo latino.
  • Um número ou uma palavra em algarismos e letras de largura cheia (123456, 50%, ¥599, 3.14, 12:30, ABC) também nunca se divide; uma linha justificada espaça seus caracteres como espaça os han.
  • Um espaço entre palavras só é ponto de quebra onde as regras o permitem: a linha não quebra ali quando o caractere seguinte não pode abrir uma linha (参见图表 ( 第三章 ) nunca abre uma linha com )) ou quando o último antes dele não pode fechá-la.
  • Uma chamada de nota, um :ref e um sobrescrito ou subscrito ficam com o caractere anterior.
  • O espaço ideográfico U+3000 é um caractere de um eme de largura: uma linha pode quebrar depois dele, nunca antes; ele nunca é esticado nem suprimido no início de uma linha. Para o recuo de dois caracteres dos parágrafos chineses, defina bodyText.firstLineIndent: { value: 2, unit: 'em' } em vez de digitar dois U+3000 (o analisador descarta os que abrem um parágrafo).

Quando um caractere não cabe na linha e pode abrir a seguinte, ele desce, e uma linha justificada distribui o que sobra. Quando não pode abrir a linha seguinte (uma vírgula, um parêntese de fechamento, o caractere depois de um de abertura), a linha tenta primeiro acomodá-lo comprimindo-se (push-in, clreq §6.2.2.3): se o branco que ela pode ceder (veja Largura da pontuação) cobre o excesso, o caractere (com os sinais que precisam ficar com ele, uma aspa de fechamento depois de um ponto final) entra na linha e a linha é ajustada à medida. Caso contrário, a linha cede caracteres a ele (push-out): a quebra recua até o último ponto que as regras permitem, e uma linha justificada distribui o que sobra. Numa linha mais estreita que um número com seu ponto final, nada pode terminar a linha, e ela acaba quebrando antes do ponto final.

A que parágrafos isso se aplica: aos que têm mais caracteres CJK (han, kana, bopomofo, hangul) do que espaços entre palavras, mesmo sem dois deles seguidos (第1条、第2条, 价¥5,999。好). Um espaço junto a um caractere ou sinal CJK não é um espaço entre palavras: 2026 年 9 月 28 日 é composto como 2026年9月28日. Esses parágrafos são compostos linha a linha, tanto no caminho simples quanto no formatado, de modo que um parágrafo com uma palavra em negrito quebra exatamente como o mesmo texto sem ela. Um parágrafo latino que cita um título ou um nome chinês tem mais espaços que caracteres: ele mantém a quebra de linhas ótima, e uma linha pode quebrar junto aos seus caracteres CJK pelas mesmas regras. Legendas, células de tabela, notas e boxes também quebram no nível do documento. A hifenização por dicionário não alcança as palavras latinas de um parágrafo CJK.

Uma linha CJK justificada que não é a última do parágrafo é espaçada até a medida nesta ordem (clreq §6.2.2.4): os espaços entre palavras ocidentais, até meio eme cada um; os espaços entre han e latim, até meio eme cada um; depois todos os intervalos entre caracteres, e esses espaços, por igual. Nenhum espaço entra numa palavra latina, num número ou num sinal de dois emes, nem junto a um conector ou a uma barra. O espaçamento é definido por segmento da linha (VDTLineSegment.tracking, px depois de cada caractere, incluídos no width do segmento), que o canvas, o HTML e o PDF pintam como espaçamento entre letras; uma palavra latina seguida de um intervalo dá à sua última letra um segmento próprio. Um segmento contém um trecho ocidental, ou caracteres de um mesmo estilo, link e espaçamento que avançam igual, por isso o Sandbox distribui sua largura por igual entre eles ao posicionar o cursor, e um link cobre só os seus próprios caracteres. Quando uma linha precisaria de mais de meio eme entre os caracteres (ou mais que bodyText.maxJustifyTracking, quando definido), ela é composta com esse máximo e termina antes da medida: a linha recebe as marcas cjkLoose e ragged, e a composição emite um aviso de conteúdo cjkLooseLine com o texto da linha. A causa costuma ser uma palavra latina longa ou um endereço web que não consegue subir. Uma linha sem nenhum caractere CJK (o começo de um endereço web longo) fica em bandeira sem o aviso, como uma linha latina de uma única palavra. Veja Hifenização e justificação.

#Largura da pontuação

Um sinal chinês de largura cheia é meio eme de glifo e meio eme de branco, e os ajustes mexem no branco, nunca no glifo (clreq §6.3.2). O branco fica antes do glifo num parêntese ou aspa de abertura, depois do glifo num de fechamento, e depois do glifo nos sinais de pausa e de fim de frase da China continental 、,。.;:?!, que ficam no canto da sua caixa; os sinais que Taiwan e Hong Kong centralizam (、,。.;:) e os pontos médios guardam um quarto de eme de cada lado. ?! continuam com um eme no texto horizontal de Taiwan e Hong Kong, e :;?! no texto vertical em todas as regiões. O ponto médio da China continental ocupa meio eme em qualquer estilo, centralizado, nos dois sentidos de escrita (GB/T 15834, clreq §5.1): um glifo de largura cheia cede o branco dos dois lados, e no texto vertical sua célula já mede meio eme.

EstiloDentro da linhaNo fim da linha
fullwidth (全角式)Todos os sinais com um eme.Um eme; um parêntese de fechamento, meio, com trimLineStart.
kaiming (开明式)。.?! com um eme; ,、;:, parênteses, aspas e pontos médios com meio eme. A maioria dos livros da China continental.Todos os sinais com meio eme.
lineEndHalf (行末半角)Todos os sinais com um eme.Todos os sinais com meio eme (a GB/T 15834—2011 §5.1.10 ao pé da letra).
halfwidth (半角式)Todos os sinais com meio eme, como nos dicionários.Meio eme.

No Japão (punctuationWidth: 'fullwidth', o valor automático), os sinais seguem o JLReq §3.1: 、,。. mantêm o branco depois do glifo, :; e ・ um quarto de eme de cada lado, e ?! preenchem o seu eme. Um sinal de fechamento no fim da linha conserva seu meio eme e o cede inteiro, antes de qualquer outro branco, quando a linha precisa acomodar mais um caractere; o branco depois de 。 nunca é reduzido dentro da linha. Uma linha devolve espaço na ordem do JLReq §3.8.3: espaços entre palavras, o branco do sinal que termina a linha, pontos médios, parênteses e 、,, e depois o espaço entre japonês e latim; e é espaçada sem acrescentar espaço depois de um parêntese de abertura, antes de um de fechamento ou junto a 、。・:;?! e U+3000 (§3.1.11). 、 e ・ entre dois numerais kanji são compostos sem branco e nunca quebram (二、三日, 三・一四). Um documento japonês com outro punctuationWidth (kaiming, halfwidth…) segue, em vez disso, as regras do clreq desta seção.

Com compressAdjacent, dois sinais que se encontram cedem o branco entre eles (as oito regras dos rascunhos anteriores do clreq): um parêntese de fechamento depois de outro de fechamento ou depois de um sinal de pausa ou de fim de frase da China continental (。」, não depois dos sinais centralizados de Taiwan e Hong Kong), um sinal de pausa ou de fim de frase depois de um parêntese de fechamento (」,), um parêntese de abertura depois de qualquer um deles ou depois de outro de abertura (,「, 》(, 「『), e um quarto de eme entre um ponto médio e um parêntese de fechamento antes dele ou um de abertura depois dele. Nunca mais do que o necessário para deixar o par em 1,5 eme: no Kaiming, 。” já ocupa 1,5 eme e assim fica, 》( ocupa um eme. O Kaiming põe o branco de um sinal de fim de frase depois do sinal de fechamento que o segue, com ou sem compressAdjacent: os dois glifos ficam juntos e o meio eme vem depois da aspa (。”␣母, não 。␣”母), e no fim da linha ele cai como o de um ponto final. Com trimLineStart, um parêntese de abertura que inicia uma linha cede o branco anterior, de modo que sua tinta se alinha com a borda do texto (na primeira linha de um parágrafo ele fica meio eme para dentro do recuo), e um de fechamento que termina uma linha cede o branco posterior. Um sinal centralizado cede um quarto de eme de cada lado, nunca meio eme de um só. Quando uma linha quebra entre dois sinais, nenhum deles mantém a compressão: cada um é composto como sinal na borda da linha (uma , de largura cheia cujo 「 abre a linha seguinte termina sua própria linha com um eme inteiro).

Uma linha acomoda um caractere que não pode abrir a linha seguinte (push-in) quando o branco que ela ainda pode ceder cobre o excesso, na ordem do clreq: espaços entre palavras até um quarto de eme, depois pontos médios, parênteses, sinais de pausa, os espaços entre han e latim até um oitavo de eme e, por último, os sinais de fim de frase, cada etapa distribuída por igual. kaiming só deixa seus sinais de fim de frase descerem a meio eme, e só nesse caso: uma linha à qual simplesmente falta espaço para mais um caractere é espaçada, de modo que 。?! mantêm o seu eme dentro da linha. lineEndHalf deixa todos os sinais descerem a meio eme; os sinais de fullwidth não cedem nada (um livro de largura cheia mantém sua grade), e os de halfwidth não têm mais nada a ceder.

Os sinais que o texto latino compartilha com o chinês (“ ” ‘ ’ … — ·) ocupam no texto chinês a caixa de um sinal chinês, seja qual for o avanço da própria fonte: a LXGW WenKai desenha “ ” com 0,35 eme, a Noto Serif SC o travessão com 0,89 eme e o · com um terço de eme. Eles contam como chineses quando o caractere mais próximo de um dos lados (passando por outros sinais desses) é chinês, ou quando não há nada ocidental ao lado deles. O glifo de uma aspa de abertura fica no fim da sua caixa de um eme, o de uma de fechamento no começo; um ponto médio, umas reticências e um travessão isolado ficam no meio; um par de reticências (……) é composto como a fonte compõe os dois juntos e centralizado nos seus dois emes. Um 破折号 (——) é um fio contínuo: cada travessão é esticado sobre o seu eme a partir do espaço lateral da própria fonte, os dois traços se sobrepõem na junção e sobem até o centro dos caracteres (VDTLineSegment.inkScale, uma escala que só afeta a pintura). Depois o estilo ajusta a caixa como a de qualquer outro sinal. Com texto ocidental dos dois lados (他说:He said “yes” and left.), eles mantêm o avanço da fonte, e um glifo que já mede um eme não muda nada.

Os renderizadores mantêm fora do texto o espaçamento de pontuação do próprio navegador. O Chrome compõe com meia largura o primeiro de dois sinais vizinhos quando os mede ou pinta num único trecho (本)》录 em Noto Serif SC mede 3,5 eme desse jeito), por isso dois sinais vizinhos são medidos e pintados separadamente, e as linhas HTML de texto CJK levam text-spacing-trim: space-all e text-autospace: no-autospace, com os recursos chws, halt e vchw desativados.

No VDT, um sinal que cedeu branco é um segmento próprio, cujo width é o avanço que ele conserva, com inkOffset (px) definido: os renderizadores pintam seu glifo em x + inkOffset, de modo que um sinal que cedeu o branco anterior ao glifo (um parêntese de abertura no início de uma linha) é desenhado essa distância antes da sua caixa (um deslocamento negativo). Um sinal compartilhado composto numa caixa chinesa carrega a posição do seu glifo na caixa, menos o branco que tenha cedido antes dele, o que pode dar um valor positivo. O PDF mostra esse glifo com um espaçamento entre caracteres que termina o avanço onde termina a caixa, para que um leitor nunca o veja invadir o caractere seguinte, e põe a linha num trecho /ActualText (veja Espaço entre han e latim). Quem decide de que lado fica o branco de um sinal é a região, não a fonte: componha um livro numa fonte da sua região (Noto Serif SC para a China continental, TC para Taiwan, HK para Hong Kong); uma fonte tradicional sob uma etiqueta da China continental comprime o lado errado dos seus sinais centralizados.

#Pontuação pendente

hangingPunctuation: 'allow', o valor automático no Japão, deixa um dos sinais 、,。. (na China continental, cujos sinais ficam no início da caixa, também ;:?!) ficar pendente além do fim da linha quando, do contrário, ele abriria a linha seguinte e a compressão da linha não consegue acomodá-lo; nunca no texto horizontal de Taiwan e Hong Kong, cujos sinais centralizados pareceriam cortados (no texto vertical, sim). No texto vertical o sinal pendente fica abaixo do pé da linha. 'force' deixa esse sinal pendente sempre que ele termina uma linha (menos a última de um parágrafo), e de imediato quando ele não cabe. Um sinal nunca fica pendente quando outro sinal o toca (。」, ,「). O segmento pendente recebe a marca hangs; o bbox.width da linha, sua justificação e seu alinhamento o deixam de fora, e o canvas e o PDF alargam o recorte da coluna pelo sinal pendente mais largo (hangingPunctuationOverhang), de modo que ele nunca é cortado. A maioria dos livros chineses não usa pontuação pendente; o clreq só a recomenda com uma grade de caracteres. Os livros japoneses deixam pendentes 、。,. (burasagari), nunca ;:?! nem os parênteses, nos dois modos de escrita, e só quando a compressão da linha não consegue acomodar o sinal.

#Espaço entre han e latim

latinSpacing (por padrão um quarto de eme) põe um espaço entre um caractere han (ou kana) e uma letra latina ou algarismo europeu vizinho: 用iPhone拍照 e 1999年 são compostos 用 iPhone 拍照 e 1999 年. Não há esse espaço no início nem no fim da linha, nem entre um caractere latino e um sinal chinês (用iPhone, não põe nada antes da vírgula), nem dentro de parênteses chineses ((iPhone)), nem junto a um símbolo que não seja letra nem algarismo (为¥5,999). Um espaço que o autor digitou nesse limite (用 iPhone 拍照, como trazem muitos textos da web) é substituído pelo espaço entre han e latim, não somado a ele, e as duas grafias são compostas igual; um espaço inseparável ou um espaço ideográfico fica como está. Numa linha justificada ele cresce até meio eme antes de os caracteres serem espaçados; numa linha que acomoda um caractere que não pode abrir a seguinte, ele encolhe até um oitavo de eme. Uma medida em qualquer unidade que não seja em é convertida pelo dpi da página. 0 o desativa, e um espaço digitado ali continua sendo um espaço entre palavras.

O espaço é um segmento de kind: 'space' com a marca autospace, com text vazio (ou o espaço que o autor digitou); seu width é definitivo, e a justificação de espaços própria dos renderizadores o deixa como está. Ele nunca entra no texto simples, de modo que a busca, copiar e colar e os intervalos de origem leem o texto como foi escrito. O PDF põe cada linha composta em partes (caracteres espaçados, espaços entre han e latim, sinais que cederam branco ou estão pendentes) num /Span cujo /ActualText é o texto da linha, de modo que a extração de texto lê 用iPhone拍照, não 用 iPhone 拍 照, e lê uma linha com sinais de meia largura como uma única linha.

#Números em texto vertical

No texto vertical, uma palavra latina ou um número longo é composto deitado, e um número curto fica em pé, com os algarismos lado a lado numa única célula de um eme: tate-chu-yoko (縱中橫, clreq §2.1.3, CSS text-combine-upright). uprightDigits define quantos algarismos esse número pode ter: 2 (o padrão), 3, 4, ou 0 para nenhum. 2026年9月28日 com 2 fica com 2026 deitado, 9 em pé e 28 em pé numa única célula.

  • O número inteiro ou nada dele: com 2, um número de três algarismos continua deitado, nunca dividido.
  • Um número que toca uma letra latina (A4, mp3, 3D) fica na sua palavra, deitado; o mesmo vale para um número escrito com ponto decimal ou separador de milhares (3.14, 10,000).
  • Um número dentro de uma frase latina acompanha a frase: quando há uma palavra latina dos dois lados, ele corre deitado com as palavras (printed in 49 and 32 copies, chapters 49, 32 and (7) of). De cada lado, a busca passa por espaços, outros números, chamadas de nota, a pontuação de um trecho deitado (, . : ; ( ) ' " - /), a meia-risca, o travessão e as aspas curvas (pages 3–5 of, the “49” copies) até a primeira letra ou caractere chinês. Um número cujos sinais levam a texto chinês fica em pé (上午12:30:45开会, 比分为3:2:1, 见图(3)所示, 他住在"12"号楼), e o mesmo vale para um número junto a um caractere ou sinal chinês, com ou sem espaço (第 3 回, 用iPhone 15拍攝, 第3 copies), para um ao lado de um símbolo que fica em pé (a 30×40 print) e para um no início ou no fim de um parágrafo, que tem palavra de um lado só (49 copies were printed, on page 7.: escreva :sideways[…] para deitá-lo). O parágrafo é lido inteiro, atravessando quebras de linha, ênfases e links.
  • Junto a um símbolo que o Unicode põe em pé, cada número ocupa sua própria célula: 30×40 fica 30, ×, 40, tudo em pé.
  • Uma célula com mais caracteres do que cabem num eme é comprimida na horizontal até o eme; o tracking e a justificação nunca entram nela, só depois dela.
  • Para quebrar e justificar, a célula conta como um caractere chinês, e nenhum espaço entre han e latim é posto em volta dela.
  • No Japão, os pares !!, !?, ?! e ??, de meia largura ou de largura cheia, também ficam em pé numa única célula (すごい!?), a menos que uma letra latina, um algarismo ou um terceiro sinal os toque; um par de largura cheia é pintado com os sinais de meia largura. Kana pequenos, :;, “ ” (pintados 〝〟) e ・ usam as formas verticais japonesas (veja Composição japonesa › Formas verticais).
  • O medidor e todos os renderizadores encontram as mesmas células: o canvas pinta os algarismos de volta em pé e comprimidos, o PDF faz o mesmo com a fonte horizontal, e o HTML os envolve em text-combine-upright: all.

À mão, três marcas inline substituem o ajuste no texto vertical e não mudam nada no texto horizontal (veja Formato do documento › Orientação no texto vertical):

第:tcy[120]回、:upright[GDP]與:sideways[12]

:tcy[…] compõe seu texto numa única célula em pé, :upright[…] põe cada caractere em pé numa célula própria (as letras latinas centralizadas nela), :sideways[…] deita o trecho inteiro, incluindo os caracteres chineses. No VDT, um trecho :tcy é um segmento com tcy: true, de um eme de largura; um trecho :upright ou :sideways carrega orientation. Os números compostos numa única célula por uprightDigits não carregam marca: verticalRuns(graphemes, region, uprightDigits) os encontra. Num parágrafo, um número curto que corre com palavras latinas carrega orientation: 'sideways', como se estivesse escrito :sideways[…]: seu segmento nem sempre contém as palavras que ele acompanha.

#Grade de caracteres

Uma mancha chinesa é especificada em caracteres (clreq §7.1.1): o corpo do texto × caracteres por linha × linhas por página, mais o espaço entre linhas e, com duas colunas, a medianiz. grid a define dessa forma:

cjk: { grid: { enabled: true, charsPerLine: 28, linesPerPage: 28, show: true } }

Com enabled, a configuração é reescrita antes que qualquer coisa a leia: cada coluna tem charsPerLine emes de bodyText.fontSize de largura, e a mancha linesPerPage linhas de bodyText.lineHeight de altura; com layoutType: 'double', a medianiz é layout.gutterWidth arredondada para um número inteiro de emes, no mínimo um. As margens em page.margins funcionam como mínimos: a mancha é centralizada na área que elas deixam, e cada margem cresce metade da sobra no seu eixo (um livro espelhado mantém as margens interna e externa diferentes, as duas crescidas na mesma medida). Sem valor, charsPerLine e linesPerPage assumem quantos couberem. Um número maior do que cabe é reduzido ao que cabe e informado como um aviso de configuração cjkGridClamped. O corpo e a entrelinha ficam como escritos. Numa diagramação oneAndHalf, charsPerLine é o da coluna principal; a coluna lateral recebe o número inteiro de emes mais próximo da largura que sideColumnPercent lhe dá dentro das margens configuradas (no mínimo um), a medianiz é arredondada como numa de duas colunas, e sideColumnPercent é reescrito para que as duas colunas sejam cortadas em emes inteiros. No texto vertical (layout.writingMode: 'vertical-rl'), os caracteres de uma linha correm de cima para baixo na página e as linhas a atravessam, de modo que charsPerLine mede a altura da página, uma diagramação double dá dois andares empilhados de cima para baixo, e as células da sobreposição ficam no eixo em que os caracteres estão centralizados.

Duas diagramações da prática chinesa, em 五号 (10,5 pt) com 6 pt de espaço entre linhas (lineHeight: 16.5pt):

  • 大32开, 140 × 203 mm, 28 × 28: uma medida de 294 pt (103,7 mm) e uma mancha de 462 pt (163 mm); page.margins de 16/20 mm interna e externa e de 18/20 mm superior e inferior deixam espaço para ela.
  • 16开, 184 × 260 mm, duas colunas de 23 caracteres em 小五 (9 pt, linhas de 13,5 pt) com medianiz de dois caracteres: 2 × 207 pt + 18 pt.

show desenha a grade (稿纸) sobre a mancha, um quadrado cinza-claro por posição de caractere em cada linha (a caixa de eme do caractere em torno da sua linha de base), em cada coluna da página (uma coluna lateral oneAndHalf do lado em que a paridade da página a põe), no canvas e no HTML. O PDF só a desenha quando renderToPdf recebe characterGrid: true: é uma ajuda para a tela. cjkGridGeometry(config) devolve a grade que uma configuração define (números em uso, margens, mancha) e applyCjkGrid(config) a configuração reescrita.

#Marcas, rubi e warichu

A marcação de marcas chinesas, rubi e warichu mantém o texto no parágrafo: a busca, o sumário, as âncoras do índice remissivo e o texto copiado leem os caracteres como foram escritos, e só o que se desenha em volta deles depende destes ajustes.

  • Pontos de ênfase (着重号, 傍点; :dots[…], e *…* sobre caracteres CJK com emphasis: 'dots') põem uma marca por caractere, centralizada nele (sem contar o espaçamento de uma linha justificada), nunca na pontuação nem nos espaços: sob o caractere no texto horizontal, à direita dele no texto vertical (clreq §5.3.1). style escolhe um ponto cheio, um círculo vazado ou um gergelim, fill="open" desenha o contorno, pos="over|under" o lado; emphasisMark define o que uma marca deixa sem definir (no Japão, um gergelim sobre o texto, à direita dele no texto vertical). Um gergelim fica na mesma posição na folha nos dois modos de escrita, e as marcas do lado de uma leitura em rubi ficam por fora dela.
  • Linhas laterais (傍線, :sideline[…]{style pos}) correm ao lado do texto, incluindo pontuação e espaços: solid, double, wavy ou dotted, sob o texto na horizontal e à direita dele na vertical, por padrão; duas linhas que se encontram cedem cada uma um oitavo de eme. Funcionam em qualquer escrita.
  • Linhas de nome próprio e de título de obra (专名号 :name[…], 书名号 :book[…] com bookTitleMark: 'wavy') correm sob a caixa de eme dos caracteres (à esquerda dela no texto vertical), atravessando os espaços dentro do trecho. Onde dois trechos se encontram, cada ponta cede um oitavo de eme, de modo que :name[賈寶玉]:name[林黛玉] se lê como dois nomes. Quando pontos e uma linha marcam o mesmo texto do mesmo lado, a linha fica mais perto do texto. Com 'brackets', os 《》 são texto com o qual as linhas são quebradas, pintado e copiado como qualquer outro caractere; eles não ocupam nenhum caractere do texto simples nem do mapa de origem (os segmentos recebem a marca inserted).
  • As leituras em rubi ficam no espaço entre as linhas, encostadas na caixa de eme da base, centralizadas nela; uma leitura que contém uma letra latina (pinyin) e fica sobre a base sobe a profundidade das descendentes da sua fonte (g, j, p, q, y) mais 0,04 eme do texto, para não tocar a base, e todas as leituras da linha mantêm uma única linha de base; uma leitura mais larga que a base alarga a caixa da base, descontado um quarto do eme do rubi que ela pode avançar sobre um vizinho sem leitura, e duas leituras ficam a um quarto do eme do rubi uma da outra (duas leituras em zhuyin, a um quarto do eme dos símbolos, de modo que três símbolos ao lado de cada um de dois caracteres ficam nas suas células). Um rubi mono (uma leitura por caractere) pode quebrar entre seus caracteres; um rubi de grupo nunca quebra. No Japão, uma leitura segue ruby.align e ruby.overhang: espaçada 1:2:1 quando é mais curta que a base, avançando sobre kana mas não sobre kanji quando é mais longa, e o resto do espaço vem de espaçar a base; um rubi por caractere de dois ou mais caracteres é rubi jukugo, cada caractere mantém sua leitura, que pode avançar sobre o caractere seguinte da palavra mas nunca se sobrepõe a outra leitura, e a linha continua podendo quebrar entre eles (JLReq §3.3). No início ou no fim de uma linha, base e leitura se alinham à borda (clreq §5.5.4). O zhuyin no texto horizontal fica numa coluna à direita de cada caractere, e a caixa do caractere cresce com a coluna; no texto vertical, a mesma coluna desce pela direita dos caracteres. O sinal de tom vai à direita da coluna, com metade da tinta acima do topo do último símbolo (clreq §5.5.3.3), posicionado pela tinta porque as fontes põem esses sinais no alto da sua caixa de eme, e no texto vertical ele fica em pé, como o ponto do tom neutro acima do primeiro símbolo.
  • As notas warichu (双行夹注) se dobram em duas linhas no corpo da nota, centralizadas na linha, sem espaço entre elas. A linha de cima recebe caracteres até conter pelo menos metade da parte, de modo que a de baixo nunca é a mais longa, e mais um enquanto a linha de baixo começaria com um sinal que não pode abrir linha. Uma nota mais longa que o espaço que resta na linha o preenche e continua na linha, coluna ou página seguinte; seus parênteses só vão antes da primeira linha e depois da última. No texto vertical a linha de cima é a da direita, lida primeiro.
  • As marcas de kanbun (:kunten[字]{kaeri okuri tate}) põem as marcas de retorno do último caractere do lado do pé da linha, depois dele (embaixo à esquerda no texto vertical), seu okurigana do lado da cabeça, a partir do meio, os mais longos empurrando o caractere seguinte, e o tatesen como um fio curto até o caractere seguinte (JIS X 4051 §5). O texto copiado e o /ActualText do PDF leem o okurigana depois dos seus caracteres e deixam de fora as marcas de retorno; um espaço entre linhas estreito demais para elas é informado como kuntenExceedsLeading.

O passo das linhas nunca muda: marcas e leituras vivem na entrelinha. Um parágrafo cujo espaço entre linhas (a entrelinha menos o corpo do texto) é menor que meio eme com marcas de um lado, ou cinco oitavos com marcas dos dois lados (clreq §5.6.1), é informado como um aviso de conteúdo cjkMarksExceedLeading; um cujas leituras são mais altas que o espaço (uma leitura em pinyin contada com a sua elevação), como rubyExceedsLeading. Dê a esses parágrafos um estilo de parágrafo com mais entrelinha. Os livros japoneses com furigana costumam usar uma entrelinha do texto de cerca de 1,75 eme (JLReq §2.4.2).

No VDT, um segmento marcado carrega cjkMarks, e a diagramação põe os pontos e as linhas nas suas linhas como VDTLine.marks (pontos, círculos, gergelins, linhas retas, duplas, pontilhadas e onduladas, no referencial de fluxo da linha), que o canvas, o HTML e o PDF desenham como estão (PDF: Artifact /Layout; HTML: caixas aria-hidden, texto pontilhado em <em>). O segmento de uma base de rubi carrega ruby (a leitura e seus trechos; numa leitura japonesa, também o id que sua anotação compartilha com os outros caracteres de uma palavra jukugo, e jukugo) e pinta sua base em inkOffset, com tracking, quando a leitura a espaçou; um caractere com marcas de kanbun carrega kunten (seus trechos e seu tatesen); a parte de uma nota warichu numa linha é um único segmento cujo text é a linha de cima seguida da de baixo e cujo warichu contém as linhas que os renderizadores pintam no lugar. O PDF etiquetado põe uma leitura no RT do Ruby da sua base e uma nota num Warichu, e o /ActualText da linha lê o texto da base e a nota uma única vez. Marcas, leituras e notas são desenhadas no texto corrido, nos títulos, nas listas e nos boxes, e nas legendas, células de tabela e notas dos recursos, cujas linhas carregam marks e ruby como as linhas do corpo; o texto de design mantém o texto sem elas.

O resolvedor e o removedor de padrões seguem as outras seções. spaceAfterQuestion, paragraphStartBracket e wordBreak, e os overhang, align e smallKana do rubi, só aparecem na configuração resolvida quando mudam alguma coisa (no Japão, ou quando definidos). setCjkLineBreak e setCjkComposition definem o nível e a composição (larguras da pontuação, pontuação pendente, espaço entre han e latim; cjkCompositionOf(resolved.cjk, dpi)) para medições feitas fora de buildDocument, que os define a partir da configuração. Fora de uma composição, os sinais mantêm o avanço completo e nenhum espaço entre han e latim é posto:

import { DEFAULT_CJK_CONFIG, resolveCjkConfig, stripCjkDefaults, cjkRegionOf, setCjkLineBreak, setCjkComposition, cjkCompositionOf } from 'postext';
 
resolveCjkConfig(undefined, 'zh-HK');
// { region: 'hongkong', lineBreak: 'basic', punctuationWidth: 'fullwidth', compressAdjacent: true,
//   trimLineStart: true, hangingPunctuation: 'none', latinSpacing: { value: 0.25, unit: 'em' }, uprightDigits: 2,
//   grid: { enabled: false, charsPerLine: 0, linesPerPage: 0, show: false },
//   emphasis: 'dots', emphasisMark: { style: 'dot', fill: 'auto', position: 'auto' },
//   bookTitleMark: 'wavy', bookTitleBrackets: [{ open: '《', close: '》' }, { open: '〈', close: '〉' }],
//   ruby: { fontSize: { value: 0.5, unit: 'em' }, position: 'auto' },
//   warichu: { fontSize: { value: 0.5, unit: 'em' }, open: '', close: '' }, kunten: { … } }
 
resolveCjkConfig(undefined, 'ja');
// { region: 'japan', lineBreak: 'ja-very-strict', punctuationWidth: 'fullwidth', compressAdjacent: true,
//   trimLineStart: true, hangingPunctuation: 'allow', spaceAfterQuestion: true, paragraphStartBracket: 'half', …,
//   emphasisMark: { style: 'sesame', fill: 'auto', position: 'over' }, bookTitleMark: 'brackets',
//   bookTitleBrackets: [{ open: '『', close: '』' }, { open: '「', close: '」' }],
//   ruby: { fontSize: { value: 0.5, unit: 'em' }, position: 'auto', overhang: 'kana', align: 'jis' },
//   warichu: { fontSize: { value: 0.5, unit: 'em' }, open: '(', close: ')' }, kunten: { … } }

No Sandbox, esses ajustes ficam em Design › Sistema de escrita › Tipografia do Leste Asiático.

#Tipos de recurso

Um tipo de recurso é uma categoria que você mesmo define (Figura, Tabela, Diagrama, Listagem…) e que determina como os recursos daquele tipo são numerados, legendados e referenciados. A lista fica em config.resourceTypes; no Sandbox, ela é editada em Design → Figuras e tabelas → Numeração e posicionamento.

Quando config.resourceTypes não está definido, o Postext traz três tipos padrão embutidos: Figura, Tabela e Vídeo, cada um numerado separadamente como {h1}.{n} (reiniciando a cada título de nível 1) com contadores decimais. Uma lista sem o tipo video (a de um livro salvo antes de existirem vídeos) continua numerando os recursos de vídeo com tipo video: o tipo embutido Vídeo é acrescentado para eles (effectiveResourceTypes(config, resources)), e defaultVideoResourceType(locale) devolve esse tipo isoladamente. Os nomes seguem o idioma do documento: config.locale, senão bodyText.hyphenation.locale, senão o inglês (veja Idioma do documento).

Os tipos padrão embutidos acompanham o idioma. A função exportada defaultResourceTypes(locale = 'en') traduz os nomes dos tipos, os rótulos curtos e os prefixos de legenda para o idioma do documento: o inglês gera Figure/Fig. e Table/Tab.; o espanhol gera Figura/Fig. e Tabla/Tabla; francês, alemão, italiano, português, catalão e holandês também têm os seus (a tabela em Idioma do documento traz todos). Etiquetas regionais como es-ES são resolvidas pelo idioma, e qualquer idioma sem tradução recai no inglês. O comportamento da numeração (numberingTemplate: '{h1}.{n}', resetOn: 'h1', contadores decimais) não depende do idioma. Cada chamada devolve objetos novos, então você pode modificar o resultado à vontade:

import { defaultResourceTypes } from 'postext';
 
const types = defaultResourceTypes('es');
// => [{ id: 'figure', name: 'Figura', shortLabel: 'Fig.', captionPrefix: 'Figura',
//       numberingTemplate: '{h1}.{n}', resetOn: 'h1', counterFormat: 'decimal', … },
//     { id: 'table',  name: 'Tabla',  shortLabel: 'Tabla', captionPrefix: 'Tabla', … },
//     { id: 'video',  name: 'Vídeo',  shortLabel: 'Vídeo', captionPrefix: 'Vídeo', … }]
type ResourceCounterFormat =
  | 'decimal'
  | 'roman-lower'
  | 'roman-upper'
  | 'alpha-lower'
  | 'alpha-upper';
 
type ResourceCounterReset = 'never' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
 
interface ResourcePlacement {
  position?: 'auto' | 'top' | 'bottom' | 'here'; // que espaço livre um flutuante pode ocupar; 'here' = inserção em linha na diretiva ::resource
  span?: 'column' | 'page' | 'side';             // uma coluna, a largura total do conteúdo ou a coluna lateral só de flutuantes
  rotate?: 'ccw' | 'cw';                         // um quarto de volta: uma tabela em paisagem numa página só para ela
  width?: number;                                // fração (0 < width < 1) da largura da coluna ou da página; padrão: a largura toda
  align?: 'left' | 'center' | 'right';           // onde fica um flutuante mais estreito que a coluna; padrão 'left'
  captionSide?: boolean;                         // legenda ao lado da figura, na coluna lateral de uma diagramação oneAndHalf (só flutuantes de coluna)
  columns?: number;                              // um flutuante 'column' ocupando esse número de colunas vizinhas (desde 1.18)
}
 
interface ResourceType {
  id: string;                          // id estável, referenciado por Resource.typeId
  name: string;                        // nome de exibição no singular, ex. "Figure"
  namePlural?: string;                 // plural opcional, ex. "Figures"
  shortLabel: string;                  // rótulo compacto para referências no texto, ex. "Fig."
  numberingTemplate: string;           // "{h1}.{n}" ou "{n}"
  resetOn: ResourceCounterReset;       // quando o contador {n} reinicia
  counterFormat: ResourceCounterFormat;// como {n} é formatado
  captionPrefix: string;               // anteposto à legenda, ex. "Figure"
  defaultPlacement?: ResourcePlacement;// posicionamento de reserva para os recursos deste tipo
}

ResourcePlacement tem a mesma forma que um recurso define no seu próprio placement. position escolhe o tipo de espaço livre que um flutuante pode ocupar: auto (o padrão) fica com o primeiro depois da primeira referência, top / bottom o restringem a esse tipo de faixa, here insere o recurso em linha. span define a extensão do flutuante: uma coluna, a largura total do conteúdo ou a coluna lateral só de flutuantes de uma diagramação de coluna e meia. rotate gira o recurso um quarto de volta e o transforma em flutuante de página inteira, numa página só para ele. width estreita o flutuante a uma fração da coluna (ou da página, num flutuante de página inteira), por exemplo uma tabela pequena numa coluna larga. align diz onde esse flutuante mais estreito fica (à esquerda por padrão, centralizado ou à direita) e também onde fica uma imagem mais estreita que o seu espaço: um bitmap menor que a coluna, ou uma imagem que layout.fitFiguresToPage reduziu. A legenda e a nota mantêm a medida do espaço. (Até o postext 1.4, uma imagem assim sempre ficava alinhada à esquerda.) captionSide põe a legenda ao lado da figura, na coluna lateral só de flutuantes de uma diagramação de coluna e meia (layout.sideColumnRole: 'floats'), alinhada com o topo da figura (com a base, num flutuante inferior); vale apenas para flutuantes de coluna, e numa página sem essa coluna a legenda continua embaixo da figura. Quando nem o recurso nem o seu tipo definem um posicionamento, o padrão embutido é auto / column.

columns (desde o postext 1.18) estende um flutuante span: 'column' por esse número de colunas vizinhas numa página de várias colunas: uma foto ocupando duas das cinco colunas de um jornal. A sua medida são essas colunas mais as medianizes entre elas. Ele ocupa o topo de uma sequência de colunas vazias que começam na mesma altura, ou o pé da coluna que o cita e das colunas vazias seguintes; com tantas colunas quantas a página tem, ou mais, vira um flutuante de página inteira. É ignorado nos spans 'page' e 'side', num recurso girado (rotate) e numa inserção em linha (here), e captionSide só vale para um flutuante de uma coluna de largura.

PropriedadeTipoDescrição
idstringIdentificador estável referenciado pelo typeId de cada recurso. É definido uma única vez, quando o tipo é criado; excluir um tipo que ainda é referenciado por recursos gera um aviso de tipo órfão.
namestringNome de exibição no singular. Usado pela referência no texto style="full" (ex.: Figure 1.7).
namePluralstring (opcional)Nome de exibição no plural, para rótulos da interface e listas de recursos.
shortLabelstringAbreviação compacta usada pelo estilo padrão de referência no texto (ex.: Fig. 1.7).
numberingTemplatestringModelo do número calculado. Veja Tokens do modelo abaixo. As formas comuns são (por capítulo, ex.: 2.3) e (uma contagem corrida única).
resetOnResourceCounterReset'never' dá uma contagem corrida no documento inteiro; 'h1'..'h6' reiniciam o contador sempre que aparece um título daquele nível (ou de um nível superior). Ajuste-o ao nível de título que aparece no modelo, ex.: com resetOn: 'h1'.
counterFormatResourceCounterFormatComo o contador é impresso: decimal (1, 2, 3), romano minúsculo/maiúsculo (i, ii / I, II) ou alfabético minúsculo/maiúsculo (a, b / A, B). As grafias de páginas e listas também são aceitas ('lower-roman', 'arabic'…; veja Grafias dos formatos de numeração); um valor desconhecido conta em decimal e é reportado. Os tokens de título (…) sempre saem em decimal.
captionPrefixstringTexto anteposto à legenda da figura ou da tabela. O número calculado vem depois do prefixo: uma legenda sai como . , ex.: Figura 1.7. A planta original. Um tipo com numberingTemplate vazio não tem número, e a sua legenda fica . . Os espaços no fim do prefixo são descartados, e um prefixo que já termina em ., :, !, ? ou … (ou na forma de largura total de um deles) não recebe um segundo ponto: Pr. Linhas a 0°.
defaultPlacementResourcePlacement (opcional)Posicionamento usado pelos recursos deste tipo que não definem o próprio placement: position, span, rotate, width, align, captionSide e columns, cada um resolvido separadamente. Quando nem o recurso nem o tipo definem um campo, vale o padrão embutido: auto / column, sem giro, largura total, alinhado à esquerda, legenda embaixo da figura. Veja Numeração e referências abaixo para a cadeia de resolução e Formato do documento › Recursos para o que cada valor faz, inclusive nos recursos girados.
captionStyleCaptionStyleConfig (opcional)Substituição parcial do estilo de legenda para os recursos deste tipo. Só as chaves que você define substituem o captionStyle global; todo o resto é herdado (uma color substituída também define as cores do rótulo e da nota, a menos que elas sejam definidas explicitamente). Uso típico: tabelas com legenda em cima, sobre uma barra colorida, enquanto as figuras mantêm a legenda embaixo. Referências à paleta são resolvidas como qualquer outra cor.

#Tokens do modelo

numberingTemplate é processado pelo mesmo mecanismo da numeração de títulos (veja Títulos). Ele reconhece dois tipos de token:

  • {n}: o contador do tipo, formatado conforme counterFormat. É o valor que aumenta a cada recurso e reinicia de acordo com resetOn.
  • {h1} … {h6}: os números de título vigentes no ponto da primeira referência, sempre em decimal. {h1} é o número do título de nível 1 atual, {h2} o de nível 2, e assim por diante.

Qualquer outro texto é literal. Uma barra invertida escapa um {, } ou \ literal. Quando um token de título não tem valor naquele ponto (ex.: {h1} antes de qualquer título de nível 1), ele desaparece junto com o separador vizinho; assim, {h1}.{n} se reduz ao contador sozinho.

Um modelo vazio ('') não imprime número, embora o tipo continue contando os seus recursos: a legenda fica Do. Linhas a 0° e um :ref imprime só o rótulo (Do). Até o postext 1.4, essa legenda saía Do .Linhas a 0° e a referência terminava num espaço não separável.

ModeloCom h1 = 2, contador = 3Observações
3Uma contagem corrida única. Combine com resetOn: 'never'.
2.3Por capítulo. Combine com resetOn: 'h1'.
2.0.3Por seção. Combine com resetOn: 'h2'.

#Numeração e referências

O número que um tipo de recurso produz é o que :ref imprime e o que vem depois do prefixo da legenda. :ref{id} é a forma principal: a primeira referência na ordem de leitura incorpora o recurso, que flutua para o primeiro espaço livre depois dela, seja o pé da coluna que o cita, o topo ou o pé da próxima coluna vazia, ou uma faixa da página seguinte (conforme o posicionamento resolvido: position: 'auto' | 'top' | 'bottom' | 'here' e span: 'column' | 'page' | 'side', mais rotate, width, align e captionSide, resolvidos primeiro no recurso, depois no defaultPlacement do tipo e por fim no padrão embutido auto / column; 'top' / 'bottom' restringem a busca a esse tipo de espaço). A inserção em bloco ::resource{id} é opcional e só é necessária para placement.position: 'here', uma inserção em linha, sem flutuar, num ponto exato do fluxo. A gramática completa do lado do documento (as duas formas, mais as opções style e text de :ref) está documentada em Formato do documento › Recursos, inclusive como a ordem das primeiras referências define a contagem.

Isso espelha a numeração de títulos: assim como um nível de título tem um numberingTemplate, um tipo de recurso também tem, mas o contador do recurso ({n}) avança a cada primeira referência, e não a cada título, e resetOn o amarra de volta à hierarquia de títulos.

#O que é numerado

Um recurso é numerado quando o texto o referencia (com :ref ou com uma inserção ::resource), na ordem dessas primeiras referências, qualquer que seja o seu posicionamento: flutuante, em linha (here), na coluna lateral ou girado. Um recurso que só um design desenha (um elemento image de uma abertura de capítulo, de um cabeço ou de uma página de parte) ou que nada referencia não recebe número nem faz avançar o contador do seu tipo. Assim, num ensaio fotográfico cujas pranchas sangradas são imagens das aberturas e cuja única prancha menor é um flutuante citado, esse flutuante é a prancha I, por mais pranchas que as aberturas tenham mostrado antes; numere as pranchas das aberturas no próprio design delas (com um atributo como {attr.plate}) e deixe o contador para as pranchas que o texto cita.

Num livro diagramado capítulo a capítulo (o Sandbox, buildBundle, ou buildDocument com os contadores que continuationAfter() repassa), vale a primeira referência do livro inteiro: o recurso mantém o número que recebeu no capítulo que o cita primeiro, e só esse capítulo o posiciona. O :ref de um capítulo posterior imprime esse número e não posiciona nada, e ali uma inserção ::resource de um recurso flutuante é só mais uma referência (uma inserção em linha here continua sendo composta onde está escrita). Essa referência vira link para a figura quando a figura está na mesma saída: um PDF do livro inteiro aponta para a página do capítulo anterior. Um capítulo renderizado sozinho, em HTML ou em PDF, a compõe como texto simples na cor dos links, já que a figura não está naquele documento. Um host que junta o HTML dos capítulos numa só página passa a renderToHtml os recursos que os capítulos ancoram, como refTargets, e essa referência volta a apontar para a figura do capítulo anterior:

import { anchoredResourceIds, buildBundle, renderToHtml } from 'postext';
 
const docs = buildBundle(bundle);
const refTargets = new Set(docs.flatMap((d) => [...anchoredResourceIds(d)]));
const html = docs.map((d) => renderToHtml(d, { refTargets })).join('');

Mudou no postext 1.5: até a 1.4, cada capítulo que referenciava uma figura a fazia flutuar de novo, e o HTML de todo :ref era um link, estivesse a figura na página ou não.

{h1} é a contagem corrida dos títulos de nível 1: todo H1 a faz avançar, a menos que o seu estilo de título defina numbered: false (um numberingTemplate vazio esconde o número do título, mas não interrompe a contagem). Por isso, um artigo cujo único H1 é o título numera as figuras como 1.1, 1.2… com os tipos embutidos {h1}.{n}. Há duas maneiras de imprimir Figura 1, 2…:

  • um tipo numerado {n} com resetOn: 'never' (com resetOn: 'h1' a contagem recomeçaria a cada H1);
  • um estilo de título com numbered: false no título do artigo e em qualquer outro H1 que não deva contar: um título assim não faz {h1} avançar, mas o deixa como estava (vazio antes do primeiro H1 contado, onde {h1}.{n} se reduz ao contador sozinho) e nunca dispara resetOn: 'h1', de modo que a contagem passa direto por ele. Depois de # Introduction e da sua Figura 1.1, a primeira figura sob um # Appendix sem número é a 1.2, e não a 2.1.
// Figura 1, 2, 3… num documento de um só artigo
resourceTypes: defaultResourceTypes('en').map((t) => ({ ...t, numberingTemplate: '{n}', resetOn: 'never' })),

#Estilo de tabela

A propriedade tableStyle controla a tipografia e a decoração dos recursos de tabela: de todas as tabelas, a menos que uma delas escolha um estilo de tabela nomeado. As células do corpo e as de cabeçalho têm estilos independentes. Família, tamanho e cores herdam o texto corrido resolvido quando não são definidos, então um documento sem tableStyle compõe as tabelas com a tipografia do texto corrido.

const config: PostextConfig = {
  tableStyle: {
    headerBold: true,
    headerBackground: { hex: '#f0f0f0', model: 'hex' },
    borders: true,
    borderWidth: { value: 0.75, unit: 'pt' },
  },
};
PropriedadeTipoPadrãoDescrição
bodyFontFamilystringfonte do texto corridoFamília tipográfica das células do corpo.
bodyFontSizeDimensiontamanho do texto corridoTamanho da fonte das células do corpo.
bodyColorColorValuecor do texto corridoCor do texto das células do corpo.
headerFontFamilystringfonte do texto corridoFamília tipográfica das células de cabeçalho.
headerFontSizeDimensiontamanho do texto corridoTamanho da fonte das células de cabeçalho.
headerColorColorValuecor do texto corridoCor do texto das células de cabeçalho.
headerBoldbooleantrueCompor as células de cabeçalho em negrito.
headerItalicbooleanfalseCompor as células de cabeçalho em itálico.
headerLetterSpacingDimension0ptEspaçamento entre letras (tracking) depois de cada caractere de uma célula de cabeçalho, espaços incluídos, como o letter-spacing do CSS. Valores positivos afastam as letras (um cabeçalho em maiúsculas costuma pedir de 0.05em a 0.1em), valores negativos as aproximam. Um em é o tamanho do cabeçalho. As linhas do cabeçalho são medidas com esse valor, então quebram, centralizam e alinham já com o tracking, e canvas, HTML e PDF o pintam igual. Vale para todas as células de cabeçalho: as linhas de cabeçalho e qualquer célula marcada como isHeader.
headerTextTransform'none' | 'uppercase''none'Compõe as células de cabeçalho em maiúsculas. O texto mantém o comprimento, para que o Sandbox continue associando cada letra ao texto-fonte: uma letra cuja maiúscula é mais longa (ß) fica como está. As referências a recursos mantêm o seu rótulo.
headerBackgroundEnabledbooleantruePinta um fundo atrás da linha de cabeçalho.
headerBackgroundColorValue#f0f0f0Cor de fundo da linha de cabeçalho.
bodyBackgroundEnabledbooleanfalsePinta um fundo atrás das linhas do corpo.
bodyBackgroundColorValue#ffffffCor de fundo das linhas do corpo (só é pintada quando ativada).
bodyAlternateBackgroundEnabledbooleanfalseLinhas zebradas: preenche uma a cada duas linhas do corpo com bodyAlternateBackground. Veja linhas zebradas.
bodyAlternateBackgroundColorValue#f2f2f2Fundo das linhas alternadas do corpo (só é pintado quando ativado).
bordersbooleantrueDesenha as bordas das células.
borderColorColorValuecor do texto corridoCor do traço das bordas.
borderWidthDimension0.75ptEspessura do traço das bordas (≈1 px a 96 DPI; acompanha o DPI da página).
cellPaddingDimension0.375emMargem interna de todas as células.
rules'grid' | 'horizontal' | 'outer' | 'none''grid'Quais fios traçar quando borders está ativado: a grade completa das células, só os fios horizontais (borda superior e inferior de cada linha, sem verticais), só a moldura externa, ou nenhum.
borderRadiusDimension0Raio dos cantos da moldura externa da tabela. A moldura é traçada arredondada (com os fios grid ou outer), os fundos das células e o fundo do cabeçalho são recortados por ela (também com rules: 'none' ou com as bordas desativadas) e os fios horizontais são aparados no seu contorno externo; os fios internos continuam retos. Uma tabela dividida entre páginas arredonda os cantos superiores da primeira parte e os inferiores da última. Limitado à metade da largura e da altura da tabela.
overflow'split' | 'clip' | 'hide''split'O que acontece com uma tabela mais alta que a página: continuar nas páginas seguintes, manter só as linhas que cabem ou deixá-la de fora. Veja abaixo.
continuedSuffixstring'(cont.)'Acrescentado em itálico, depois de um espaço, à legenda de cada parte que continua uma tabela dividida; um sufixo que começa com um caractere chinês ou de largura total ('(续)') fica colado à legenda.
continuesMarkerEnabledbooleantrueColoca um aviso embaixo de cada parte que continua na página seguinte.
continuesMarkerstring'Continued' / 'Continúa'Texto desse aviso, alinhado à direita embaixo da parte, na tipografia das notas (veja estilo de legenda). O padrão acompanha o idioma do documento (veja Idioma do documento para os oito idiomas).

As espessuras das bordas mantêm as frações: um fio de 0.5pt é traçado como filete no PDF e na tela, em vez de ser arredondado para um pixel inteiro (o mínimo é 0,25 px).

#Linhas zebradas

Tabelas longas de dados ficam mais fáceis de acompanhar na horizontal quando uma linha a cada duas é tingida. bodyAlternateBackgroundEnabled ativa as faixas e bodyAlternateBackground define a cor delas:

const config: PostextConfig = {
  tableStyle: {
    bodyBackgroundEnabled: true,
    bodyBackground: { hex: '#ffffff', model: 'hex' },
    bodyAlternateBackgroundEnabled: true,
    bodyAlternateBackground: { hex: '#eef3fa', model: 'hex' },
  },
};

As linhas são contadas a partir da primeira linha depois das linhas de cabeçalho (TableModel.headerRowCount, ou as linhas iniciais formadas por células de cabeçalho): essa linha fica com bodyBackground (ou sem fundo, enquanto bodyBackgroundEnabled estiver desativado), a seguinte recebe o fundo alternado, e assim por diante. A contagem segue o modelo da tabela, não a página; assim, uma tabela dividida entre páginas mantém a faixa de cada linha em todas as páginas, e uma célula mesclada entre linhas recebe a faixa da sua primeira linha. As células de cabeçalho mantêm o fundo do cabeçalho, o background próprio de uma célula prevalece sobre os dois, e uma cor ligada à paleta acompanha a paleta. Um estilo de tabela nomeado define os dois campos como qualquer outro, então um estilo pode ser zebrado sem que as outras tabelas do documento sejam; no Sandbox, eles são a chave Linhas zebradas e a sua cor, em Células do corpo.

Na VDT, as células das linhas alternadas levam alternate: true, e o layout da tabela leva bodyAlternateBackground. tableCellFill(table, cell) devolve o fundo com que uma célula é pintada (o próprio, o do cabeçalho, o alternado ou o do corpo), que é o que os renderizadores de canvas, HTML e PDF pintam. Fundos vizinhos se encontram sem emenda: um navegador com proporção de pixels fracionária ou um leitor de PDF suaviza cada fundo separadamente e deixaria a página aparecer num filete entre duas células, por isso os renderizadores de HTML e PDF pintam tableCellFillRects(table) (o fundo de cada célula com uma tira sobre cada borda que ela compartilha com uma célula pintada depois, que então a cobre), e o canvas ajusta os fundos aos pixels do dispositivo.

#Estilos de tabela nomeados

Um documento raramente compõe todas as tabelas iguais: uma lista de verificação numa grade azul-marinho com moldura arredondada, uma linha de opções emoldurada só pela borda externa, uma tabela de dados com simples fios horizontais. tableStyles declara variantes nomeadas, e um recurso de tabela escolhe uma delas com table.styleId. Cada campo que um estilo deixa sem definir é lido primeiro de tableStyle e depois do texto corrido, então um estilo declara só o que distingue as suas tabelas. Uma tabela sem styleId, ou com um id que nenhum estilo declara, mantém tableStyle; um documento sem tableStyles sai exatamente como antes.

const config: PostextConfig = {
  tableStyle: {
    borderColor: { hex: '#163a76', model: 'hex' },
    borderWidth: { value: 1.3, unit: 'pt' },
    borderRadius: { value: 10, unit: 'pt' },
  },
  tableStyles: [
    {
      id: 'option',
      name: 'Option row',
      rules: 'outer',
      borderColor: { hex: '#7a9cc6', model: 'hex' },
      borderWidth: { value: 1, unit: 'pt' },
      borderRadius: { value: 8, unit: 'pt' },
      headerBackgroundEnabled: false,
    },
  ],
};
 
// Nos recursos: esta tabela é composta no estilo "option".
const resource: Resource = {
  id: 'choices', typeId: 'table', kind: 'table', createdAt: 0, updatedAt: 0,
  table: { model: { rows: [/* … */] }, styleId: 'option' },
};

Cada entrada aceita todos os campos de tableStyle, mais id (o que table.styleId referencia) e um name opcional para o editor (por padrão, o id). Tudo o que um estilo pode definir vale por tabela: tipografia, fundos, bordas, fios, raio dos cantos, margem interna e o comportamento de transbordamento com os seus textos de continuação. resolveTableStylesConfig(styles, tableStyle, resolvedBodyText, locale?) devolve a lista resolvida, pickTableStyle(resolved, styleId) devolve o estilo em que uma tabela é composta, e stripTableStylesDefaults descarta os campos não definidos (mantém um campo igual ao seu padrão embutido, que ainda substitui um valor diferente de tableStyle).

#Tabelas mais altas que a página

Uma tabela flutuante que não cabe na página nova que lhe é oferecida não é espremida nem transborda: com overflow: 'split' (o padrão), o motor a corta entre linhas, na última borda que cabe na página, e a continua nas páginas seguintes, quantas forem necessárias. Cada parte de continuação repete as linhas de cabeçalho da tabela (TableModel.headerRowCount, ou as linhas iniciais formadas por células de cabeçalho quando ele não está definido) e traz a legenda de novo, com continuedSuffix depois da descrição: “Tabela 6-4. Título (cont.)”. Toda parte que continua recebe continuesMarker embaixo, alinhado à direita, na tipografia das notas; a nota da tabela fica para a última parte. Um corte nunca atravessa uma célula mesclada (uma célula com rowspan passa inteira para a parte seguinte), e uma linha que encabeça as linhas abaixo dela (uma única célula ocupando a tabela toda) é levada para a parte seguinte, em vez de ficar sozinha no pé de uma página.

Onde termina a primeira parte. Uma tabela à qual é oferecido o topo de uma coluna vazia depois da sua referência fica com as linhas que cabem ali e continua no espaço seguinte. Ela preenche a coluna até o pé quando tem a coluna só para si: uma parte que deixaria menos de três linhas de texto embaixo dela fica também com essas linhas, em vez de deixar um toco de texto. Quando a coluna já tem outra faixa de flutuantes (uma figura de página inteira no topo da página, por exemplo), a parte para pelo menos três linhas de texto antes do pé, o espaço para texto que qualquer flutuante deixa quando divide a coluna com outro, de modo que a coluna termina com algum texto embaixo da tabela e não só com flutuantes. Para que uma tabela longa vá até o pé da coluna, cite-a num ponto em que a página onde ela começa não tenha outro flutuante (depois da página de uma figura de página inteira, por exemplo), ou ajuste as linhas dela à coluna.

'clip' mantém as linhas iniciais que cabem na página e descarta as outras sem aviso (a nota continua fechando a parte); 'hide' deixa a tabela de fora por completo. Os dois só valem quando a tabela é mais alta que uma página: uma tabela que cabe é posicionada inteira em qualquer modo. Tabelas em linha (placement.position: 'here') não são divididas.

Os textos de continuação têm padrão por idioma do documento (locale, senão o idioma de hifenização): inglês (cont.) / Continued, espanhol (cont.) / Continúa, e da mesma forma em francês, alemão, italiano, português, catalão e holandês (listados em Idioma do documento).

O conteúdo das células é markdown em linha, e uma quebra de linha dentro de uma célula (uma nova linha, ou \\ como nas legendas e notas) começa um novo parágrafo. Um parágrafo que começa com um marcador ou travessão (•, -, *, –) ou com um número (1., 1)) seguido de espaço é composto como item de lista: o marcador é pintado como foi escrito, o texto se pendura nele com o unorderedLists.gap do documento, as linhas seguintes se alinham com o texto, e dois espaços iniciais aninham um nível. Assim, uma célula escrita como • Ofrece elección\n• Acomoda a personas diestras y zurdas sai como uma lista de dois itens. Uma linha só com espaços comuns não acrescenta nada; uma linha com um espaço não separável (U+00A0) é uma linha da célula, como no CommonMark, então 1\n seguido de um espaço não separável faz a linha da tabela ter duas linhas de altura. Um espaço não separável no fim do texto de uma célula mantém a sua largura: 760 e um espaço não separável, alinhados à direita sobre (231), terminam um espaço antes da borda, o que aproxima o 0 do 1. Os algarismos só se alinham exatamente onde o espaço é tão largo quanto o parêntese, e na maioria das fontes ele é mais estreito. (Até o postext 1.4, ambos eram descartados.)

As larguras das colunas pertencem ao modelo da tabela, não ao estilo: TableModel.columnWidths é uma lista opcional de pesos relativos, um por coluna, normalizados na hora do layout; [2, 1, 1] dá à primeira coluna metade da largura. Uma lista ausente, de comprimento errado ou com um peso não positivo recai na divisão em partes iguais. O editor de tabelas mantém a lista alinhada quando colunas são adicionadas ou removidas.

#Construção de modelos de tabela

Um TableModel é uma grade organizada por linhas, e cada célula é diagramada pela sua posição nela: rows[r][c] fica na coluna c. Por isso, uma célula mesclada mantém na grade as células que cobre, cada uma marcada com hiddenBy apontando para a sua célula principal, ao contrário de uma tabela HTML, que as omite. As funções de modelo exportadas por postext preservam essa forma; são funções puras que devolvem um modelo novo: mergeCells(model, { start, end }) e unmergeCell(model, at), addRow, addColumn, removeRow, removeColumn, setCellContent, setCellImage, setCellBackground e setAlignment. As quatro funções de linhas e colunas mantêm as mesclagens inteiras: uma linha ou coluna adicionada dentro de um bloco mesclado o alarga, uma adicionada antes dele o desloca, uma removida dele o encolhe (um bloco que perde a primeira linha ou coluna mantém o conteúdo na sua nova célula superior esquerda), e todo hiddenBy continua apontando para a sua célula principal.

parseTSV(text, options?) monta um modelo a partir de texto separado por tabulações (um intervalo colado de uma planilha): as linhas se dividem nas quebras de linha, as células nas tabulações, e as linhas curtas são completadas para que a grade fique retangular. headerRows transforma as linhas iniciais em linhas de cabeçalho: as células delas recebem isHeader e o modelo recebe headerRowCount, então uma tabela dividida entre páginas as repete.

import { parseTSV, mergeCells } from 'postext';
 
let model = parseTSV('Part\tQty\tNote\nBolt\t4\tM6\nNut\t8\t', { headerRows: 1 });
// model.headerRowCount === 1; model.rows[0][0] é { content: 'Part', isHeader: true }
model = mergeCells(model, { start: { row: 2, col: 1 }, end: { row: 2, col: 2 } });
// rows[2][1] recebe colSpan: 2; rows[2][2] continua na grade com hiddenBy: { row: 2, col: 1 }

tableGridIssues(model) verifica a grade. Devolve uma lista vazia para um modelo correto e, caso contrário, todos os pontos onde a grade se quebra, na ordem das linhas: spanOverlap, uma célula visível sob o colSpan / rowSpan de outra célula (coveredBy indica essa célula), que é o que acontece quando se omite uma célula coberta à maneira do HTML, já que todas as células seguintes deslizam para cima da mesclagem; e missingCells, uma linha que termina antes da última coluna sem uma mesclagem cobrindo o resto, o que deixa um buraco.

import { tableGridIssues } from 'postext';
 
tableGridIssues({
  rows: [
    [{ content: 'A', colSpan: 2 }, { content: 'C' }],
    [{ content: '1' }, { content: '2' }, { content: '3' }],
  ],
});
// => [{ kind: 'spanOverlap', row: 0, col: 1, coveredBy: { row: 0, col: 0 } },
//     { kind: 'missingCells', row: 0, col: 2 }]

Uma tabela usada no documento cuja grade tenha esses problemas é reportada em doc.contentWarnings como raggedTableGrid (veja Avisos no documento).

#Estilo de legenda

A propriedade captionStyle controla as legendas dos recursos (a linha Figure 1 — … embaixo, ou em cima, de imagens, SVGs e tabelas). O rótulo numerado e a descrição usam a mesma fonte e o mesmo tamanho (uma limitação do motor), mas o rótulo pode ter peso, itálico e cor próprios. Família, tamanho e cor herdam o texto corrido quando não são definidos. A legenda pode ficar acima do recurso (a convenção usual nas tabelas) e ser composta sobre uma barra colorida da largura do bloco; uma nota opcional, menor (linha de fonte, créditos: Resource.note), recebe estilo pelo subobjeto note. Um tipo de recurso pode substituir qualquer um desses campos para os seus próprios recursos com ResourceType.captionStyle (veja Tipos de recurso).

const config: PostextConfig = {
  captionStyle: {
    align: 'center',
    labelBold: true,
    labelColor: { hex: '#295AA3', model: 'hex' },
    descriptionItalic: true,
    position: 'above',
    backgroundEnabled: true,
    padding: { value: 0.35, unit: 'em' },
    note: { italic: true, align: 'left' },
  },
};
PropriedadeTipoPadrãoDescrição
fontFamilystringfonte do texto corridoFamília tipográfica da legenda (rótulo e descrição).
fontSizeDimensiontamanho do texto corridoTamanho da fonte da legenda (rótulo e descrição).
colorColorValuecor do texto corridoCor do texto da descrição.
align'left' | 'center' | 'right' | 'justify' | 'start' | 'end''left'Alinhamento horizontal das linhas da legenda. 'justify' estende todas as linhas, menos a última, à largura total. Numa barra de legenda, as linhas se alinham dentro da margem interna dela; uma legenda lateral se alinha dentro da sua própria largura.
gapDimension0.75emEspaço vertical entre o recurso e a legenda.
labelBoldbooleantrueCompõe o rótulo numerado (ex.: Figure 1) em negrito.
labelItalicbooleanfalseCompõe o rótulo numerado em itálico.
labelColorColorValuecolor da legendaCor do rótulo numerado.
descriptionItalicbooleanfalseCompõe o texto da descrição em itálico.
position'above' | 'below''below'Onde fica a legenda. Com 'above', a legenda (e a sua barra) vem primeiro, e o corpo do recurso desce a altura da legenda mais gap; a nota então vai embaixo do corpo.
backgroundEnabledbooleanfalsePinta uma barra atrás da legenda. A barra ocupa toda a largura do bloco e envolve as linhas da legenda mais padding de cada lado.
backgroundColorValuecor principal da paletaCor de preenchimento da barra (só é pintada quando ativada).
paddingDimension0.35emMargem interna entre a borda da barra e o texto da legenda. Ignorada quando a barra está desativada.
noteobject—Estilo da nota do recurso; veja a subtabela abaixo.
labelNumberGapstringespaço não separável; '' num documento em japonêsO que fica entre o rótulo e o número, na legenda e num :ref no texto: Figura 1.7, Fig. 1.7. O chinês e o japonês os compõem colados: '' gera 图1-1 e é o padrão num documento em japonês (図1-1).
labelSeparatorstring'. '; ' ' num documento em japonêsO que vem depois do número, antes da descrição: Figura 1.7. Uma legenda. As legendas em chinês levam um espaço ideográfico, ' ' (图1-1 标题), e as japonesas também, por padrão (図1-1 東京の地図, JLReq §4.3). Um rótulo sem número mantém a sua própria regra: um ponto, a menos que o prefixo já termine em um.

O subobjeto note define o estilo de Resource.note, um trecho curto (fonte, créditos, uma observação) composto embaixo do recurso num tamanho menor. Ele aceita a mesma formatação em linha e as mesmas marcas :ref da legenda e herda a fonte da legenda. Fica embaixo da legenda quando a legenda está embaixo, e embaixo do corpo do recurso quando a legenda está em cima; a sua altura conta para o bloco, então um recurso com nota flutua como uma unidade.

PropriedadeTipoPadrãoDescrição
note.fontSizeDimension0,85 × tamanho da legendaTamanho da fonte da nota.
note.colorColorValuecolor da legendaCor do texto da nota.
note.italicbooleanfalseCompõe a nota em itálico.
note.gapDimension0.35emEspaço entre a nota e o que vem antes dela (legenda ou corpo).
note.align'left' | 'center' | 'right' | 'justify' | 'start' | 'end''left'Alinhamento horizontal das linhas da nota, como align na legenda.

As substituições por tipo são mescladas com mergeCaptionStyle(resolvedCaptionStyle, override, palette?), exportada para os hosts que precisam da mesma resolução fora do pipeline.

#Estilo de diagramas

A propriedade diagramStyle controla a cor dos diagramas SVG incorporados (recursos kind: 'svg'). O seu único recurso hoje é o modo de tinta única: uma passada de recoloração que converte cada cor de um diagrama numa retícula de uma única tinta, para que as figuras se reproduzam fielmente quando o documento é impresso com uma só cor especial.

const config: PostextConfig = {
  diagramStyle: {
    singleInk: true,
    inkColor: { hex: '#295AA3', model: 'hex' },
  },
};
PropriedadeTipoPadrãoDescrição
singleInkbooleanfalseRecolore todos os diagramas SVG incorporados em retículas de uma única tinta.
inkColorColorValueCor principal (#295AA3)A tinta. Por padrão é a cor principal da paleta do documento (ligada à paleta por paletteId: 'main-color'), de modo que trocar essa amostra de cor da paleta tinge de novo os diagramas junto com os títulos e os trechos em negrito.

#Como funciona a tinta única

Com singleInk ativado, cada cor do código SVG é reescrita como uma retícula de inkColor cuja intensidade é 1 − luminância relativa (coeficientes Rec. 709 aplicados aos canais com codificação gama, uma aproximação perceptual mais que suficiente para mapear retículas). O mapeamento preserva o valor percebido: o branco vira o branco do papel, o preto vira a tinta cheia, e os preenchimentos claros continuam claros, seja qual for o matiz original. Um fundo amarelo-claro vira uma retícula clara da tinta; um traço escuro se aproxima da tinta cheia.

A recoloração é feita pela função exportada applySingleInkToSvg(svgText, inkHex), que trabalha sem DOM, sobre o código SVG como texto:

  • Os literais hexadecimais #rgb / #rgba / #rrggbb / #rrggbbaa, as funções rgb() / rgba() e as funções hsl() / hsla() são reescritos onde quer que apareçam: atributos de apresentação, style em linha, gradientes, <defs>. As funções podem usar canais inteiros, decimais ou percentuais e a sintaxe com vírgulas ou com espaços, então rgb(11.37%, 20%, 50.59%) (como o Cairo escreve) e rgb(51 102 153 / 50%) também são recoloridos. Uma função tingida é reescrita como rgb(…) ou rgba(…).
  • As palavras-chave white e black só são substituídas onde aparecem como valores de pintura (fill, stroke, stop-color, flood-color, color, como atributos ou propriedades de estilo em linha), nunca dentro do conteúdo de texto ou de rótulos.
  • none, transparent e currentColor ficam intactos, assim como as outras cores nomeadas (red, steelblue…) e o preto padrão de uma forma ou texto que não define preenchimento. Dê a esses elementos uma cor explícita para que sejam recoloridos.
  • Os canais alfa são preservados (os dígitos de #rgba / #rrggbbaa e os componentes alfa de rgba(…) passam sem mudança; um alfa percentual é escrito como número).
  • Quando inkHex não pode ser interpretado, a entrada é devolvida sem mudança.
  • O resultado leva data-postext-single-ink="#…" (a tinta) no <svg> raiz, e um código que já o leva é devolvido como está, seja qual for a tinta indicada. O mapeamento não é idempotente (uma segunda passada clareia todas as cores, e o preto sai com cerca de dois terços da tinta), por isso uma imagem é recolorida uma única vez, seja pelo seu código, seja pelos renderizadores, o que chegar primeiro. (A marca é nova no postext 1.5; o código recolorido pela 1.4 não a tem.)
import { applySingleInkToSvg } from 'postext';
 
const recoloured = applySingleInkToSvg(svgText, '#295AA3');
applySingleInkToSvg(recoloured, '#295AA3') === recoloured; // true: nunca duas vezes

A tinta única vale nos três renderizadores: o renderizador de PDF recolore os bytes SVG que resourceBytes lhe entrega antes de desenhá-los como vetores, e os renderizadores de canvas e HTML tingem as imagens SVG que pintam quando você pede (veja Tinta única no canvas e no HTML), de modo que o PDF exportado corresponde à visualização na tela.

O resolvedor e o redutor seguem o padrão das outras seções, junto com os tipos DiagramStyleConfig / ResolvedDiagramStyleConfig:

import {
  DEFAULT_DIAGRAM_STYLE_CONFIG,
  resolveDiagramStyleConfig,
  stripDiagramStyleDefaults,
  applySingleInkToSvg,
} from 'postext';
import type { DiagramStyleConfig, ResolvedDiagramStyleConfig } from 'postext';
 
const resolved = resolveDiagramStyleConfig(config.diagramStyle);
// => { singleInk: false, inkColor: { hex: '#295AA3', model: 'hex', paletteId: 'main-color' } }
 
const minimal  = stripDiagramStyleDefaults(config.diagramStyle);
// => undefined quando tudo coincide com os padrões

#Tinta única no canvas e no HTML

Os renderizadores de canvas e de HTML recebem as imagens já decodificadas (registerResourceImage) ou como URLs (resourceImageUrl), e não o código SVG. Quando você pede, eles aplicam o mesmo mapeamento ao que desenham:

  • Canvas (renderPage, renderPageToCanvas, renderToCanvas). Cada imagem SVG a que a tinta se aplica (uma figura, a imagem de uma célula de tabela, uma imagem de design, o ícone ou marcador de um boxe) é rasterizada no tamanho em que foi posicionada, e os seus pixels são tingidos com a tinta, tenha ela sido registrada como <img> ou como ImageBitmap. O bitmap tingido vai para o cache como qualquer rasterização vetorial. Uma imagem bitmap nunca é tingida.
  • HTML (renderToHtml, renderToHtmlIndexed). Cada <img> SVG recebe filter: url(#pt-ink-…), que aponta para um feColorMatrix levado pela sua página: um <svg> de tamanho zero com o <filter>, colocado no início da página e, na saída indexada, parte do decorationHtml da página. Toda página o leva enquanto a tinta única estiver valendo, tenha ou não uma imagem, para que um host que atualiza os blocos um a um nunca traga uma imagem sem o seu filtro.

Nunca tingida duas vezes. Até o postext 1.4, os renderizadores de canvas e de HTML pintavam as imagens como as recebiam, então os hosts recoloriam o código por conta própria com applySingleInkToSvg antes de entregá-lo. Os adaptadores de pacote e o Sandbox ainda fazem isso, porque a passada sobre o código dá exatamente as cores do PDF (veja o último parágrafo abaixo). Por isso, uma imagem é tingida uma única vez, segundo três regras que valem nos três renderizadores:

  • Código marcado não é mexido. O renderizador de PDF recolore resourceBytes com applySingleInkToSvg, então bytes SVG já recoloridos são desenhados como estão. No canvas e no HTML, uma imagem carregada de uma URI de dados SVG cujo código leva a marca também nunca é tingida.
  • Desligada, a menos que seja pedida, no postext 1.x. O canvas tinge uma imagem SVG registrada sem indicação própria só quando a renderização passa singleInk: true (RenderPageOptions), e uma registrada com registerResourceImage(id, img, { singleInk: true }) em qualquer renderização. O renderizador de HTML tinge quando renderToHtml recebe singleInk: true, ou quando o seu resolvedor resourceImageUrl leva singleInk: true. Um host escrito para a 1.4, que recolore o código e registra a imagem decodificada sem indicação, mantém a sua saída. A próxima versão principal vai tingir por padrão.
  • singleInk: false nunca é tingida. Por trás de uma URL blob ou de rede, o código não pode ser lido de volta; por isso, uma imagem que você mesmo recoloriu e decodifica assim é registrada com singleInk: false, como fazem registerBundleImages e o Sandbox. bundleImageUrl(bundle) devolve um resolvedor que leva singleInk: false, e bundleResourceBytes entrega ao PDF os bytes do próprio pacote, que o renderizador de PDF recolore uma vez.

Os dois renderizadores sabem o tipo de cada imagem pela VDT: uma figura e uma imagem de célula levam o tipo do seu recurso, e um bloco de imagem de design leva imageKind ('svg' ou 'bitmap'), tirado do seu recurso no momento do layout. Numa VDT construída antes de existir imageKind, o canvas trata como SVG uma imagem de design registrada como fonte vetorial, e o renderizador de HTML, uma cuja URL é uma URI de dados SVG ou termina em .svg.

Ou você recolore o código, ou deixa os renderizadores tingirem a imagem original, nunca os dois:

import { applySingleInkToSvg, registerResourceImage, renderPage, renderToHtml } from 'postext';
 
// SVG original: tingido enquanto diagramStyle.singleInk estiver ativado…
registerResourceImage('diagram.svg', rawImg, { singleInk: true });
// …ou registre-o sem indicação e peça em cada renderização.
registerResourceImage('diagram.svg', rawImg);
const canvas = renderPage(doc.pages[0], doc, { singleInk: true });
 
// Recolorido antes de decodificar (como fazem os hosts do postext 1.4): pintado como está.
const inked = applySingleInkToSvg(svgText, ink);
registerResourceImage('diagram.svg', await decode(inked), { singleInk: false });
 
// O renderizador de HTML com URLs para o código original.
const html = renderToHtml(doc, { resourceImageUrl: urlFor, singleInk: true });

renderToHtml tira o seu padrão de singleInk da indicação do próprio resolvedor, então bundleImageUrl(bundle) não precisa de opção.

Para cada cor que a passada sobre o código reescreve (valores hexadecimais, rgb() e hsl(), white e black; veja Como funciona a tinta única), o mapeamento por pixel dá o mesmo resultado, bordas suavizadas e gradientes incluídos. Os dois diferem onde a passada sobre o código deixa uma cor intacta: cores nomeadas além de white e black, currentColor, formas e textos sem preenchimento (desenhados no preto padrão) e bitmaps embutidos no SVG são tingidos na tela, mas mantêm a sua cor no PDF. Dê a cada elemento de um diagrama uma cor explícita em hexadecimal, rgb() ou hsl() para obter uma saída idêntica. Quando o canvas não consegue ler os pixels de volta (um <img> de outra origem, carregado sem CORS), a imagem é pintada sem tingir.

#Estilo de vídeo

A propriedade videoStyle define como os recursos de vídeo são impressos (a marca de reprodução e o código QR sobre o pôster, e se o pôster é um link para o vídeo) e o que os players oferecem no visualizador HTML e no EPUB.

const config: PostextConfig = {
  videoStyle: {
    playMark: { shape: 'rounded', position: 'top-left', size: { value: 10, unit: 'mm' } },
    qr: { position: 'bottom-right', size: { value: 20, unit: 'mm' }, errorCorrection: 'Q' },
    player: { download: false, privacy: true },
  },
};
PropriedadeTipoPadrãoDescrição
playMarkVideoPlayMarkConfigveja abaixoA marca impressa sobre o pôster para indicar que ele se reproduz.
qrVideoQrConfigveja abaixoO código QR impresso sobre o pôster: abre a página do vídeo no YouTube ou no Vimeo, ou o endereço de produção de um arquivo.
linkPosterbooleantrueTransforma o pôster num link para o vídeo: uma anotação de link sobre ele no PDF e um <a> em volta dele no HTML e no EPUB, onde quer que o pôster apareça.
html'player' · 'poster''player'O que a saída HTML compõe para um vídeo: o seu player, ou o pôster impresso com as suas sobreposições.
playerVideoPlayerOptionsveja abaixoAs opções de player de todos os vídeos; o video.player de cada vídeo se sobrepõe a elas.

#Marca de reprodução

PropriedadeTipoPadrãoDescrição
enabledbooleantrueImprime a marca.
shape'circle' · 'rounded' · 'triangle''circle'Um disco com um triângulo, um retângulo arredondado com um triângulo (1,45 vez mais largo que alto) ou só o triângulo, contornado na cor de fundo.
positionVideoOverlayPosition'center''center', um canto ('top-left', 'top-right', 'bottom-left', 'bottom-right') ou o meio de um lado ('top', 'bottom', 'left', 'right'). As posições são físicas: o canto superior direito é o canto superior direito também num livro da direita para a esquerda.
sizeDimension12mmAltura da marca; nunca mais de 40% do lado menor do pôster.
insetDimension4mmDistância das bordas do pôster, num canto ou num lado.
colorColorValuebrancoO triângulo.
backgroundColorValueCor principalO disco ou retângulo atrás dele; o contorno do triângulo quando ele está sozinho. Ligada à paleta por padrão.
backgroundOpacitynumber0.9Opacidade do fundo, de 0 a 1.

#Código QR

PropriedadeTipoPadrãoDescrição
enabledbooleantrueImprime o código. Um arquivo sem endereço de produção fica sem código.
positionVideoOverlayPosition'bottom-right'Como na marca de reprodução. Dê posições diferentes às duas.
sizeDimension18mmLado do código com a sua zona de silêncio; nunca mais de 45% do lado menor do pôster. As câmeras de celular leem módulos a partir de um terço de milímetro: um endereço de 30 caracteres gera um código de 29 módulos, então 18 mm com uma zona de silêncio de 2 dão módulos de 0,55 mm.
insetDimension3mmDistância das bordas do pôster.
errorCorrection'L' · 'M' · 'Q' · 'H''M'Quanto do código pode estar danificado ou coberto e ainda ser lido: cerca de 7%, 15%, 25% ou 30%. É elevado automaticamente enquanto o código mantiver o mesmo número de módulos.
quietZonenumber2Módulos claros em volta do código, sobre a sua placa (0–8). A placa se destaca do pôster, então os quatro módulos que a norma pede sobre papel livre não são necessários.
colorColorValuepretoOs módulos escuros. Mantenha-os escuros sobre uma placa clara: a maioria dos leitores não lê códigos invertidos.
backgroundColorValuebrancoA placa.
radiusDimension1mmRaio dos cantos da placa.

O código é gerado pelo próprio motor (encodeQr(text, level): modo byte, UTF-8, versões 1 a 40, a máscara com a menor penalidade) e desenhado como vetores: o canvas preenche um único caminho com as sequências de módulos, o PDF usa um drawSvgPath e o HTML um <path> com shape-rendering="crispEdges", então ele fica nítido em qualquer tamanho de impressão.

#Opções do player

VideoPlayerOptions, em videoStyle.player e no video.player de cada vídeo. O player HTML5 de um arquivo respeita todas; os players do YouTube e do Vimeo respeitam o que os seus parâmetros de incorporação permitem.

PropriedadePadrãoRespeitada porDescrição
controlstrueYouTube, Vimeo, arquivosMostra os controles do player.
downloadtruearquivosOferece o botão de download do navegador (controlslist="nodownload" quando desativado). Ele esconde o botão; não protege o arquivo. O YouTube e o Vimeo nunca oferecem download.
fullscreentrueYouTube, Vimeo, arquivosOferece a tela cheia (fs=0, o allowfullscreen do iframe, nofullscreen).
playbackRatetrueVimeo, arquivosOferece o menu de velocidade (speed=0, noplaybackrate).
pictureInPicturetrueVimeo, arquivosOferece picture-in-picture (pip=0, disablepictureinpicture).
remotePlaybacktruearquivosOferece transmitir para outra tela (disableremoteplayback).
autoplayfalseYouTube, Vimeo, arquivosComeça a tocar sozinho, sempre sem som, como os navegadores exigem.
mutedfalseYouTube, Vimeo, arquivosComeça com o som desligado.
loopfalseYouTube, Vimeo, arquivosVolta a tocar desde o início ao terminar.
exclusivetrueFolio, visualizador HTML, EPUB (onde há scripts); arquivosIniciar este vídeo pausa os outros que estão à vista, então só um toca por vez. false deixa que ele toque junto com os outros: os clipes silenciosos em loop de uma página, vários ao mesmo tempo. Desde o postext 1.18.
preload'metadata'arquivosQuanto o navegador carrega antes de tocar: 'none', 'metadata' ou 'auto'.
privacytrueYouTube, VimeoIncorporações com privacidade reforçada: YouTube a partir de youtube-nocookie.com, Vimeo com dnt=1.

Um EPUB mantém só os atributos que o seu esquema conhece: um arquivo toca com controls, autoplay, muted, loop, playsinline e preload, mais a marca data-pt-alongside, e o sistema de leitura decide o resto.

Um vídeo que não é exclusivo leva data-pt-alongside na saída HTML. coordinateVideoPlayback(root) faz os players sob root (o elemento que contém a saída de renderToHtml) seguirem a regra: iniciar um vídeo exclusivo pausa todos os outros que estiverem tocando, e iniciar um que toca junto pausa só os exclusivos. Devolve uma função que para de escutar. playsAlongside(el) e videosToPause(started, videos, alongside) dão a mesma regra a um host com players próprios. No Folio, um vídeo que toca sozinho e junto com os outros (autoplay com exclusive: false) começa, sem som, cada vez que a sua página entra em vista e para quando a página é virada, vários ao mesmo tempo, e loop o faz tocar de novo desde o início. Uma página ou capítulo de EPUB com vídeos a coordenar (dois ou mais, um deles exclusivo) vincula a mesma regra como um pequeno script, scripts/videos.js (a exportação VIDEO_PLAYBACK_SCRIPT), e o pacote declara esse documento como scripted. Um sistema de leitura que executa scripts faz os vídeos desse documento seguirem a regra, mas não os de uma página oposta, que é outro documento; um que não executa scripts toca cada vídeo por conta própria, como os players do YouTube e do Vimeo sempre fazem.

O resolvedor e o redutor seguem o padrão das outras seções, junto com os tipos VideoStyleConfig / ResolvedVideoStyleConfig:

import {
  DEFAULT_VIDEO_STYLE_CONFIG,
  DEFAULT_VIDEO_PLAYER_OPTIONS,
  resolveVideoStyleConfig,
  resolveVideoPlayerOptions,
  stripVideoStyleDefaults,
} from 'postext';
 
const resolved = resolveVideoStyleConfig(config.videoStyle);
const player = resolveVideoPlayerOptions(resource.video?.player, resolved.player);
const minimal = stripVideoStyleDefaults(config.videoStyle); // undefined quando tudo é padrão

#Estilos de parágrafo

A propriedade paragraphStyles declara estilos nomeados que um documento aplica a uma sequência de parágrafos com um contêiner :::paragraphs{style="…"}: bibliografias, glossários, notas, qualquer bloco de entradas que peça fonte, peso, inclinação, tamanho, entrelinha, maiúsculas, versaletes ou recuo deslocado próprios. Todos os campos tipográficos são opcionais e herdam o texto corrido quando não são definidos, então um estilo só declara o que difere do texto corrido.

const config: PostextConfig = {
  paragraphStyles: [
    {
      id: 'bibliography',
      name: 'Bibliography',
      fontSize: { value: 7, unit: 'pt' },
      lineHeight: { value: 1.2, unit: 'em' },
      hangingIndent: { value: 2, unit: 'em' },
      spaceBetween: { value: 0.25, unit: 'em' },
      marginTop: { value: 1, unit: 'em' },
      marginBottom: { value: 1, unit: 'em' },
    },
  ],
};
## References
 
:::paragraphs{style="bibliography"}
Knuth, D. E. (1984). *The TeXbook*. Addison-Wesley.
 
Bringhurst, R. (2004). *The Elements of Typographic Style*. Hartley & Marks.
:::
PropriedadeTipoPadrãoDescrição
idstringobrigatórioIdentificador referenciado por :::paragraphs{style="…"}.
namestringidNome legível, só para interfaces de edição.
fontFamilystringfonte do texto corridoFamília tipográfica. Os seus pesos são fontWeight / boldFontWeight, abaixo (os do texto corrido quando não definidos).
fontSizeDimensiontamanho do texto corridoTamanho da fonte.
lineHeightDimensionentrelinha do texto corridoEntrelinha. em/rem são relativos ao tamanho de fonte do próprio estilo, então um 1.5em herdado se estreita junto com um tamanho menor.
colorColorValuecor do texto corridoCor do texto. Os trechos em negrito e itálico mantêm as cores de ênfase do texto corrido, a menos que boldColor / italicColor definam as do estilo.
textAlign'left' | 'justify' | 'center' | 'right' | 'start' | 'end'alinhamento do texto corridoAlinhamento horizontal. 'center' e 'right' compõem todas as linhas em bandeira pelo outro lado: uma dedicatória, um bloco de assinatura. Num parágrafo da direita para a esquerda, 'left' é o seu lado inicial, a direita.
boldColorColorValuebodyText.boldColorCor dos trechos em negrito (uma lista de autores com os nomes na cor da casa).
italicColorColorValuebodyText.italicColorCor dos trechos em itálico (…); num estilo italic, dos trechos que ficam em redondo. Não acompanha color: um estilo colorido cujos itálicos devam ficar na sua cor define os dois.
fontWeightnumberbodyText.fontWeightPeso do texto regular (100–900): uma pergunta em seminegrito numa folha de exercícios, uma epígrafe em light.
boldFontWeightnumberbodyText.boldFontWeightPeso dos trechos em negrito (…).
italicbooleanfalseCompõe os parágrafos em itálico: rubricas de teatro, uma epígrafe. Um trecho em itálico … dentro deles fica em redondo, como numa citação em bloco.
smallCapsbooleanfalseCompõe os parágrafos em versaletes: minúsculas como maiúsculas a 70% do tamanho, maiúsculas no tamanho cheio, desenhadas da mesma forma em todos os renderizadores (veja Versaletes): uma lista de personagens, as entradas de um glossário.
hyphenationbooleanhifenização do texto corridoHifeniza quando justificado (usa o idioma do documento).
indentDimension0Recuo de todas as linhas a partir da borda esquerda da coluna (ou do boxe em que os parágrafos estão); em é o tamanho do próprio estilo. Os recuos de primeira linha e deslocado são medidos a partir dele, então um verso recuado pode pendurar a sua continuação mais fundo que o próprio início: indent: 1.5em com hangingIndent: 2.5em põe o verso a 1,5 em e a continuação a 4 em. Um valor negativo conta como 0.
endIndentDimension0Recuo de todas as linhas a partir do lado final (a direita de uma linha horizontal, o pé de uma vertical); em é o tamanho do próprio estilo. Com textAlign: 'end', coloca uma linha alguns caracteres acima do pé, o 地からN字上げ da data ou da assinatura de uma carta japonesa. Desde o postext 1.16.
firstLineIndentDimensionrecuo de primeira linha do texto corridoRecuo da primeira linha, a partir de indent. Ignorado quando hangingIndent é diferente de zero.
hangingIndentDimension0Recuo aplicado a todas as linhas menos a primeira, a partir de indent: a forma clássica de bibliografias e glossários.
spaceBetweenDimension0Espaço vertical entre parágrafos consecutivos dentro do contêiner. 0 deixa as entradas encostadas.
marginTopDimension0Espaço acima do primeiro parágrafo do contêiner. Se funde com o espaçamento já pendente e desaparece no topo de uma coluna, como qualquer outra margem.
marginBottomDimension0Espaço mínimo abaixo do último parágrafo do contêiner. Como ele se combina com o espaço do bloco seguinte ao contêiner é definido por bodyText.paragraphContainerSpacing.
snapToGridbooleantrueDevolve o fluxo à grade de linhas de base embaixo do contêiner, sendo o espaço abaixo um mínimo. false mantém o espaço exato: o texto depois do contêiner fica fora da grade até o próximo bloco que se ajusta a ela (um título, o fim de uma lista, uma fórmula em destaque), para um documento que corre fora da grade ou um grupo com entrelinha própria. Dentro de um boxe, que não tem grade, não muda nada.
textTransform'none' | 'uppercase''none'Caixa das letras dos parágrafos: 'uppercase' os compõe em maiúsculas (uma lista de personagens, uma linha de rubrica), incluindo as palavras de um chip e o rótulo de um :ref. Letra por letra, para que o mapa de origem do editor continue um para um: uma letra cuja maiúscula é mais longa (ß) fica como foi escrita. As fórmulas matemáticas não são alteradas, e um cabeço que lê o parágrafo como marca ({firstMark.style}) recebe o texto como foi escrito; o textTransform próprio de um texto de design o compõe em maiúsculas.
wordBreak'normal' | 'keep-all'cjk.wordBreakOnde as linhas CJK dos parágrafos quebram entre caracteres (veja cjk.wordBreak): 'keep-all' para uma cartilha em kana com espaços entre frases citada num livro de prosa comum, 'normal' para o contrário. Desde o postext 1.16.

Uma peça de teatro compõe as rubricas em itálico e a lista de personagens em versaletes:

paragraphStyles: [
  { id: 'direction', italic: true, fontSize: { value: 9, unit: 'pt' } },
  { id: 'cast', smallCaps: true, textAlign: 'center', fontWeight: 600 },
],
:::paragraphs{style="direction"}
Elsinore. A platform before the castle. *Francisco* at his post.
:::

A rubrica sai em itálico e o nome dentro dela em redondo; os pesos, italic e smallCaps de um estilo também valem dentro dos boxes.

Um livro de poesia recua alguns versos e pendura a continuação de um verso longo demais para a medida mais fundo que o próprio verso. indent recua todas as linhas do parágrafo, e o recuo deslocado conta a partir dali:

paragraphStyles: [
  { id: 'verse', textAlign: 'left', firstLineIndent: { value: 0, unit: 'em' }, hangingIndent: { value: 4, unit: 'em' } },
  { id: 'verse-indented', textAlign: 'left', indent: { value: 1.5, unit: 'em' }, hangingIndent: { value: 2.5, unit: 'em' } },
],

Um verso em verse-indented começa a 1,5 em e a sua continuação a 4 em, na altura das continuações dos versos em verse. Sem indent, um estilo pode recuar a primeira linha ou pendurar as outras, não as duas coisas: firstLineIndent é ignorado assim que hangingIndent é definido.

#O contêiner :::paragraphs

Uma linha :::paragraphs{style="<id>"} abre o contêiner e uma linha só com ::: o fecha; cada parágrafo entre as duas recebe o estilo nomeado, enquanto títulos, listas e outros blocos lá dentro mantêm o estilo de sempre. Os contêineres podem ficar aninhados dentro de outros contêineres delimitados. Um id de style desconhecido não é erro: os parágrafos saem como texto corrido simples.

A delimitação também aceita align (start, end, left, right, center, justify), indent e endIndent (números sem unidade são em), com ou sem estilo: com estilo, eles o substituem; sem, valem sobre o estilo do contêiner que o envolve, ou sobre o estilo de texto onde a delimitação está (o texto corrido, uma parte, uma seção com estilo ou um boxe). :::paragraphs{align=end} compõe um bloco encostado no fim da linha (地付き) e :::paragraphs{align=end endIndent=1}, um caractere antes dele. Desde o postext 1.16.

Dentro do contêiner, o fluxo sai da grade de linhas de base (uma entrada de 7 pt com entrelinha de 1,2 em não cabe numa grade de 8 pt/1,5 em), e o último parágrafo devolve o fluxo à grade (a grade prevalece; o espaço abaixo é um mínimo, a mesma convenção que os títulos seguem). As entradas se dividem entre colunas e páginas como os parágrafos do texto corrido, com a mesma proteção contra órfãs e viúvas; um título logo antes do contêiner fica junto do seu primeiro parágrafo.

O espaço embaixo do contêiner é o maior entre o spaceBetween e o marginBottom do estilo e o espaçamento entre parágrafos do texto em volta (uma linha quando bodyText.paragraphSpacing está ativado), e ele se funde com o espaço que o bloco seguinte guarda acima de si, como faz o espaço entre dois parágrafos do texto corrido: um título depois de uma bibliografia fica à distância do seu próprio marginTop abaixo da última entrada (ou do espaço do estilo, quando este é maior), e um parágrafo depois de um grupo de entradas mais apertadas mantém o espaçamento entre parágrafos do texto. O fluxo volta à grade primeiro embaixo do texto, e o que esse ajuste não cobriu é levado adiante em linhas inteiras da grade, para que o texto depois do contêiner caia na grade. Até o postext 1.4, o espaço do estilo era colocado embaixo da última linha antes do ajuste, o espaço superior do bloco seguinte era somado embaixo dele, e o espaçamento entre parágrafos ficava de fora; bodyText.paragraphContainerSpacing: 'add' mantém essa regra, e as configurações salvas antes dela são lidas com ela. Um estilo com snapToGrid: false não se ajusta à grade: o texto depois do contêiner fica exatamente à distância definida, fora da grade até o próximo bloco que se ajusta. Um contêiner que fecha com uma lista é composto como na 1.4 em qualquer das duas regras: a lista mantém o seu próprio espaço abaixo, e marginBottom vem depois desse espaço, fundindo-se com o do bloco seguinte.

Dentro de um :::callout, o contêiner aplica as margens do seu estilo da mesma maneira: marginTop e marginBottom se fundem com o espaçamento dos blocos em volta (uma margem negativa os aproxima), e um contêiner que abre o boxe não recebe margem superior, como no topo de uma coluna. Um boxe não tem grade de linhas de base à qual voltar, então o espaço abaixo do último parágrafo é o maior entre marginBottom, spaceBetween e o espaçamento entre parágrafos do próprio boxe (o seu body.paragraphSpacing, uma linha do seu texto; fica de fora com paragraphContainerSpacing: 'add'), ou a margem superior do bloco seguinte, quando esta é maior ainda; um marginBottom negativo puxa o bloco seguinte para cima. (Até o postext 1.4, um contêiner dentro de um boxe ignorava as duas margens.)

const resolved = resolveParagraphStylesConfig(config.paragraphStyles, resolvedBodyText);
// => todo campo não definido é preenchido a partir do texto corrido resolvido
 
const minimal  = stripParagraphStylesDefaults(config.paragraphStyles);
// => undefined quando a lista está vazia; margens zero e `name === id` descartados

#Estilos de chip

A propriedade chipStyles declara os estilos nomeados do :chip[text]{style="…"} em linha: as caixinhas arredondadas e coloridas de um banco de palavras, de uma tecla, de uma etiqueta (a sintaxe e as regras de quebra de linha estão na referência do formato do documento). Um estilo, chip, vem por padrão (fundo azul-claro com um filete na cor principal, cantos levemente arredondados, texto igual ao das palavras em volta), então :chip[…] funciona sem nenhuma configuração; declarar chipStyles substitui essa lista padrão. Um chip sem style, ou com um id que nenhum estilo declara, recebe o primeiro estilo.

const config: PostextConfig = {
  chipStyles: [
    { id: 'chip', name: 'Word bank' },
    {
      id: 'key',
      name: 'Keyboard key',
      background: { hex: '#fff4d6', model: 'hex' },
      borderColor: { hex: '#8a6d1f', model: 'hex' },
      borderRadius: { value: 2, unit: 'pt' },
      bold: true,
    },
  ],
};
Classify: :chip[battery] :chip[cable] :chip[switch]
 
Press :chip[Ctrl]{style="key"} + :chip[C]{style="key"}.
PropriedadeTipoPadrãoDescrição
idstringobrigatórioIdentificador referenciado por :chip[…]{style="…"}.
namestringidNome legível, só para interfaces de edição.
backgroundEnabledbooleantruePinta o fundo da caixa.
backgroundColorValue#e8eef7Fundo da caixa (pode ser ligado à paleta).
borderColorColorValuecor principal da paletaCor do contorno.
borderWidthDimension0.5ptEspessura do contorno; 0 não desenha nenhum. O contorno é traçado por dentro da borda da caixa.
borderRadiusDimension0.3emRaio dos cantos, limitado à metade da altura da caixa (um valor grande gera uma pílula).
paddingXDimension0.3emEspaço entre o contorno e o texto, à esquerda e à direita. Faz parte do avanço do chip.
paddingYDimension0.1emEspaço acima e abaixo da faixa do texto. É pintado fora da caixa da linha: nunca muda a altura da linha.
paddingTop, paddingBottomDimensionpaddingYEspaço acima ou abaixo da faixa do texto, cada um no lugar de paddingY. A faixa vai de 0,8 em acima da linha de base a 0,25 em abaixo dela, então o seu meio fica 0,275 em acima da linha de base, mais baixo que o meio de uma maiúscula (cerca de 0,35 em na maioria das fontes): uma maiúscula ou um algarismo num chip redondo (borderRadius: 1em) parece alto. Um espaço superior maior que o inferior em duas vezes a diferença o centraliza: paddingTop: 0.2em com paddingBottom: 0.05em para uma fonte cujas maiúsculas têm 0,7 em de altura.
fontFamilystringtexto em voltaFamília do texto do chip. Os pesos seguem o texto em volta.
fontSizeDimensiontexto em voltaTamanho do texto do chip; em é relativo ao texto em volta.
colorColorValuetexto em voltaCor do texto do chip. Sem definir, os trechos em negrito e itálico mantêm as cores de ênfase.
boldbooleanfalseCompõe o texto do chip em negrito, além da sua própria marcação.
italicbooleanfalseCompõe o texto do chip em itálico, além da sua própria marcação.
gapDimension0.25emEspaço mínimo mantido entre a caixa e uma palavra ou chip vizinho através de um espaço entre palavras; um espaço mais estreito é completado dentro do avanço do chip, então a justificação nunca o consome. Nada é acrescentado na borda de uma linha nem junto a uma pontuação colada.

As medidas em em da caixa (paddingX, paddingY, borderRadius, borderWidth, gap) são relativas ao tamanho de fonte do próprio chip. A caixa é uma faixa de 0,8 em acima e 0,25 em abaixo da linha de base, aumentada por paddingY e pelo contorno; ela é pintada fora da caixa da linha e nunca muda a entrelinha, então a grade de linhas de base se mantém. Quando a caixa fica mais alta que o passo das linhas, um chip pode encostar num chip da linha de cima ou de baixo: o Sandbox lista um aviso “Chips encostam na linha vizinha” quando dois chips em linhas diferentes se sobrepõem, com a sobreposição em pontos, para que você possa reduzir paddingY, o contorno ou fontSize. Um chip alto sem chip acima ou abaixo dele não é assinalado.

Na VDT, um chip é um segmento de linha de kind: 'chip' cujo campo chip leva os trechos de texto (cada um com a sua string de fonte e a sua largura), a geometria da caixa (boxWidth, ascent, descent, paddingX, borderWidth, borderRadius, as margens de espaçamento) e as suas cores; o text do segmento é um marcador de um caractere, então os deslocamentos em texto simples e os mapas de origem contam um chip como um caractere.

const resolved = resolveChipStylesConfig(config.chipStyles);
// => o estilo embutido `chip` quando não definido; todos os campos preenchidos
 
const minimal  = stripChipStylesDefaults(config.chipStyles);
// => undefined para o padrão embutido; padrões estáticos descartados
 
const style = pickChipStyle(resolved, 'key');
// => o estilo `key`, senão o primeiro

#Estilos de boxe

A propriedade calloutStyles declara os estilos de boxe nomeados que um documento aplica com um contêiner :::callout{type="…"}: notas, dicas, avisos, objetivos de aprendizagem, qualquer conteúdo separado do texto corrido em um boxe com fundo ou contorno. Um estilo neutro, note, vem por padrão (fundo cinza-claro, sem contorno, sem faixa, sem ícone, sem título), de modo que :::callout funciona sem nenhuma configuração; declarar calloutStyles substitui essa lista padrão.

const config: PostextConfig = {
  calloutStyles: [
    { id: 'note', name: 'Note' },
    {
      id: 'objectives',
      name: 'Learning objectives',
      title: 'Objectives',
      stripe: { enabled: true, side: 'left' },
      icon: { kind: 'glyph', glyph: '✓' },
      titleStyle: { textTransform: 'uppercase' },
      lists: { bulletChar: '–' },
    },
    {
      id: 'warning',
      title: 'Warning',
      backgroundEnabled: false,
      border: { enabled: true, color: { hex: '#AA0000', model: 'hex' }, width: { value: 1, unit: 'pt' } },
      borderRadius: { value: 1, unit: 'mm' },
      titleStyle: { color: { hex: '#AA0000', model: 'hex' } },
    },
  ],
};
:::callout{type="objectives"}
- Describe the parts of the lantern.
- Trim the wick at dusk.
:::
 
:::callout{type="warning" title="Do not touch the lens"}
The glass stays hot for an hour after the flame is out.
:::
PropriedadeTipoPadrãoDescrição
idstring—Identificador escolhido por :::callout{type="…"}. Um delimitador com type desconhecido ou ausente usa o primeiro estilo configurado (o Sandbox sinaliza os tipos desconhecidos).
namestringidNome legível (só na interface do editor).
titlestring''Texto padrão do título; vazio significa sem título. O atributo title do delimitador o substitui em cada ocorrência.
span'column' | 'page' | 'side''column'Extensão horizontal: a coluna, a largura total do conteúdo ou a coluna lateral reservada a flutuantes de um layout de coluna e meia (layout.sideColumnRole: 'floats'); nesse caso o boxe sai do fluxo e se empilha nessa coluna, ao lado do texto que interrompe. Pode ser substituído em cada ocorrência pelo atributo span. Em layouts de várias colunas, um boxe 'page' vira um bloco de largura de página: divide a página em faixas de colunas e ocupa uma coluna própria de largura total (veja a seção sobre o contêiner, abaixo).
columnsnumber1Quantas colunas adjacentes um boxe flutuante (placement 'auto', 'top' ou 'bottom', com span: 'column') ocupa, como faz placement.columns para uma figura: o boxe de uma matéria em três das cinco colunas de um jornal. Tantas colunas quantas a página tiver, ou mais, fazem dele um boxe da largura da página. Um boxe composto no fluxo ('here') fica na sua coluna. Pode ser substituído em cada ocorrência pelo atributo columns. Desde o postext 1.18.
placement'here' | 'auto' | 'top' | 'bottom' | 'fixed''here'Onde o boxe fica. 'here' o compõe em linha, no fluxo; 'top' / 'bottom' o fazem flutuar como um recurso ('auto' toma a faixa que ficar livre primeiro, no alto ou no pé): ele sai do fluxo onde aparece e toma a primeira faixa livre a partir desse ponto (o pé da página atual, ou o alto / o pé da próxima página que o fluxo abrir), e o texto que vem depois preenche a página em volta dele; 'fixed' o ancora em coordenadas da página por meio de fixed, abaixo, fora do fluxo das colunas. Os detalhes estão na seção sobre o contêiner. Pode ser substituído em cada ocorrência pelo atributo placement.
sideAtColumnEnd'before' | 'after''before'Onde fica um boxe lateral (span: 'side') quando o texto depois do seu delimitador não continua na mesma coluna: a coluna não tem mais espaço para ele, ou as regras de quebra o mandam adiante (um parágrafo que as regras de viúvas e órfãs movem inteiro, um título mantido com o seu texto). 'before' o mantém no seu delimitador, nessa página, ao lado do texto anterior, subindo a partir do pé da coluna quando não cabe abaixo do delimitador: o lugar de uma glosa escrita depois do trecho que explica. 'after' o alinha com a primeira linha do texto depois do delimitador, na coluna lateral da página em que esse texto continua: o lugar de um número de linha ou de um título marginal escrito antes da sua linha. Quando o texto continua na mesma coluna, os dois põem o boxe no seu delimitador. Um boxe que não é seguido de nada no seu capítulo fica com o texto anterior em qualquer caso, e boxes laterais delimitados um depois do outro mantêm a sua ordem. Até o postext 1.4 todo boxe lateral se comportava como 'before', que continua sendo o padrão: um estilo para glosas mantém os seus boxes na página do trecho que explicam.
fixedPosição de um boxe 'fixed': um ElementAnchor (to: 'container' = a área de conteúdo da página, espelhada nas páginas pares; 'page' = a caixa de refile; 'bleed' = a caixa de sangria; edge: uma das nove bordas do contêiner) mais um offset opcional (dimensões x / y).
floatBarrierbooleanfalseTorna o boxe uma barreira de flutuantes: toda figura ou tabela referenciada antes dele é colocada antes dele (nos espaços livres da página ou, se não houver, em páginas abertas antes do boxe), de modo que nenhum flutuante escapa para depois do boxe que fecha um capítulo (normalmente um resumo de “pontos-chave”). Aberturas de capítulo, :::part e o fim do documento são sempre barreiras.
Um boxe span: 'page' em uma página de várias colunas corta a faixa sob o texto que ele segue; uma figura de largura total referenciada antes dele fica com esse corte primeiro: o texto é nivelado, a figura se assenta logo onde ele terminou e o boxe continua abaixo dela (ou na página seguinte, quando já não cabe). Uma figura alta demais para vir depois do texto nivelado abre a página seguinte, com o boxe depois dela, e a faixa que ela deixou termina nivelada mesmo assim. Um boxe divisível (keepTogether: false) começa sob o texto e a figura com tantos itens quantos couberem, e o resto continua na página seguinte.
width'fill' | 'auto''fill''fill' ocupa a largura disponível; 'auto' se ajusta ao título (uso como selo) e ignora os filhos.
backgroundEnabled / backgroundboolean / ColorValuetrue / #f4f4f4Preenchimento do boxe.
border{ enabled, color, width }false, #cccccc, 0.5ptContorno do boxe, traçado por dentro da borda do boxe, como a borda de um elemento de caixa (veja Elementos de caixa).
borderRadiusDimension0Raio dos cantos do fundo / do contorno (limitado à metade da largura e da altura do boxe). A faixa o acompanha: em um boxe arredondado, a faixa é recortada pelo quadro arredondado, como o CSS recorta um border-left pelo border-radius. A aba label mantém os cantos retos.
padding{ top, right, bottom, left }0.75em cadaRecuo entre a borda do boxe e o seu conteúdo. Os valores em em são relativos ao tamanho do corpo do boxe.
stripe{ enabled, side, width, color }false, 'left', 1.5em, cor principalFaixa sólida ao longo de uma borda. Uma faixa 'left' / 'right' estreita o conteúdo; uma faixa 'top' o empurra para baixo. 'left' e 'right' são lados do fluxo do corpo (em um livro da direita para a esquerda, 'left' é o lado direito da folha); 'start' e 'end' seguem a direção do próprio boxe, de modo que um :::callout{dir=ltr} em um livro árabe põe uma faixa 'start' à esquerda. Em um boxe com borderRadius, os seus cantos externos são arredondados com os do quadro (até o postext 1.4 ficavam retos e passavam do arredondamento).
icon{ kind, glyph, resourceId, fontFamily, fontWeight, size, width, color, align, position, cornerSide }'none', fonte dos títulos, 400, 1.5em, cor principal, 'top', 'inline', 'right'Um glifo de texto (kind: 'glyph') ou um recurso bitmap / SVG (kind: 'resource' + resourceId) ao lado do conteúdo. Com uma faixa lateral, o ícone é centralizado sobre a faixa; caso contrário, reserva uma coluna própria (size + titleStyle.gap). align: 'center' o centraliza verticalmente em relação ao conteúdo. Uma imagem de recurso é ajustada dentro do quadrado mantendo a proporção, ou dentro de uma caixa width × size quando width está definido (uma tira larga de ícones); um ícone mais alto que o conteúdo faz o boxe crescer para contê-lo (e, com align: 'center', centraliza o conteúdo nele). position: 'corner' pendura o ícone em um canto superior como um selo, com metade dele para fora da borda, sem tirar espaço do conteúdo; um ícone largo (width) é centralizado no canto pela largura com que é desenhado, e no canto esquerdo o título começa depois da sua metade interna (até o postext 1.4 era posicionado pela altura, de modo que uma tira larga ficava pendurada para fora do boxe e sobre o título); cornerSide escolhe o canto: 'right' / 'left', ou 'outer' / 'inner', que seguem a paridade da página das margens espelhadas (externo = direita em uma página ímpar, esquerda em uma par).
marker{ kind, glyph, resourceId, fontFamily, fontWeight, size, color, align, gap, rule }'none', fonte dos títulos, 400, 1.5em, cor principal, 'center', 0.5em, fio desligado (0.5pt, cor principal, comprimento 0)Um segundo ícone desenhado fora do boxe, em uma coluna à esquerda dele, com um rule (fio vertical) opcional entre ele e o boxe: a mãozinha de “toque aqui” ao lado de um selo de autoavaliação. O quadro passa a ser [marker][rule][gap][box], com a altura do mais alto dos três; align os centraliza entre si ou os alinha pelo alto. rule.length é um mínimo: o fio sempre cobre pelo menos a altura do boxe.
titleStyle{ fontFamily, fontSize, fontWeight, italic, color, textTransform, gap, letterSpacing, indent, lineHeight }fonte dos títulos, tamanho do corpo, 700, false, cor principal, 'none', 0.5em, 0, 0, 1.2emTipografia do título. gap é o espaço entre o título e o primeiro filho (e o espaço da coluna do ícone). lineHeight é a entrelinha das linhas do título, com o em contado sobre o tamanho do próprio título; a linha de base fica a 0,8 dela abaixo do topo de cada linha, como no texto corrido, de modo que um título na entrelinha do corpo (lineHeight: 12pt sobre uma grade de 12 pt) mantém o boxe com um número inteiro de linhas e o título na grade, enquanto o padrão de 1,2 em acrescenta uma fração de linha a cada boxe. textTransform: 'uppercase' preserva o comprimento. letterSpacing aplica tracking ao título (letterSpacing do canvas / Tc do PDF); indent o afasta para a direita da borda interna do boxe. Um selo de canto pendurado do lado do título (o canto esquerdo) reserva primeiro o seu próprio espaço (a metade interna do selo mais gap), para que o título não o toque em qualquer página em que caia; indent só soma além disso.
body{ fontFamily, fontSize, lineHeight, color, boldColor, italicColor, fontWeight, boldFontWeight, italic, smallCaps, textAlign, hyphenation, paragraphSpacing, firstLineIndent }herda bodyText; italic / smallCaps falseTipografia dos parágrafos e itens de lista dentro do boxe. Todo campo não definido herda o texto do corpo; italicColor define a cor dos trechos em itálico (uma citação em destaque em itálico na cor do boxe). fontWeight / boldFontWeight definem os pesos dos trechos regulares e em negrito; italic compõe o boxe em itálico, com os trechos … voltando ao redondo; smallCaps o compõe em versaletes. Veja Tipografia dentro de um boxe para o que mais o texto do boxe recebe.
lists{ bulletChar, color, indent, gap, itemSpacing, bulletFontSize, bulletFontWeight }herda unorderedListsTipografia das listas dentro do boxe (color, indent, gap e itemSpacing também valem para as listas numeradas). bulletFontSize / bulletFontWeight compõem o glifo do marcador na fonte do corpo do boxe nesse tamanho e peso (um marcador colorido e pesado).
label{ fontFamily, fontSize, fontWeight, color, background, position, height, paddingX, offset, inset, icon, rule }não definido (sem aba)Uma aba na borda superior do boxe que imprime o atributo label do delimitador: o número de um boxe numerado (“BOXE 1-1”). Ela encosta no canto indicado por position ('top-right' / 'top-left'), recuada em inset, sobe offset acima do topo do boxe (esse espaço faz parte do bloco, somado a marginTop, para que a aba o mantenha também no alto de uma coluna), tem height de altura com paddingX de cada lado do texto e pode levar um recurso icon ao lado ({ resourceId, width, gap }, do lado oposto ao canto) e um rule ({ enabled, color, width }) ao longo da borda superior, do canto oposto até ela. Padrões: fonte dos títulos, tamanho do corpo, 700, branco sobre a cor principal, 1.4em de altura, 0.6em de margem interna. A aba é uma forma própria apoiada sobre o quadro, por isso mantém os cantos retos qualquer que seja o borderRadius do boxe.
columnGapDimension1.5emEspaço entre as colunas de um grupo :::columns dentro do boxe (veja a seção sobre o contêiner, abaixo).
marginTop / marginBottomDimension0.75em / 0.75emEspaço acima do boxe (funde-se com a margem do bloco anterior) e espaço mínimo abaixo dele (o espaço exato com snapToGrid: false). Um boxe flutuante (placement: 'top', 'bottom' ou 'auto') mantém o espaço de flutuante, uma linha do corpo, entre a sua faixa e o texto; um marginBottom maior que isso define o espaço sob um boxe em uma faixa superior, e um marginTop maior, o espaço sobre um boxe em uma faixa inferior, arredondado para cima até a grade junto com a faixa. Até o postext 1.4 um boxe flutuante ignorava as suas margens.
snapToGridbooleantrueCom true, o fluxo depois do boxe volta a se encaixar na grade de linhas de base, de modo que o espaço sob ele é marginBottom arredondado para cima até linhas inteiras da grade. Com false, o boxe mantém o seu marginBottom exato, que se funde com a margem superior do bloco seguinte (dois boxes seguidos de um estilo assim ficam exatamente a max(marginBottom, marginTop) um do outro), e o texto depois dele pode ficar fora da grade até o próximo ponto de encaixe (um título, o fim de uma lista), como depois de um título com headings.snapToGrid: false. Pensado para documentos feitos de boxes empilhados (fichas de exercícios, formulários). Vale para os boxes no fluxo; os boxes de largura de página em um layout de várias colunas e os boxes flutuantes, fixos e laterais mantêm a grade, porque as faixas de colunas e as zonas de flutuantes são dispostas sobre ela. As alavancas de equilíbrio de colunas não mudam: um boxe que fecha uma coluna continua sendo empurrado até a última posição da grade da coluna.
keepTogetherbooleantrueCom true, o boxe é mantido inteiro: um boxe que não cabe no espaço restante passa inteiro para a coluna ou página seguinte. Só um boxe mais alto que uma coluna vazia e completa (uma página inteira para um boxe span: 'page') não pode ser mantido inteiro: ele se divide pelas regras de false abaixo em vez de transbordar, começando onde aparece, e uma continuação que caiba em uma coluna passa inteira adiante; um boxe flutuante (placement: 'top' | 'bottom' | 'auto') com essa altura não flutua, fica no fluxo onde aparece. Com false, qualquer boxe pode quebrar entre os seus blocos filhos ou entre as linhas de um parágrafo ou item de lista: o corte mais fundo que couber fecha a coluna atual (ou, para um boxe span: 'page', a página, rente ao pé das colunas) e o resto continua no alto da seguinte, em um boxe próprio (mesmo quadro e faixa, sem ícone e sem título, a menos que repeatTitle o repita), dividindo-se de novo se ainda for alto demais. O texto de cada fragmento mantém, vazia, a coluna que um ícone em linha ocupa no primeiro trecho, para que o boxe tenha uma única medida em todas as páginas. Um corte dentro de um item de lista deixa o marcador com o primeiro trecho. O quadro de cada fragmento compartilha o contentIndex / containerId do delimitador e registra callout.part / callout.continued. Use-o em um boxe longo de “pontos-chave” no fim do capítulo junto com headings.balancing.beforeSpan, ou em um estilo de nota cujos boxes nunca devem empurrar uma figura para fora da página. Um boxe aninhado (um :::callout dentro de outro) é um filho do seu pai: um corte pode cair antes ou depois dele, e dentro dele só quando o seu próprio estilo permite a divisão (keepTogether: false, ou mais alto que uma coluna completa), segundo o seu próprio splitMinLines.
splitMinLinesnumber2Número mínimo de linhas de texto que um fragmento de um boxe dividido (keepTogether: false, ou um boxe indivisível mais alto que uma coluna completa) mantém de cada lado do corte. Ele protege apenas o texto: um lado que contenha pelo menos uma figura, tabela, fórmula em destaque ou boxe aninhado é aceitável qualquer que seja o seu número de linhas, de modo que um boxe de imagens pode deixar uma só em uma página. Um corte dentro de um parágrafo ou item de lista continua contando todas as linhas de cada lado (uma figura ou fórmula ali conta como uma linha) e também deixa pelo menos layout.boxChildSplitMinLines linhas desse parágrafo ou item de cada lado (duas por padrão; este mínimo quando for menor, de modo que 1 permite uma). Com os padrões, um item de duas ou três linhas nunca é dividido, e um de quatro só se divide em duas e duas. Com o padrão, nenhum boxe quebra deixando uma linha de texto solitária no pé de uma coluna ou no alto da seguinte; quando nenhum corte satisfaz o mínimo, o boxe passa inteiro. (Antes do postext 1.5, um corte dentro de um parágrafo verificava só as linhas do lado inteiro, de modo que um item de duas linhas podia se dividir em uma e uma quando outras linhas do boxe completavam o mínimo.)
repeatTitlebooleanfalseRepete o título no alto de cada continuação de um boxe dividido, seguido de continuedSuffix (“Pontos-chave (cont.)”). A repetição usa o estilo do título. Um boxe sem título não repete nada. Veja Marcas em um boxe dividido.
continuedSuffixstring'(cont.)'Texto depois do título repetido, no idioma do documento (locale ou, na falta dele, o idioma de hifenização), como o de uma tabela dividida, e unido ao título como o de uma tabela.
continuesMarkerEnabledbooleanfalsePõe continuesMarker sob a última linha de cada parte de um boxe dividido que continua, dentro do boxe.
continuesMarkerstring'Continued' / 'Continúa'Texto dessa marca (o “(MORE)” de um roteiro), na fonte e no tamanho do corpo do boxe, conforme o idioma do documento.
continuesMarkerAlign'left' | 'center' | 'right''right'Onde a marca fica na largura interna do boxe.
continuesMarkerItalicbooleantrueCompõe a marca em itálico.
numbering{ label, counter, numberingTemplate, resetOn, counterFormat, placement, bold, italic, suffix }não definidoConta os boxes deste estilo como enunciados numerados: teoremas, lemas, definições. Veja Enunciados numerados e demonstrações.
endMarkstring''Uma marca alinhada à direita no fim da última linha do boxe, como uma demonstração termina com '∎' ou '□': na última linha quando cabe depois de um espaço, senão em uma linha própria. Um boxe que termina em uma fórmula em destaque sem número a recebe como etiqueta da fórmula. Com a matemática ativada, os quadrados (∎ □ ■ ▪ ◻ ▫) são desenhados com os glifos do próprio TeX, de modo que uma fonte que não os tenha (os arquivos latin da Fontsource) também os imprime; com a matemática desativada, são compostos na fonte do corpo do boxe.

#O contêiner :::callout

Uma linha :::callout{type="<id>"} abre o boxe e uma linha ::: sozinha o fecha. O delimitador aceita cinco atributos, type (o id do estilo), title (substitui o título do estilo), span, placement e columns (substituem os valores do estilo), e o conteúdo entre as duas linhas é composto dentro do boxe: um título opcional e depois os parágrafos, listas, citações, fórmulas ou recursos incorporados, cada um com a tipografia body / lists do estilo (os títulos mantêm os seus estilos habituais). As margens entre os filhos se fundem como no texto corrido; o interior sai da grade de linhas de base, e o fluxo volta a ela depois do boxe com pelo menos marginBottom abaixo (a grade prevalece; a margem é um mínimo, a mesma convenção que os recursos seguem). Um estilo com snapToGrid: false mantém, em vez disso, o marginBottom exato, e o texto depois do boxe fica fora da grade até o próximo título ou fim de lista. Um título imediatamente antes de um boxe fica junto dele.

Limites desta versão:

  • Um boxe é mantido inteiro, a menos que o seu estilo defina keepTogether: false. Quando não cabe no espaço restante da coluna, passa inteiro para a coluna ou página seguinte, inclusive saindo de uma coluna vazia que as faixas de flutuantes ou um limite de faixa encurtaram, desde que uma coluna completa o comportasse. Um boxe mais alto que uma coluna completa se divide em vez disso, como um divisível; só aquele que nenhum corte consegue dividir (uma figura, tabela ou grupo :::columns mais alto que a coluna, ou um splitMinLines que nenhum corte satisfaz) é colocado mesmo assim e transborda; o layout registra então um aviso calloutOverflow (VDTDocument.warnings), que o Sandbox lista. Um boxe divisível deixa para trás a parte que cabe (filhos inteiros, ou as linhas de um parágrafo até splitMinLines de cada lado; uma figura, tabela ou fórmula em destaque sozinha basta para um lado) e continua em um boxe sem ícone na coluna ou página seguinte, sem o título a menos que o estilo o repita e, opcionalmente, com uma marca sob a parte que deixa (veja Marcas em um boxe dividido).
  • Os flutuantes cedem a um boxe indivisível. Quando o bloco logo depois da referência a uma figura é um boxe keepTogether, um espaço que deixaria o boxe sem nenhuma coluna da faixa atual onde cair (a coluna da referência ou uma vazia depois dela, que o comportava antes do flutuante) não é tomado: a figura passa para o seu próximo espaço, normalmente a página seguinte, e o boxe continua no fluxo, como um compositor o comporia, em vez de empurrar o boxe para fora da página e deixar a coluna só com a figura.
  • span: 'page' em um layout de várias colunas faz do boxe um bloco de largura de página: ele é composto na largura total do conteúdo e divide a página em faixas de colunas. As colunas de texto acima dele são fechadas na linha de corte, o boxe ocupa uma coluna própria de largura total e uma nova faixa de colunas de texto se abre abaixo dele, de modo que o fluxo continua sob o boxe em todas as colunas. Onde as colunas estão niveladas (no alto de uma página, logo abaixo de um título de abertura span: 'page', logo abaixo de outro bloco de largura de página ou logo abaixo de uma faixa de flutuantes superior), o boxe simplesmente corta ali. Chegando no meio da página, com as colunas desiguais, ele é composto como um compositor faria: o texto acima é cortado nivelado em todas as colunas (o motor refaz a colocação com as colunas da faixa encurtadas para o mesmo número de linhas da grade, de modo que o texto passa de coluna em coluna naturalmente e todas as regras de órfãs, viúvas e manter com o seguinte continuam valendo), o boxe ocupa a largura da página e as colunas continuam abaixo dele. O corte custa algumas passadas extras de colocação; quando a linha de corte não deixaria espaço para o boxe mais o mínimo de linhas do corpo para viúvas abaixo dele, ou nenhuma disposição cabe depois de algumas tentativas, o boxe passa para o alto da página seguinte. O texto acima respeita o corte: um parágrafo que não consegue começar nas poucas linhas que uma coluna mantém acima, sob uma figura, passa para a coluna seguinte (a figura fica então sozinha na sua coluna), e quando um bloco ainda ultrapassaria o corte, o corte é feito uma linha abaixo em vez de onde esse bloco termina. Um boxe dividido que abre a faixa também é cortado ali, de modo que o resto dele nunca ultrapassa o corte. Com headings.balancing.beforeSpan (o padrão), a faixa que ele deixa é cortada nivelada atrás dele, como a faixa de fechamento de um capítulo, e, quando o estilo permite a divisão (keepTogether: false), a parte do boxe que cabe sob as colunas niveladas fecha a página e o resto abre a seguinte; com beforeSpan: false, a página que ele deixa é simplesmente equilibrada como de costume, sem forçar uma quebra de página. Um título logo antes de um bloco de largura de página não o acompanha. Em layouts de uma coluna, span: 'page' é simplesmente em linha.
  • placement: 'fixed' tira o boxe do fluxo: ele é composto (width: 'auto' se ajusta ao título; 'fill' toma a largura da coluna de texto sob o ponto de ancoragem) e fixado na página em que aparece no fluxo, na posição descrita por fixed.anchor / fixed.offset (por padrão, o canto inferior esquerdo da área de conteúdo). As colunas de texto que ele cobre cedem essa zona (cortada por baixo, ou por cima quando a coluna ainda está vazia), exatamente como uma faixa de flutuantes; quando a zona já contém texto, um flutuante ou um bloco de largura de página, o boxe passa para a página seguinte. Um boxe fixo que fecha o capítulo (o bloco seguinte é uma abertura de capítulo, um :::part, um boxe barreira de flutuantes ou o fim do documento) primeiro nivela as colunas acima dele (headings.balancing.trailing), de modo que uma página final curta termina nivelada, com o selo embaixo. O quadro e os filhos ficam em page.floats e são renderizados fora do recorte das colunas em todos os renderizadores.
  • placement: 'top' | 'bottom' faz o boxe flutuar como um recurso: ele sai do fluxo onde aparece e toma a primeira faixa livre depois desse ponto, isto é, o pé da página atual ('bottom') ou o alto / o pé da próxima página que o fluxo abrir, na largura da coluna (span: 'column') ou na largura total do conteúdo (span: 'page'); o texto que vem depois preenche a página que ele deixou. O seu quadro e os seus filhos vão para page.floats, como os de um boxe fixo. Um boxe flutuante no alto de uma página nova é colocado antes das figuras que esperam por essa página, e uma figura citada em uma página anterior que então caiba sob ele fica com o resto da página, mesmo que restem menos de três linhas de texto (uma página de galeria: boxe mais figura, sem texto entre eles). Um boxe span: 'side' nunca flutua: ele se empilha ao lado do texto, qualquer que seja a sua posição. Com columns acima de 1 (no estilo ou como atributo do delimitador, desde o postext 1.18), um boxe span: 'column' ocupa esse número de colunas adjacentes: o alto de uma sequência de colunas vazias que começam niveladas, ou o pé da coluna atual e das vazias depois dela, como em :::callout{placement="top" columns="2"}.
  • width: 'auto' se ajusta só ao título; os filhos são ignorados.
  • Um :::callout aninhado dentro de outro boxe é um boxe próprio: é composto com o seu próprio estilo (fundo, contorno, raio, margem interna, faixa, título, ícone, marcador, aba, tipografia) na largura interna total do pai e se empilha como um filho dele, com o seu marginTop / marginBottom se fundindo com os vizinhos. O seu span e o seu placement (do delimitador ou do estilo) são ignorados (um boxe aninhado sempre flui dentro do pai), assim como floatBarrier e snapToGrid. Os boxes se aninham em qualquer profundidade e podem ficar dentro de um grupo :::columns (cada um inteiro, em uma coluna). Quando o pai se divide, o corte cai antes ou depois de um boxe aninhado, ou dentro dele quando o estilo aninhado permite a divisão; cada fragmento redesenha os quadros que atravessa, e um boxe aninhado que continua do fragmento anterior perde o título e o ícone, como uma continuação de primeiro nível.
  • Um grupo :::columns{count=N} … ::: entre os filhos compõe esses filhos em N colunas de mesma largura, separadas por columnGap, dentro do boxe: a sequência é cortada nos limites de bloco ou de linha que melhor nivelam as colunas (um parágrafo ou item de lista cortado no meio continua no alto da coluna seguinte sem o marcador), cada coluna começa no alto do grupo e o grupo tem a altura da coluna mais alta; os filhos depois dele voltam à largura total. Um boxe dividido (keepTogether: false) nunca corta dentro de um grupo. Use-o para um resumo de pontos-chave em duas colunas, ou para as tabelas de um boxe largo postas lado a lado.
  • O sexto atributo do delimitador, label, é impresso na aba de rótulo do estilo (veja label acima): :::callout{type="box" label="BOX 1-1" title="The octet rule"}; sem um estilo label, o atributo é ignorado.

No VDT, o boxe é um bloco de quadro type: 'callout' cuja decoração (fundo, faixa, ícone, título) fica em designOverlay, seguido dos seus blocos filhos na mesma coluna; o quadro e cada filho levam o containerId do delimitador. Um boxe aninhado é um quadro type: 'callout' próprio entre os filhos, seguido dos seus blocos; eles mantêm o containerId do delimitador de primeiro nível (a colocação e o equilíbrio continuam vendo uma só unidade) e acrescentam calloutPath, os ids de contêiner dos delimitadores aninhados em volta deles, do mais externo para o mais interno (o id do próprio quadro aninhado é a última entrada). O PDF com tags dá a cada boxe aninhado um Div dentro daquele do pai. As imagens de ícone são resolvidas como as imagens de recursos: o registro de imagens do canvas, a opção resourceImageUrl do HTML e o provedor resourceBytes do PDF.

const resolved = resolveCalloutStylesConfig(config.calloutStyles, resolvedBodyText, resolvedHeadings, resolvedUnorderedLists, config.locale);
// => todo campo herdado é preenchido a partir das seções resolvidas; o locale
//    opcional escolhe o idioma das strings de continuação
 
const minimal  = stripCalloutStylesDefaults(config.calloutStyles);
// => undefined para o padrão `note` embutido; os padrões estáticos são removidos

#Enunciados numerados e demonstrações

Um estilo com numbering conta os seus boxes, como o amsthm do LaTeX conta os ambientes de teorema (desde o postext 1.19). Cada boxe imprime o seu rótulo e número (“Theorem 2.” abrindo o primeiro parágrafo), e o title do delimitador vem depois do número, entre parênteses: :::callout{type="theorem" title="Bradley–Terry"} começa com “Theorem 2 (Bradley–Terry).”. Um boxe aberto com um identificador ({#thm:main}) é alvo de referências cruzadas, que imprimem “Theorem 2” (:ref{id="thm:main"}), e \ref{thm:main} ou style=number imprime 2.

CampoTipoPadrãoDescrição
labelstring—A palavra antes do número: 'Theorem', 'Lemma', 'Definición'.
counterstring | falseo id do estiloO contador que os boxes incrementam. Estilos que indicam o mesmo contador o compartilham, como faz \newtheorem{lemma}[theorem]: Teorema 1, Lema 2, Teorema 3. 'equation' conta junto com as equações rotuladas. false imprime o rótulo sem número (o “Demonstração.” de uma demonstração).
numberingTemplate / resetOn / counterFormatstring / ResourceCounterReset / ResourceCounterFormat'{n}' / 'never' / 'decimal'Como os de um tipo de recurso: '{h1}.{n}' com resetOn: 'h1' numera Teorema 2.1, 2.2… por capítulo; '{h1}.{h2}.{n}' com 'h2', por seção. Um contador compartilhado por vários estilos conta sobre o que os modelos deles imprimirem.
placement'runIn' | 'title''runIn''runIn' abre o primeiro parágrafo do boxe com o rótulo (um boxe que começa com uma lista, uma fórmula ou outro boxe ganha um parágrafo para ele); 'title' faz do rótulo o título do boxe, no seu titleStyle: “Theorem 2 (Bradley–Terry)”.
bold / italicbooleantrue / falseO estilo do rótulo em linha e do seu sufixo, qualquer que seja o corpo do boxe: um corpo em itálico (body.italic) mantém redondo um rótulo redondo. O título entre parênteses é composto em redondo, no peso regular.
suffixstring'.'Colocado depois do rótulo em linha e do seu título.

Uma demonstração é um estilo com um rótulo sem número e uma marca final:

calloutStyles: [
  { id: 'theorem', numbering: { label: 'Theorem' }, body: { italic: true } },
  { id: 'lemma', numbering: { label: 'Lemma', counter: 'theorem' }, body: { italic: true } },
  { id: 'definition', numbering: { label: 'Definition' } },
  { id: 'proof', backgroundEnabled: false, endMark: '□',
    numbering: { label: 'Proof', counter: false, bold: false, italic: true } },
]

Em um livro diagramado capítulo por capítulo, os contadores continuam a partir do capítulo anterior (LayoutContinuation.statementCounters, que continuationAfter preenche), e o esboço do livro dá à âncora de cada boxe numerado o seu rótulo (OutlineEntry.numberLabel), para que uma referência vinda de outro capítulo o imprima.

#Tipografia dentro de um boxe

Um boxe compõe o seu conteúdo com a sua própria tipografia body e lists; todo o resto mantém os estilos do documento:

  • Parágrafos recebem o body do boxe: fonte, tamanho, entrelinha, cor, cores de ênfase, pesos, italic, smallCaps, alinhamento, hifenização, recuo e espaçamento entre parágrafos. Um campo não definido herda bodyText. Uma cor de ênfase herdada mantém o vínculo com a paleta, de modo que o negrito, o itálico e os rótulos de :ref em um boxe mudam com colorPalette como mudam fora dele. (Até o postext 1.4, o negrito em um boxe ficava #295AA3 qualquer que fosse a cor principal.)
  • Listas com marcadores recebem o lists do boxe (caractere do marcador, cor, tamanho e peso do glifo, recuo, espaço, espaçamento entre itens) por cima de unorderedLists, e o texto delas é o texto do corpo do boxe. Um lists.bulletChar ou lists.color diferente do documento (unorderedLists) substitui o marcador ou a cor de todos os níveis; um que o repita, ou que fique sem definir, deixa a cada nível o seu (unorderedLists.levels), de modo que os travessões aninhados se mantêm no boxe.
  • Listas numeradas recebem lists.indent, gap e itemSpacing, e lists.color sempre que o estilo a define, inclusive quando é a cor dos marcadores do documento; um estilo que a deixa sem definir mantém os números em orderedLists.color. (Até o postext 1.4, a cor só chegava aos números quando era diferente de unorderedLists.color, de modo que defini-la com essa mesma cor não fazia nada.) O número em si (a sua fonte, tamanho e separador) vem do orderedLists global, já que lists só tem campos de marcador: estilize ali os números de um boxe.
  • :::paragraphs dentro de um boxe usam o seu estilo de parágrafo, também em boxes aninhados. Os campos que o estilo deixa sem definir herdam o bodyText do documento, não o body do boxe (nem o seu itálico ou versaletes).
  • Grupos :::columns não têm estilo próprio: todas as colunas compartilham a tipografia de corpo e de listas do boxe, e columnGap define o espaço entre elas.
  • Citações recebem a fonte, o tamanho e os pesos do corpo do boxe (em itálico e cinza, como no texto corrido), e os seus versaletes. Títulos mantêm os estilos de título; fórmulas em destaque, as configurações de matemática.
  • Figuras e tabelas mantêm os estilos de legenda e de tabela do documento, na largura interna do boxe; os seus pesos regular e negrito seguem os pesos do body do boxe.
  • Chips mantêm o seu estilo de chip; um tamanho em em é medido sobre o tamanho do corpo do boxe.
  • :::space é medido em linhas do corpo do boxe (veja :::space para saber onde ele é descartado).
  • Um boxe aninhado recebe o seu próprio estilo por inteiro; o seu span, placement, floatBarrier e snapToGrid são ignorados.

#Marcas em um boxe dividido

Quando um boxe se divide entre colunas ou páginas (keepTogether: false, ou um boxe mais alto que uma coluna), cada parte depois da primeira começa sem o título e sem o ícone e, por padrão, nada avisa o leitor de que o boxe continua. Duas opções acrescentam as marcas que um livro ou um roteiro usa:

  • repeatTitle: true repete o título no alto de cada continuação, seguido de continuedSuffix (“Pontos-chave (cont.)” por padrão). A repetição usa o estilo do título, de modo que, com textTransform: 'uppercase', ela dá o “HAMLET (CONT'D)” de um roteiro. O ícone e a aba de rótulo ficam na primeira parte.
  • continuesMarkerEnabled: true põe continuesMarker (“Continued”, ou “Continúa” em um documento em espanhol) sob a última linha de cada parte que continua, dentro do boxe, na fonte e no tamanho do corpo do boxe: em itálico a menos que continuesMarkerItalic seja false, alinhado à direita a menos que continuesMarkerAlign diga 'left' ou 'center'. A marca ocupa espaço na parte que fecha, e o corte é escolhido de modo que ela caiba.
calloutStyles: [{
  id: 'speech',
  keepTogether: false,
  titleStyle: { textTransform: 'uppercase' },
  repeatTitle: true,
  continuedSuffix: "(CONT'D)",
  continuesMarkerEnabled: true,
  continuesMarker: '(MORE)',
  continuesMarkerAlign: 'center',
  continuesMarkerItalic: false,
}],
:::callout{type="speech" title="Hamlet"}
A speech long enough to run over the foot of the page…
:::

A parte que fecha a página termina com “(MORE)”, e a página seguinte começa com “HAMLET (CONT'D)”. As duas são elementos de paginação: em um PDF acessível são artefatos e no HTML ficam ocultas para as tecnologias assistivas, de modo que o título é lido uma só vez. No VDT são blocos de texto de designOverlay marcados com artifact: true.

#Partes

A propriedade parts configura as páginas divisórias de parte que um documento abre com um contêiner :::part{number="…" title="…"}: a página “Parte I — Fundamentos” que agrupa uma sequência de capítulos. Uma parte sempre ocupa uma página própria: o contêiner quebra para uma página nova da paridade configurada, transforma-a em uma página de coluna única cuja área do corpo vem de parts.margins, aplica o design de abertura sobre a página inteira e quebra de novo depois do delimitador de fechamento, para que o capítulo seguinte (com o seu próprio breakBefore.parity) comece limpo. Com as configurações padrão de H1, isso dá a clássica página de parte em página ímpar, o verso em branco e o capítulo na página ímpar seguinte.

const config: PostextConfig = {
  parts: {
    breakBefore: { parity: 'odd' },
    breakAfter: { enabled: true, parity: 'any' },
    margins: { top: { value: 9, unit: 'cm' }, left: { value: 3, unit: 'cm' }, right: { value: 3, unit: 'cm' } },
    design: {
      elements: [
        {
          kind: 'text', id: 'number', content: 'Part {numberRoman}',
          fontSize: { value: 12, unit: 'pt' }, fontWeight: 600, align: 'left',
          placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: { value: 3, unit: 'cm' }, y: { value: 5, unit: 'cm' } }, size: { width: 'auto', height: 'auto' } },
        },
        {
          kind: 'text', id: 'title', content: '{titleText}',
          fontSize: { value: 28, unit: 'pt' }, fontWeight: 700, align: 'left', overflow: 'wrap',
          placement: { anchor: { to: '#number', edge: 'below' }, size: { width: { value: 15, unit: 'cm' }, height: 'auto' } },
        },
      ],
    },
    bodyStyle: { fontSize: { value: 11, unit: 'pt' }, numberColor: { hex: '#AA0000', model: 'hex' } },
  },
};
:::part{number="I" title="Foundations"}
1. The lantern and its parts
2. Trimming the wick
3. Reading the weather
:::
 
# The lantern and its parts
PropriedadeTipoPadrãoDescrição
pagebooleantrueSe um :::part abre uma página divisória. Com false, nenhuma página é aberta e o corpo do delimitador não é composto: o número, o título e a paleta da parte valem a partir do conteúdo seguinte, sem quebra própria. Uso típico: htmlViewer.overrides.parts.page: false, uma edição para tela sem divisórias de seção.
breakBefore.parityHeadingBreakParity'odd'Paridade da página em que a parte começa. Mesmos valores e mesmas regras de posse das páginas em branco que o breakBefore dos títulos: uma página em branco inserida para alcançar a paridade pertence à parte (o seu já resolve para a nova parte); o separador obrigatório de 'always-*' pertence ao conteúdo anterior.
breakAfter.enabledbooleantrueLeva o conteúdo depois do delimitador de fechamento para uma página nova. Com false, ele continua na coluna única da página da parte.
breakAfter.parityHeadingBreakParity'any'Paridade dessa página nova. Deixe em 'any' e deixe que o próprio breakBefore.parity do capítulo seguinte decida se vem um verso em branco. A quebra é aplicada quando o bloco seguinte é colocado, de modo que uma parte que fecha o documento não deixa uma página vazia no fim.
marginsPageMarginsmargens da páginaÁrea do corpo da página da parte: a coluna única em que fluem os blocos dentro do delimitador. Cada lado herda a margem da página quando não definido; mirror troca interna/externa nas páginas pares, exatamente como as margens da página.
designDesignSlotvazioDesign da abertura. O seu contêiner é a caixa de refile da página, de modo que as âncoras de contêiner e as âncoras 'page' coincidem e 'bleed' vai até a sangria quando as marcas de corte estão ativadas. É puramente decorativo: nunca reserva espaço no corpo; aumente margins.top para manter o corpo livre dele. Quando vazio, um texto padrão na tipografia do H1 é gerado no canto superior esquerdo da área do corpo, com o numberSeparator do H1 entre o número e o título.
versoDesignDesignSlotvazioDesign do verso em branco que vem depois de uma página de parte (o verso da folha divisória): mesmo contêiner e mesmos marcadores de substituição que design. Deixe vazio para um verso liso. Só é pintado quando a página depois da página da parte fica em branco, o que exige uma quebra com paridade: veja O design do verso abaixo.
bodyStyle.fontFamily, fontSize, lineHeight, color, textAligncomo em bodyTextherdam bodyTextTipografia dos parágrafos, citações e itens de lista dentro do delimitador. Os pesos, as cores de ênfase e a hifenização vêm do texto do corpo.
bodyStyle.bulletColorColorValueunorderedLists.colorCor dos marcadores das listas não numeradas dentro da parte.
bodyStyle.numberColorColorValueorderedLists.colorCor dos números das listas numeradas dentro da parte. Os números são sempre compostos no peso negrito do corpo, para que uma lista de capítulos se leia como um sumário.
bodyStyle.unorderedListsUnorderedListsConfig—Substituições parciais aplicadas por cima do unorderedLists do documento dentro da parte, depois de bulletColor. Os valores gerais da lista se propagam para os níveis que os herdavam; as entradas de levels valem só para o seu nível.
bodyStyle.orderedListsOrderedListsConfig—Substituições parciais aplicadas por cima do orderedLists do documento dentro da parte, depois de numberColor e do peso negrito; por exemplo, um separator '•' com o seu próprio separatorFontFamily e separatorColor para a lista de capítulos de uma abertura de parte.

#Marcadores do design de parte

O slot de design resolve o conjunto de marcadores de título com os valores da própria parte: {titleText} é o title do bloco; {number}, o number exatamente como foi escrito; {numberDecimal}, {numberRoman}, {numberRomanLower}, {numberAlpha}, {numberAlphaLower} o reformatam (o número é lido como decimal, como algarismo romano ou como numerais chineses, com ou sem as palavras que o envolvem: "IV", "iv", "4", "4", "四", "卷四" e "第四卷" dão todos {numberDecimal} = 4) e resultam em '' para qualquer outra coisa. Também estão disponíveis {partTitle} / {partNumber}, {chapterTitle} / {chapterNumber} (o capítulo anterior à parte), {pageNumber}, {totalPages}, {bookTotalPages} e os marcadores de metadados. {attr.<key>} lê os atributos do H1 do capítulo atual.

#O design do verso

versoDesign decora a página logo depois de uma página de parte quando essa página não tem conteúdo: o verso da folha divisória. A parte sozinha nunca deixa essa página em branco. breakAfter.parity tem 'any' como padrão, então o conteúdo depois do bloco começa na página seguinte, a menos que algo peça uma paridade:

  • O título do capítulo seguinte. O breakBefore padrão do H1 é 'always-odd', e 'odd' faz o mesmo depois de uma página de parte em página ímpar: o capítulo passa para a próxima página ímpar e o verso fica em branco. O design do verso é pintado.
  • parts.breakAfter: { enabled: true, parity: 'odd' }. A própria parte pede a próxima página ímpar, faça o que fizer o título seguinte. Use quando os capítulos puderem abrir em qualquer lado (breakBefore.parity: 'any', ou breakBefore.enabled: false).

Sem nenhum dos dois, o capítulo abre no verso e nenhum design de verso é desenhado. Com breakAfter.enabled: false, o conteúdo continua na própria página de parte. O verso usa a paleta da parte, então um palette="band=#…" no bloco também muda a cor dele.

parts: {
  breakBefore: { parity: 'odd' },
  breakAfter: { enabled: true, parity: 'odd' },   // sempre um verso em branco para pintar
  versoDesign: {
    elements: [{
      kind: 'box', id: 'field',
      style: { backgroundColor: { hex: '#b07d2b', model: 'hex', paletteId: 'band' } },
      placement: { anchor: { to: 'page', edge: 'top-left' }, size: { width: 'fill', height: 'fill' } },
    }],
  },
}

Uma parte que fecha o seu capítulo. Num livro diagramado capítulo a capítulo (o Sandbox, buildBundle), um bloco :::part pode ser um capítulo sozinho, ou o fim de um. A página de parte é então a última página do capítulo, e o capítulo seguinte assume o que a parte ainda deve: aplica breakAfter antes do seu primeiro bloco e pinta versoDesign na sua primeira página quando essa página fica em branco. As páginas saem como sairiam com o livro inteiro num só documento. Depois do bloco só podem vir diretivas que não posicionam nada (:::numbering, :::space); qualquer outra coisa é conteúdo do capítulo, que então faz a quebra por conta própria. Um capítulo vazio logo depois da parte é uma página própria: essa página é o verso. Um host que diagrama os capítulos por conta própria obtém isso de continuationAfter(), que informa afterPartPage: true para um capítulo que termina com uma parte; repasse esse valor na continuation do capítulo seguinte.

#O contêiner :::part

Uma linha :::part{number="…" title="…"} abre a parte e um ::: sozinho a fecha; os dois atributos são opcionais (o padrão é ''). Os blocos entre eles (normalmente a lista de capítulos) correm na coluna única da página de parte com bodyStyle, a partir de margins.top; um corpo mais longo que a página continua em páginas comuns. A página é classificada como role: 'part' (VDTPage.partInfo traz o número e o título), de modo que os elementos de cabeçalho e rodapé podem visá-la ou pulá-la com pages: 'part' / pages: 'body'; o renderizador de PDF acrescenta a parte aos marcadores do PDF, acima dos seus capítulos. Um corpo vazio (:::part{…} seguido diretamente de :::) é o caso comum e ainda assim produz a página; duas partes seguidas nunca dividem uma página. Um :::part aninhado em outra parte é incorporado à parte externa.

Um terceiro atributo, palette="<id>=<hex>[, <id>=<hex>…]", dá à parte as suas próprias cores: na página de parte e em todas as páginas que vêm depois dela, até a parte seguinte, cada cor de design (cabeçalho, rodapé, faixa de abertura, designs de parte e de verso) vinculada a um desses ids de paleta usa o valor da parte em vez do valor da paleta do documento. É assim que as seções de um livro mudam a cor da aba do canto, do ponto do cabeço e da faixa de abertura de capítulo sem um segundo design: :::part{number="II" title="…" palette="band=#f6c297"}. O fluxo de texto acompanha: nessas mesmas páginas, toda cor do fluxo igual ao valor base de uma entrada de paleta substituída (títulos, cores de negrito, itálico e referências, marcadores e números de lista, rótulos e barras de legenda, texto, fios e preenchimentos de tabela (cabeçalho, corpo, linhas alternadas e o preenchimento próprio de uma célula), boxes (fundo, borda, faixa, título) e chips (preenchimento, contorno e texto)) usa o valor da parte, de modo que um headings.levels[1].color vinculado a band compõe os títulos de cada seção na sua própria cor. As amostras de cor no texto mantêm a cor escrita nelas. Os pares são separados por vírgulas, ponto e vírgula ou espaços, = ou : une id e cor, e o # é opcional. Uma parte continua valendo depois que o bloco fecha: {partTitle}, {partNumber} e a paleta acompanham o fluxo nos capítulos seguintes e (por meio de continuation.part, que continuationAfter() informa) nos capítulos diagramados separadamente, de modo que o segundo capítulo de uma seção mostra a seção nos cabeços exatamente como o primeiro.

Duas entradas da paleta podem ter o mesmo valor base e ainda assim receber valores diferentes numa parte, quando a parte substitui uma e não a outra ou dá a elas cores diferentes. O valor sozinho não diz de qual entrada veio uma cor do fluxo, então cada cor do fluxo é comparada com as configurações de onde ela pode vir e usa o valor a que essas configurações estão vinculadas. Distinguem-se:

  • as cores do texto de um bloco: a cor do texto, as cores de negrito, itálico e referências, o marcador de lista (um marcador ou um número) e o separador depois de um número. Com bodyText.color vinculado a ink e bodyText.boldColor vinculado a accent, ambos #1a1a1a, uma parte com palette="accent=#b8413d" muda a cor dos trechos em negrito e deixa o texto como está; se for unorderedLists.color o vinculado a accent, muda a cor dos marcadores e deixa o texto dos itens;
  • cada nível de título, e cada estilo de título que define uma cor;
  • o texto de uma linha do sumário, o seu número, e o seu número de página e subtítulo;
  • em cada estilo de boxe, os preenchimentos (fundo, faixa, aba do rótulo), a borda, os fios (do marcador e do rótulo) e o texto (título, ícone, glifo do marcador, rótulo);
  • cada cor de cada estilo de tabela, de chip e de legenda. Um estilo de tabela nomeado é distinto de tableStyle, e o estilo de legenda de um tipo de recurso é distinto de captionStyle. O preenchimento próprio de uma célula segue o seu próprio vínculo.

Ainda resta um caso decidido pelo valor: configurações em lugares diferentes que definem a mesma cor de um bloco. bodyText.color, bodyText.blockquote.color, o color de um estilo de parágrafo e o body.color de um estilo de boxe definem todos a cor do texto de um bloco, por exemplo. Quando duas delas se vinculam a entradas com o mesmo valor base e a parte as separa, a cor usa a substituição (a última escrita, quando as duas são substituídas). Dê a essas entradas valores base próprios.

const resolved = resolvePartsConfig(config.parts, resolvedPage, resolvedBodyText, resolvedUnorderedLists, resolvedOrderedLists);
// => margens preenchidas a partir da página, bodyStyle a partir das configurações do corpo / das listas
 
const minimal = stripPartsDefaults(config.parts);
// => undefined quando só restam padrões estáticos

#Estilos de título

A propriedade headingStyles declara estilos nomeados que um documento aplica a um título com # Title {style="<id>"}. Um estilo faz duas coisas. Substitui a tipografia, o design e a numeração do nível do título (qualquer campo de uma entrada de nível exceto level: fonte, corpo, cor, breakBefore, span, advancedDesign, textTransform, hidden, numberingTemplate…) e rege a seção que o título abre: as páginas dela, até o próximo título do mesmo nível ou de um nível superior, usam os cabeços, a geometria de página, a tipografia do corpo e a paleta do estilo. É assim que os elementos pré-textuais de um livro (um prefácio numa única coluna larga, com fólios em algarismos romanos e faixas azuis) convivem com um manual em duas colunas e numeração decimal sem uma segunda configuração.

const config: PostextConfig = {
  headingStyles: [
    {
      id: 'front-matter',
      numbered: false,
      breakBefore: { enabled: true, parity: 'odd' },
      span: 'page',
      advancedDesign: { enabled: true, minHeight: { value: 52, unit: 'mm' }, slot: { elements: [/* bands, `{titleText}` */] } },
      header: { elements: [/* folio | rule | `{title}. {subtitle}` */] },
      margins: { left: { value: 50, unit: 'mm' }, right: { value: 17, unit: 'mm' } },
      layout: { layoutType: 'single' },
      bodyStyle: { fontSize: { value: 10.5, unit: 'pt' }, textAlign: 'justify' },
      palette: { band: '#547396' },
    },
  ],
};
# Preface {style="front-matter"}
PropriedadeTipoPadrãoDescrição
idstring—Identificador referenciado por numa linha de título. Um id desconhecido deixa o título como está.
namestringidNome legível (só na interface do editor).
numberedbooleantrueSe o título conta: avança o contador do seu nível (os números do numberingTemplate, o dos números de recursos), o ordinal de capítulo por trás de e o número impresso no sumário. false para um prefácio, uma lista de autores, um índice: o primeiro capítulo numerado depois deles continua sendo o capítulo 1, e fica vazio nas páginas deles.
tocbooleantrueSe :::toc lista o título. Um título substitui isso com / .
runningChapterbooleantrueSe um título de nível 1 deste estilo passa a ser o capítulo corrente: o capítulo que , , e as suas formas …AtTop nomeiam na página dele e nas seguintes. false para uma prancha, um mapa ou uma capa composta como H1 dentro de um capítulo: os cabeços passam por cima dele, inclusive na sua própria página, e continuam nomeando o capítulo que ele interrompe; ele também não define nenhuma palavra-guia h1. O título continua contando quando numbered (o seu próprio design lê o seu próprio ) e continua listado por :::toc quando toc. Com toc: false também, não recebe marcador no PDF: a prancha de um capítulo na página antes da abertura deixa os marcadores para os capítulos. Títulos de outros níveis ignoram esta opção. Com o padrão, as páginas depois de uma prancha imprimem o título da prancha. Um prefácio ou um prólogo com numbered: false continua sendo um capítulo próprio e mantém o padrão. A opção muda apenas qual capítulo os marcadores nomeiam: o estilo continua abrindo uma seção própria, como todo estilo de título, então até o próximo título de nível 1 as páginas usam os slots de cabeço, as margens, as colunas, o estilo do corpo e a paleta do estilo da prancha (os do documento quando o estilo não define nenhum), e não os de uma seção com estilo aberta pelo capítulo interrompido. Num livro diagramado capítulo a capítulo, os cabeços não passam de um arquivo de capítulo para o seguinte, então uma prancha que abre um arquivo mostra os marcadores de capítulo vazios até o primeiro título de capítulo do arquivo.
campos do nívelcomo em headings.levels[]os valores do nívelfontFamily, fontSize, lineHeight, fontWeight, italic, color, marginTop, marginBottom, snapToGrid, breakBefore, span, advancedDesign, textTransform, letterSpacing, lineSpan, indent, jidori, hidden: cada um que for definido substitui o valor do nível do título para os títulos deste estilo. breakBefore é mesclado campo a campo sobre o do nível: um estilo que só define parity mantém o enabled do nível, e um que só define enabled: true mantém a paridade do nível (até o postext 1.4, o campo ausente vinha do padrão sem quebra).
numberingTemplatestringo do nívelModelo com que os títulos do estilo são numerados, no lugar do modelo do nível (os mesmos tokens de levels[].numberingTemplate). O contador continua sendo o do nível: um estilo de apêndice com 'Appendix ' depois de cinco capítulos imprimiria Appendix F, então reinicie a contagem com no primeiro apêndice. '' não imprime número, mas o título continua contando: nem mesmo o ordinal de capítulo que o sumário e mostram para um título de nível 1 sem modelo. O número aparece no fluxo, no do design do estilo, no sumário e em .
header, footerDesignSlotos do documentoCabeços das páginas da seção, que substituem header / footer ali (os filtros parity e pages dos elementos continuam valendo). Um slot vazio os remove.
marginsPageMarginsmargens da páginaÁrea do corpo das páginas da seção; cada lado herda a margem da página quando não definido, mirror inclusive. Vale nas páginas que a seção abre: combine com breakBefore.
layoutLayoutConfiglayoutDisposição de colunas das páginas da seção (layoutType, gutterWidth…): uma única coluna larga para um prefácio composto num livro de duas colunas. O seu columnRule é desenhado nas páginas da seção, e cada campo que ele deixa sem valor usa o valor de layout.columnRule do documento, então uma seção que só muda as colunas mantém o fio do documento (veja Fio entre colunas).
bodyStylePartsBodyStyleConfigherda bodyTextTipografia dos parágrafos, citações e listas da seção: os mesmos campos de parts.bodyStyle.
paletteRecord<string, string>Substituições de paleta (id → hex) para as páginas da seção, por cima das da parte atual: o mesmo mecanismo do atributo palette de uma parte, e com o mesmo alcance. Vale não só para os slots de design dessas páginas (cabeços, a faixa de abertura, toda cor vinculada a um id substituído), mas também para o fluxo de texto, pelo valor: toda cor do fluxo igual ao valor base de uma entrada substituída (títulos, cores de negrito, itálico e referências, marcadores e números de lista, rótulos e barras de legenda, texto, fios e preenchimentos de tabela, boxes (fundo, borda, faixa, título) e chips (preenchimento, contorno e texto)) usa o valor da seção, como acontece numa parte, incluindo a regra para duas entradas com o mesmo valor base (veja O contêiner :::part). As amostras de cor no texto mantêm a cor escrita nelas. A cor da página também acompanha: quando page.backgroundColor está vinculada a uma entrada substituída (ou, sem vínculo, tem o valor base dela), as páginas da seção são pintadas com o valor da seção, e assim as páginas de economia de um jornal saem em salmão enquanto as demais ficam brancas. Desde o postext 1.18.

A seção fecha no próximo título do mesmo nível ou de um nível superior: um # sem estilo depois de um com estilo volta aos cabeços e à geometria do documento; um com estilo abre a sua própria seção. As páginas que a seção deixou em branco por paridade pertencem a ela, como acontece com os títulos de capítulo.

Uma quebra de página não substitui a quebra do próprio título. Um estilo herda o breakBefore do seu nível (o padrão do nível 1 é { enabled: true, parity: 'always-odd' }), e um título o aplica onde quer que esteja, inclusive logo depois de um :::pagebreak. A quebra de página abre uma página nova, e o título ainda pede o seu lado da página dupla. Com parity: 'odd', uma quebra que cai num verso é seguida de uma página em branco, e o título abre na próxima página ímpar. Com 'always-odd' vem também a página em branco separadora, e a quebra de página não muda nada, já que o título abriria essa página de qualquer jeito. Um estilo que deve começar na página aberta por uma quebra manual, como uma página de sumário depois da folha de rosto, desliga a sua própria quebra:

headingStyles: [
  // Começa onde o texto o põe: na página que o `:::pagebreak` anterior abriu.
  { id: 'contents', numbered: false, toc: false, breakBefore: { enabled: false } },
],

Para ter uma página própria sem escolher lado, use breakBefore: { parity: 'any' } e deixe de fora o :::pagebreak.

A que seção uma página pertence. Cabeços e paleta são escolhidos por página, não por título. Uma página fica com a seção em vigor depois da última mudança de seção nela: onde uma seção termina e outra começa na mesma página (duas letras curtas de um dicionário, por exemplo), a página leva os cabeços e a paleta da segunda; onde uma seção com estilo termina no meio da página num título sem estilo, a página volta aos do documento. {chapterTitle} segue a mesma regra: uma página onde dois capítulos se encontram mostra o título do segundo. As páginas em branco seguem a regra dos títulos de capítulo: uma página em branco de paridade (blankForParity) pertence à seção que abre depois dela, e a separadora que uma quebra 'always-odd' / 'always-even' acrescenta (blankForForce) pertence à seção anterior. Uma divisória de parte fecha a seção aberta.

# A {style="letter"}
 
Aardvark, abacus.
 
# B {style="letter"}
 
Babble, badger… (runs on to the next page)

As duas letras começam na página 1, então a página 1 usa os cabeços da seção B: uma aba lateral definida no cabeçalho do estilo mostra “B” ali, e nenhuma página leva a aba de “A”. Dê a cada seção uma página própria (breakBefore) quando todas precisarem da sua aba. Apêndices com letras depois de capítulos numerados, e uma página de dedicatória que o sumário e os marcadores do PDF listam, mas que a página não titula:

headingStyles: [
  { id: 'appendix', numberingTemplate: 'Appendix {1:A}' },
  { id: 'silent', hidden: true, numbered: false },
],
# Dedication {style="silent"}
 
For M., who read every draft.
 
# Method
 
…
 
# Survey instrument {style="appendix" startAt=1}
 
# Raw data {style="appendix"}

Com numberingTemplate: '{1}.' no nível 1, os capítulos imprimem 1., 2.…, e os apêndices Appendix A e Appendix B. A dedicatória abre a sua página (o breakBefore do seu nível), imprime só o seu parágrafo e ainda aparece como Dedication em :::toc, nos cabeços com {chapterTitle} e nos marcadores do PDF; acrescente toc: false ao estilo para tirá-la do sumário. Uma prancha ou um mapa composto como H1 no meio de um capítulo pede o contrário da dedicatória: um estilo com runningChapter: false (em geral com numbered: false e toc: false) mantém os cabeços no capítulo que ele interrompe.

const resolved = resolveHeadingStylesConfig(config.headingStyles, resolvedPage, resolvedBodyText, resolvedUnorderedLists, resolvedOrderedLists);
// => substituições de nível normalizadas, margens preenchidas a partir da página, bodyStyle a partir do corpo
 
const minimal = stripHeadingStylesDefaults(config.headingStyles);
// => undefined quando não resta nenhum estilo

#Sumário

A propriedade toc configura o que uma diretiva :::toc imprime (veja Formato do documento). O sumário é montado a partir da estrutura do documento (cada título com o seu número e o rótulo da sua página, cada :::part), então acompanha os capítulos: renomeie um, mova-o para outra parte, mude os seus autores, e as entradas mudam junto. Uma entrada é o número do título numa coluna própria, o título, uma linha de pontos e o rótulo da página na margem direita, e depois uma linha de subtítulo opcional; uma parte é uma linha desenhada por parts.design.

const config: PostextConfig = {
  toc: {
    levels: [{ level: 1, fontWeight: 700, color: { hex: '#00507b', model: 'hex' }, numberWidth: { value: 7.4, unit: 'mm' } }],
    unnumbered: { color: { hex: '#000000', model: 'hex' } },
    pageNumber: { fontWeight: 400, width: { value: 8, unit: 'mm' } },
    leader: { char: '.', gap: { value: 1, unit: 'mm' } },
    subtitle: { enabled: true, attr: 'author', italic: true, fontSize: { value: 8.5, unit: 'pt' } },
    parts: {
      height: { value: 23, unit: 'pt' },
      marginTop: { value: 11.5, unit: 'pt' },
      design: { elements: [/* a band box, 'SECTION {number}', '{titleText}', '{pageNumber}' */] },
    },
  },
};
PropriedadeTipoPadrãoDescrição
levelsTocLevelConfig[]nível 1Níveis de título listados, cada um com a tipografia da sua entrada: fontFamily, fontSize, lineHeight (o padrão é a entrelinha do corpo, para o sumário ficar na grade), fontWeight, italic, color, indent (da entrada inteira), numberWidth / numberGap (a coluna do número, depois da qual começa o título; os números ficam alinhados à direita nela, e um número mais largo que numberWidth, como الفصل الحادي عشر ou Chapter 12, alarga a coluna do seu nível até o mais largo), numberFontFamily, numberFontSize, numberFontWeight, numberColor, marginTop, marginBottom. Os campos sem valor herdam do texto do corpo. O número fica na linha de base da primeira linha do título, qualquer que seja a fonte e o corpo, no canvas, no HTML e no PDF (até o postext 1.4 ele ficava centralizado na altura-x como um marcador de lista, e uma fonte display ou um corpo maior ficava acima do título). Um renderizador próprio encontra essa linha de base em bulletBaselineY do bloco da entrada; bulletY continua sendo o ponto médio da caixa eme do número, como na 1.4, então um renderizador anterior ao novo campo desenha os números onde sempre desenhou.
unnumberedTocEntryStyleConfig—Substituições para títulos cujo estilo tem numbered: false (um prefácio): não imprimem número e começam alinhados no indent do nível.
pageNumberobjectfonte do nível 1, peso do corpofontFamily, fontSize, fontWeight, italic, color do rótulo da página, e width (padrão 2em): a coluna reservada para ele na margem direita, onde fica alinhado à direita.
leaderobjectchar se repete no espaço entre o título e o número da página, alinhado à direita para que os pontos de entradas seguidas fiquem alinhados ('. ' os espaça); gap é o espaço mínimo mantido entre o título e a linha de pontos. A linha de pontos leva quantos caracteres couberem, medidos como uma sequência inteira na sua fonte, então uma fonte que afasta pontos finais seguidos com kerning recebe menos pontos, em vez de pontos que chegam até o número da página. (Até o postext 1.4 a contagem vinha de um único ponto, e numa fonte assim a linha de pontos corria do título até o número.) Um título que não deixaria espaço para o rótulo quebra um pouco antes.
subtitleobjectUma segunda linha sob a entrada, tirada de um atributo do título (attr), como os autores do capítulo, com seus próprios fontFamily, fontSize, fontWeight, italic (padrão true), color e indent extra. A linha usa a entrelinha da entrada e nunca se separa do seu título.
parts.enabledbooleantrueSe as divisórias de parte recebem uma linha.
parts.breakBeforebooleanfalseAbre uma página nova antes de cada linha de parte, exceto a primeira, para que os capítulos de cada parte sejam listados numa página própria.
parts.designDesignSlotvazioDesign da linha; o seu contêiner é a linha (largura da coluna × height). Marcadores: , , …, e (o rótulo da página de parte; com parts.page: false, que não abre página de parte, o rótulo da página em que começa o conteúdo da parte, onde os cabeços passam a ela; num livro diagramado capítulo a capítulo, um bloco que fecha o seu capítulo aponta para a primeira página de conteúdo do capítulo seguinte). As cores vinculadas à paleta usam a palette da própria parte, então a linha de cada seção sai na sua cor. Quando vazio, (com o numberSeparator do H1 entre eles) e o número da página são compostos com a tipografia da entrada de nível 1.
parts.height, marginTop, marginBottomDimension2em, 0, 0Altura da linha e o espaço em volta dela. em é o corpo do texto, então a linha padrão tem o dobro do corpo de altura, e não duas linhas do texto: com um texto em 9,5/13,5 pt, ela tem 19 pt. Para uma linha de duas linhas do texto, dê a altura em pt (27pt nesse caso).

Os rótulos de página são os que o documento imprime. buildDocument() diagrama de novo um documento com :::toc usando os rótulos da passada anterior até que eles se estabilizem (no máximo três passadas extras); um host que diagrama um livro capítulo a capítulo fornece em vez disso a estrutura do livro inteiro como PostextContent.outline, montada a partir de contentOutline() (títulos e partes só a partir do texto) e de outlineFromDoc() (as mesmas entradas com os rótulos de página de uma diagramação), e diagrama de novo o capítulo do sumário sempre que o outlineKey() dessa estrutura muda. Cada entrada de título traz o number que o sumário imprime e, para um título numerado, o seu counter: a contagem corrente do nível depois de qualquer startAt, seja o que for que o modelo imprima; é o que um host mostra ao lado de um capítulo nas suas próprias listas.

#Índice remissivo

A propriedade index configura o que uma diretiva :::index imprime (veja Formato do documento): os termos marcados com :index[…] e :index{term="…"} no texto, ordenados, agrupados pela letra inicial (pela inicial do pinyin ou pelo número de traços em chinês, pela linha do gojūon em japonês, veja groupBy), cada um com as páginas em que aparece. Uma entrada é o seu termo, um separador e os seus números de página; as subentradas vêm em seguida, com um passo de recuo por nível, e as linhas quebradas recuam por turnoverIndent para nunca se alinharem com uma subentrada.

const config: PostextConfig = {
  headingStyles: [
    // O índice em duas colunas, sob o seu próprio título.
    { id: 'index', numbered: false, layout: { layoutType: 'double', gutterWidth: { value: 6, unit: 'mm' } } },
  ],
  index: {
    fontSize: { value: 8.5, unit: 'pt' },
    lineHeight: { value: 11, unit: 'pt' },
    rangeFormat: 'chicago',
    groups: { fontFamily: 'Source Sans 3', fontWeight: 700, color: { hex: '#8a1c1c', model: 'hex' } },
  },
};
PropriedadeTipoPadrãoDescrição
fontFamily, fontSize, lineHeight, fontWeight, color—o texto do corpoTipografia das entradas. Toda linha do índice, cabeças de letra inclusive, é composta em lineHeight; o índice mantém o seu próprio ritmo e não se encaixa na grade de linhas de base.
indentDimension1emRecuo de cada nível de subentrada.
turnoverIndentDimension2emRecuo extra das linhas quebradas de uma entrada, além do recuo do seu nível.
entrySpacingDimension0Espaço acima de cada entrada principal.
separator, locatorSeparator, rangeSeparatorstring', ', ', ', '–'; os dois primeiros '، ' em escrita árabeO que é impresso entre o termo e a sua primeira página, entre duas páginas e entre as pontas de um intervalo.
mergeRangesbooleantrueJunta páginas consecutivas de um mesmo formato de numeração num intervalo: 12, 13, 14 imprime 12–14. As páginas principais nunca são juntadas.
rangeFormat'full' | 'chicago''full'Como se escreve o segundo número de um intervalo: por extenso (234–237) ou sem os algarismos que tem em comum com o primeiro, como pede o The Chicago Manual of Style (9.64): 71–72, 100–104, 101–8, 321–28, 1496–500. Os rótulos em algarismos romanos são sempre escritos por extenso.
mainnegritoComo é composta uma página principal (main na marcação).
seeconforme o idioma, itálico (redondo em escrita árabe)As palavras antes de uma referência cruzada. Sem valor, seguem o idioma do documento: See / See also, Véase / Véase también, Voir / Voir aussi, 见 / 另见 (見 / 另見 em chinês tradicional)… Um índice em chinês compõe a referência depois de um ponto final, sem espaço: 贾琏 12。见贾政.
localestringo do documentoO idioma cuja ordem alfabética ordena as entradas (uma tag BCP 47, lida por Intl.Collator). Em espanhol, ñ vem depois de n e encabeça um grupo próprio; os acentos nunca mudam a ordem.
groupBy'auto' | 'letter' | 'pinyin' | 'stroke' | 'gojuon' | 'kana' | 'none''auto'O que são as cabeças de grupo. 'letter': a primeira letra da chave de ordenação. 'pinyin': uma entrada que começa com um caractere han fica sob a inicial latina da sua leitura em pinyin (贾宝玉 sob J), e uma chave de ordenação latina fica sob a sua letra, depois das entradas chinesas dessa letra (o collator põe as letras latinas depois dos caracteres chineses): sort="jia mu" fecha o J. 'stroke': sob o número de traços do primeiro caractere, 一畫, 二畫… (一画… em chinês simplificado). 'gojuon': uma entrada em kana fica sob a sua linha do gojūon, あ行, か行 … わ行, e 'kana' sob o seu primeiro kana (katakana e hiragana dividem as cabeças), na ordem JIS X 4061 de um índice japonês: pela leitura (o yomi da marcação, senão a leitura em kana do seu rubi, senão sort, senão o texto), katakana como hiragana, kana pequeno como grande, ー como a vogal anterior, 清 antes de 濁 antes de 半濁; símbolos, depois números pelo valor, depois palavras latinas sob as suas letras, depois kana; uma entrada ainda encabeçada por um kanji é informada como indexReadingMissing e composta depois dos kana, sem cabeça. 'none': sem cabeças; símbolos, números e palavras ficam separados apenas por groups.marginTop. 'auto' agrupa um índice japonês (ja, ja-*) pela linha do gojūon, um índice em chinês simplificado (zh, zh-Hans, zh-CN) por pinyin, um em chinês tradicional (zh-Hant, zh-TW, zh-HK) por traços, e todos os outros idiomas por letra. As entradas são ordenadas na colação de onde vêm as cabeças: um índice zh-Hant agrupado por pinyin é ordenado por pinyin. As leituras e os números de traços são os do collator (CLDR); onde ele lê um caractere errado (重 como zhòng em 重阳, 行 como xíng em 行业), dê à marcação uma chave sort em caracteres que só tenham a leitura desejada, que fica ordenada no lugar certo: sort="崇阳" para 重阳, sort="航业" para 行业. Um navegador sem dados de colação chinesa imprime um índice por pinyin ou por traços sem cabeças.
ignoreArticlebooleantrue em árabeOrdena e agrupa as entradas árabes como se um artigo inicial ال (ٱل) não estivesse ali: البصرة fica sob ب, entre بدر e بغداد, e é impresso como foi escrito. الله mantém o seu artigo, e uma entrada com a sua própria chave sort é ordenada por essa chave tal como foi dada. Seja qual for o valor desta opção, um índice árabe ignora os sinais vocálicos e o tatweel, põe أ إ آ ٱ sob ا e ordena ؤ como و, ئ e ى como ي, ة como ه.
groups.enabledbooleantrueImprime uma cabeça acima de cada grupo de entradas (A, B…, ou o número de traços, 0–9 para números, Symbols para o resto; 数字 / 數字 e 符号 / 符號 em chinês).
groups.fontFamily, fontSize, fontWeight, italic, color—as das entradas, peso 700A fonte da cabeça de letra. Ela é composta no passo de linha das entradas.
groups.marginTopDimensionuma linha do índiceEspaço acima de cada grupo, com ou sem cabeça; nenhum acima do primeiro grupo, cuja distância do título é a do título, e nenhum no topo de uma coluna.
groups.symbolsLabel, numbersLabelstringconforme o idioma; '0–9', '数字' / '數字' em chinêsAs cabeças das entradas que começam com um símbolo e com um algarismo.

Os números de página vêm da estrutura do livro, como os do sumário: computeOutline() e contentOutline() listam as marcações de índice de um texto como entradas do tipo 'indexMark' (com indexMark.path, sort, see, seeAlso, main, range e index), e outlineFromDoc() dá a cada uma a página em que caiu, que a construção registra em doc.indexMarks ({ sourceStart, pageIndex } por marcação). buildDocument() diagrama de novo um documento que imprime o seu próprio índice até os números se estabilizarem; um host que diagrama um livro capítulo a capítulo entrega ao capítulo que contém :::index a estrutura do livro inteiro como PostextContent.outline. tocOutline() e indexOutline() dividem uma estrutura no que o sumário e o índice leem, para que um host possa associar cada capítulo ao que ele imprime: o Sandbox diagrama de novo o capítulo do índice só quando uma marcação se move, e o do sumário só quando um título se move. contentOutline() também diz se um texto imprime um índice (hasIndex).

#Quadrinhos

A propriedade comics configura as páginas de quadrinhos (:::page), as tiras (:::strip) e as páginas duplas: a moldura e as sarjetas de onde os quadros são recortados, os estilos de quadro, o letreiramento, os estilos de balão e o elenco. Só os documentos com quadrinhos a leem; um documento sem eles é diagramado exatamente como antes, e um documento com quadrinhos e sem a seção comics usa os padrões do seu idioma. A marcação e a forma como as páginas são letreiradas estão explicadas em Quadrinhos.

const config: PostextConfig = {
  comics: {
    readingDirection: 'auto',
    gutter: { horizontal: { value: 5, unit: 'mm' }, vertical: { value: 2.5, unit: 'mm' } },
    panel: { borderWidth: { value: 0.8, unit: 'pt' }, borderStyle: 'rough' },
    panelStyles: [{ id: 'night', background: { hex: '#14142b', model: 'hex' }, borderColor: { hex: '#ffffff', model: 'hex' } }],
    lettering: { fontSize: { value: 8.5, unit: 'pt' }, textTransform: 'uppercase' },
    balloonStyles: [{ id: 'eerie', shape: 'wavy', italic: true }],
    cast: [{ id: 'maya', name: 'Maya' }, { id: 'tomas', name: 'Grandpa Tomás' }],
  },
};
PropriedadeTipoPadrãoDescrição
readingDirection'auto' | 'ltr' | 'rtl''auto'A ordem dos quadros numa fileira. 'auto' lê da direita para a esquerda num documento da direita para a esquerda, num documento vertical e em japonês e chinês tradicional e, nos demais casos, no sentido de artDirection (incluídos o chinês simplificado e o coreano). O atributo direction de uma página tem prioridade. Com page.binding: 'auto', um livro cujos quadrinhos são lidos da direita para a esquerda é encadernado à direita.
artDirection'ltr' | 'rtl''ltr'O sentido de leitura para o qual a arte foi desenhada: 'rtl' para mangá.
mirrorArtbooleanfalseEspelha os desenhos de uma página lida no sentido contrário ao de artDirection. Um quadro mantém o desenho como foi feito com mirror=false.
frame.marginsPageMarginsas margens da páginaMargens próprias para as páginas de quadrinhos (top, bottom, left, right, mirror). Os quadros são recortados da caixa que elas deixam; sem valor, da área de texto da página.
gutter.horizontalDimension4mmO espaço entre fileiras. O atributo gutter de uma página tem prioridade.
gutter.verticalDimension2mmO espaço entre quadros lado a lado numa fileira.
panelPanelStyleConfigveja abaixoO estilo de quadro padrão.
panelStylesNamedPanelStyleConfig[][]Estilos de quadro nomeados, escolhidos com :::page{style=…} ou ::panel{style=…}. Cada um tem um id, um name opcional (só nas interfaces de editor) e os campos de um estilo de quadro; os campos sem valor seguem panel.
letteringLetteringConfigveja abaixoA fonte, o corpo e as regras da casa de todos os balões do livro.
balloonStylesBalloonStyleConfig[]os nove estilos integradosEstilos de balão por tipo. Os integrados estão sempre presentes; uma entrada com o id de um deles o altera, e uma entrada com um id novo acrescenta um estilo aplicado sobre speech.
castComicCastMember[][]Os personagens (veja Elenco).
runningHeadsbooleanfalseImprime os cabeços e os fólios nas páginas de quadrinhos. Desligado, uma página de quadrinhos é só quadros, e o seu número continua contando.
viewerLeafComicViewerLeafConfigsem valorPara hosts cujas páginas não são folhas: diagrama as páginas de quadrinhos numa folha impressa (width, height, margins) escalada para fitWidth px (e no máximo fitHeight px de altura) no topo da página. O visualizador HTML do Sandbox a define; ela nunca é salva com um documento.

#Estilos de quadro

comics.panel e cada entrada de comics.panelStyles:

PropriedadeTipoPadrãoDescrição
borderWidthDimension1ptEspessura da borda; 0 não desenha nenhuma. A borda é traçada sobre o contorno do quadro.
borderColorColorValuepretoCor da borda (pode ser vinculada à paleta).
borderRadiusDimension0Raio dos cantos de um quadro retangular, limitado à metade do seu lado menor. Um quadro cortado por uma linha inclinada mantém os cantos vivos.
borderStyle'solid' | 'rough' | 'none''solid'Uma linha limpa, uma linha à mão que treme um pouco (com semente tirada do quadro, então sai igual em toda construção) ou nenhuma.
backgroundColorValuebrancoO preenchimento sob o desenho: a cor de um quadro vazio e das faixas em volta de um desenho mostrado inteiro.
fit'cover' | 'contain''cover'cover recorta o desenho para preencher o quadro, nunca dentro da área segura; contain o mostra inteiro.
bleedbooleanfalseOs quadros deste estilo passam da moldura até o refile e a sangria em todos os lados que tocam a moldura.

#Letreiramento

comics.lettering define todos os balões do livro. Há um único corpo: um estilo de balão pode escalá-lo (fontScale), e o texto nunca é reduzido para caber num balão.

PropriedadeTipoPadrãoDescrição
fontFamilystringconforme o idiomaA fonte do letreiramento. Sem valor: Comic Neue; Zen Antique em japonês, Noto Sans SC em chinês simplificado, LXGW WenKai TC em chinês tradicional, Playpen Sans Arabic em árabe, persa e urdu (defaultComicFont(locale)).
fontSizeDimension7.5ptO corpo do texto dos balões. Os quadrinhos costumam ser letreirados em 7 a 9 pt na página impressa.
lineHeightnumber1.15Distância entre as linhas, um múltiplo do corpo. Deixado no padrão, os balões verticais, chineses e japoneses usam 1,5 e os árabes 1,45.
colorColorValuepretoCor do texto.
bold, italicbooleanfalseCompõe todo o letreiramento em negrito ou em itálico. O letreiramento em chinês, japonês e árabe nunca é inclinado.
letterSpacingDimension0Espaço extra entre as letras; em é relativo ao corpo do texto. Nunca se aplica ao árabe.
writingMode'auto' | 'horizontal' | 'vertical''auto''auto' letreira o japonês e o chinês tradicional, e qualquer documento composto em escrita vertical, em colunas verticais, e todo o resto na horizontal. A marca vertical ou horizontal de uma linha do roteiro compõe essa linha de outro modo (veja Quadrinhos › Letreiramento).
textTransform'none' | 'uppercase''none'Tudo em maiúsculas, o visual clássico do letreiramento americano e europeu. As escritas sem caixa ficam como estão.
dropFinalStop'auto' | boolean'auto'Omite o ponto final que fecha um balão (。 no mangá). 'auto': só em japonês e chinês.
doubleDashbooleanfalseLetreira um travessão como --, um costume do letreiramento americano.
insetDimension1.5mmEspaço mantido entre um balão e a borda do seu quadro. Um recordatório encostado na borda o ignora.
joinSameSpeaker'butt' | 'connector' | 'none''butt'Dois balões seguidos do mesmo personagem: corpos fundidos num só contorno, ligados por um gargalo estreito, ou mantidos separados. Uma linha do roteiro pede o seu próprio comportamento com join ou join=false.
maxColumnCharsnumber8Letreiramento vertical: o máximo de caracteres numa coluna de um balão. As onomatopeias quebram depois de cinco.

#Estilos de balão

Cada entrada de comics.balloonStyles se sobrepõe ao estilo embutido de mesmo id (speech, thought, whisper, shout, radio, caption, inner, note, sfx), ou a speech quando o id é novo. Os valores padrão abaixo são os de speech; a página de Quadrinhos lista o que cada estilo embutido muda. Os comprimentos em em são relativos ao tamanho do texto do balão.

PropriedadeTipoPadrãoDescrição
idstringobrigatórioA palavra com que uma linha do roteiro nomeia o estilo: ben{whisper}: … ou style=whisper.
namestringidNome legível, só para as interfaces de edição.
shape'oval' | 'rounded' | 'rectangle' | 'cloud' | 'burst' | 'wavy' | 'electric' | 'none''oval'O contorno em volta do texto. 'none' põe o texto direto sobre o desenho.
fillColorValuebrancoPreenchimento do balão.
strokeColorValuepretoCor do contorno.
strokeWidthDimension0.6ptEspessura do contorno.
dashbooleanfalseUm contorno tracejado (um sussurro).
doublebooleanfalseUm contorno duplo.
wobblenumber0Tremor de traço feito à mão no contorno, de 0 a 1, com uma semente tirada do balão para que nunca mude de uma composição para outra.
roundnessnumber2.2O expoente de superelipse de um oval: 2 é uma elipse, e valores maiores o deixam mais quadrado.
burstPointsnumber14As pontas de uma explosão; 0 as conta a partir do perímetro.
burstDepthnumber0.22A profundidade das pontas de uma explosão, como fração do raio do corpo.
paddingDimension0.55emO respiro entre o texto e o contorno.
aspectnumber1.6A razão entre largura e altura que a quebra de linhas busca para o bloco de texto (letreiramento horizontal).
tail'curved' | 'wedge' | 'bubbles' | 'zigzag' | 'none''curved'O rabicho.
tailWidthDimension0.9emA largura do rabicho no ponto em que sai do contorno.
tailReachnumber0.55Até onde o rabicho avança em direção a quem fala, como fração da distância entre o contorno e a boca.
target'mouth' | 'head''mouth'Para onde o rabicho aponta: a boca da âncora ou a cabeça (balões de pensamento).
position'auto' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' | 'top' | 'bottom''auto'Onde fica o balão quando a linha não fixa nenhuma posição: onde o letreiramento o colocar, ou num canto ou numa borda do quadro.
buttbooleanfalseEncosta na borda do quadro um balão de canto ou de borda (recordatórios).
fontFamilystringa fonte do letreiramentoA fonte deste estilo. sfx usa por padrão a fonte de onomatopeias do idioma: Bangers, Dela Gothic One em japonês, ZCOOL KuaiLe em chinês simplificado, LXGW WenKai TC em chinês tradicional, Lalezar em árabe (defaultComicSfxFont(locale)).
fontScalenumber1O tamanho do texto, como múltiplo do tamanho do letreiramento.
bold, italicbooleanos do letreiramentoTexto em negrito ou em itálico.
colorColorValuea do letreiramentoCor do texto.
textTransform'none' | 'uppercase'o do letreiramentoTudo em maiúsculas neste estilo.
letterSpacingDimensiono do letreiramentoEspaço extra entre as letras.
align'center' | 'start''center'Alinhamento das linhas dentro do balão.
haloDimensionnenhumUm contorno traçado em volta das letras, com essa espessura, para que o texto se leia sobre o desenho (onomatopeias).
haloColorColorValuebrancoA cor do halo.
rotatenumber0Rotação em graus, no sentido horário. O atributo rotate da linha tem prioridade.

#Elenco

Cada entrada de comics.cast descreve um personagem:

PropriedadeTipoPadrãoDescrição
idstringobrigatórioA chave de quem fala no roteiro e nas âncoras dos desenhos.
namestringidO nome que a saída HTML e o EPUB refluível imprimem antes das falas do personagem, e que os editores mostram.
balloonStylestringspeechO estilo das falas do personagem que não nomeiam nenhum.
colorColorValuea do estiloCor do texto dos balões do personagem.
fillColorValueo do estiloPreenchimento dos balões do personagem.
fontFamilystringa do estiloA fonte dos balões do personagem.

A color e a font próprias de uma linha têm prioridade sobre o elenco, e o elenco sobre o estilo de balão. Qualquer cor dos quadrinhos pode ser uma entrada da paleta, que acompanha a paleta como qualquer cor vinculada.

const resolved = resolveComicsConfig(config.comics, 'ja');
// => todos os campos preenchidos; Zen Antique e Dela Gothic One como fontes
 
const minimal = stripComicsDefaults(config.comics, 'ja');
// => undefined quando tudo está no valor padrão
 
const shout = pickBalloonStyle(resolved, 'shout'); // o grito embutido com as mudanças do livro
const night = pickPanelStyle(resolved, 'night');   // um estilo de quadro com nome ou, se não houver, o quadro padrão

#Unidades e cores

#Dimensões

Todas as medidas físicas no Postext usam o tipo Dimension, um valor acompanhado de uma unidade:

interface Dimension {
  value: number;
  unit: DimensionUnit; // 'cm' | 'mm' | 'in' | 'pt' | 'px' | 'em' | 'rem'
}

Unidades absolutas (cm, mm, in, pt, px) são convertidas em pixels com o DPI configurado. A 300 DPI, 1 cm equivale a cerca de 118 px.

Unidades relativas (em, rem) acompanham o tamanho de fonte atual. Um em é relativo ao tamanho de fonte do próprio elemento; rem é relativo ao tamanho de fonte do texto do corpo.

#Cores

As cores são guardadas com uma representação hexadecimal e um modelo de cor de destino:

interface ColorValue {
  hex: string;         // '#ff0000', 'transparent' etc.
  model: ColorModel;   // 'hex' | 'rgb' | 'cmyk' | 'hsl'
  cmyk?: CmykPercent;  // Os valores exatos de quadricromia de uma cor definida em CMYK.
}

O campo model indica o espaço de cor pretendido. Para renderização na web, o normal é 'hex' ou 'rgb'. Em fluxos de impressão, 'cmyk' diz que a cor foi especificada em CMYK, e cmyk guarda os seus valores: uma renderização de impressão em CMYK os aplica como estão, e hex é a forma como aparecem na tela (veja Cores definidas em CMYK).

Como o Postext mira uma saída de qualidade editorial, a cor padrão do texto do corpo vem com model: 'cmyk' (#000000). As cores de títulos, negrito, itálico e listas usam por padrão a Cor principal vinculada à paleta (#295AA3, model: 'hex'). O fundo da página e as sobreposições da interface (grade de linhas de base, marcas de corte, indicadores de depuração) usam por padrão model: 'hex'. Sobrescreva color.model em qualquer campo se precisar de outra semântica de exportação.

#Transparência

Uma cor pode ser translúcida. hex aceita um canal alfa, como #rgba ou #rrggbbaa. Também aceita uma cor rgb() / rgba(), na sintaxe com vírgulas ou com espaços, com o alfa como número ou porcentagem. transparent é totalmente transparente:

const config: PostextConfig = {
  header: {
    elements: [{
      kind: 'box',
      id: 'veil',
      placement: {
        anchor: { to: 'bleed', edge: 'top-left' },
        size: { width: 'fill', height: { value: 40, unit: 'mm' } },
      },
      style: { backgroundColor: { hex: '#ffffffb3', model: 'hex' } }, // branco a 70 %
    }],
  },
  bodyText: { color: { hex: 'rgba(0, 0, 0, 0.85)', model: 'rgb' } },
};

Os três renderizadores a pintam do mesmo jeito. O canvas e o visualizador HTML tomam o valor como uma cor CSS. O renderizador de PDF aplica a opacidade da cor como um alfa constante, um ExtGState com ca para preenchimentos e CA para traços. Isso vale para texto, fios, boxes, preenchimentos e bordas de tabela, chips, amostras de cor e fórmulas. Uma cor translúcida se compõe sobre tudo o que foi pintado antes dela. Um boxe no cabeçalho ou no rodapé é pintado por último, por isso vela o texto que está embaixo; um boxe numa faixa de abertura é pintado primeiro, por isso tinge a página sob o texto. Quando o PDF é forçado para outro espaço de cor (pdfGeneration.forceColorSpace com colorSpace: 'cmyk' ou 'grayscale'), a cor é convertida e seu alfa se mantém. No Sandbox, o controle deslizante de opacidade do seletor de cor grava esses valores como #rrggbbaa, e o seletor também lê as outras formas.

#Fontes personalizadas

O Postext resolve cada string de fontFamily tanto no catálogo do Google Fonts quanto na lista customFonts do documento. As fontes personalizadas têm prioridade quando os nomes coincidem: se você declarar customFonts: [{ name: 'Roboto', … }], o Postext usa o arquivo que você enviou em vez da “Roboto” do Google Fonts.

Use fontes personalizadas quando:

  • O documento precisa de uma família tipográfica da marca ou licenciada que não está no Google Fonts.
  • O ambiente não alcança a CDN do Google Fonts (offline, intranet, contextos sensíveis à privacidade).
  • Você precisa manter o binário da fonte privado e não enviá-lo a terceiros.

#Esquema de configuração

type CustomFontFormat = 'woff2' | 'woff' | 'ttf' | 'otf';
type CustomFontStyle = 'normal' | 'italic';
 
interface CustomFontVariant {
  weight: number;           // font-weight do CSS, 100..900
  style: CustomFontStyle;
  fileId: string;           // id opaco do binário num armazenamento à parte
  format: CustomFontFormat;
  fileName?: string;        // nome original do arquivo enviado (opcional, mostrado na interface)
}
 
interface CustomFontFamily {
  name: string;             // usado em qualquer lugar onde caiba um nome de família do Google Fonts
  variants: CustomFontVariant[];
}
 
interface PostextConfig {
  // ...
  customFonts?: CustomFontFamily[];
}

O binário de cada variante não fica embutido na própria configuração. A configuração guarda apenas ponteiros fileId; os bytes ficam num armazenamento à parte. No Sandbox, isso significa IndexedDB (armazenamento chave-valor, só no navegador, privado do documento). Quem integra o Postext em outro ambiente pode resolver fileId como quiser (um endpoint no servidor, o cache de um service worker, qualquer coisa), desde que os bytes cheguem à thread principal antes de buildDocument rodar.

#Como gerenciar fontes personalizadas no Sandbox

Abra o painel Fontes na barra de atividades à esquerda (entre Recursos e Design). A lista Famílias tipográficas deste livro mostra cada família que o design usa, com sua função e se vem do Google Fonts ou de um arquivo seu. Em Seus arquivos de fonte, para cada família:

  1. Adicionar família: cria uma família vazia; renomeie-a ali mesmo.
  2. Enviar variante(s): escolha um peso (100–900) e um estilo (normal / itálico) e selecione um ou vários arquivos .woff2, .woff, .ttf ou .otf. Cada arquivo vira uma variante própria, associada ao par (peso, estilo) selecionado no momento; o nome do arquivo enviado fica registrado e aparece na linha, para você distinguir as variantes. Você pode reajustar o peso ou o estilo de uma variante nos menus suspensos a qualquer momento.
  3. Variantes duplicadas são permitidas. Se dois arquivos caírem no mesmo par (peso, estilo), os dois são mantidos e aparece um aviso Variante de fonte duplicada, para você saber que deve diferenciar os ajustes das variantes extras.
  4. Excluir variante ou Excluir família: remove a entrada da configuração e os bytes guardados no IndexedDB.

Depois que uma família é declarada, todo seletor de fonte a agrupa em Personalizado, acima da lista do Google Fonts. Selecioná-la liga a família a cada campo de família tipográfica em que você a aplicar.

#Comportamento na renderização

Por baixo dos panos:

  • Quando customFonts muda, cada família declarada é registrada automaticamente como entradas FontFace em document.fonts. Assim, o visualizador HTML, a área de visualização do canvas (que mede por meio de document.fonts) e qualquer referência CSS direta passam a usar a fonte personalizada sem que o usuário precise abrir antes o seletor de fontes.
  • O worker de layout recebe os mesmos ArrayBuffers pelo caminho já existente de transferência de fontes, de modo que a medição (buildFontString, pretext) produz métricas idênticas às do Google Fonts.
  • Alterar ou remover uma variante descarta a fonte que o worker tinha em cache para essa família e a registra de novo na composição seguinte, para que as visualizações acompanhem o conjunto atual de variantes.
  • Exportação para PDF: os binários enviados passam pelo mesmo pipeline de PdfFontProvider. Arquivos .woff2 são descompactados; .ttf e .otf passam direto. .woff é recusado com um erro claro (o pdf-lib não consegue embutir WOFF puro; envie de novo como .woff2/.ttf/.otf). OpenType com contornos CFF (.otf com o número mágico OTTO) é embutido sem subconjunto, porque o gerador de subconjuntos CFF do pdf-lib percorre cada glifo no momento do save() e pode travar por minutos com fontes reais; abrir mão do subconjunto troca um PDF um pouco maior por tempos de renderização estáveis.

#Avisos de fontes ausentes

O painel Verificações do Sandbox lista, no grupo Fontes, três falhas específicas das fontes personalizadas (todas ativadas pela mesma opção debug.warnings.missingFont que já controla o aviso genérico de fonte “não carregada”):

  • Família de fontes desconhecida: um fontFamily cita um nome que não é uma fonte conhecida do Google Fonts nem uma família personalizada declarada no momento. O aviso também aparece de imediato quando você exclui uma família personalizada que algum campo fontFamily ainda cita, em vez de esperar que o DOM perceba.
  • Variante de fonte ausente: a família existe, mas pelo menos uma das combinações padrão de peso/estilo (400 / 700, normal / itálico) não tem arquivo enviado. O aviso lista as combinações que faltam.
  • Variante de fonte duplicada: dois ou mais arquivos enviados ocupam o mesmo par (peso, estilo) dentro de uma família. Só um arquivo é usado de fato na renderização; o aviso sugere que você reajuste as demais entradas.

Clicar em qualquer um desses avisos abre o painel Fontes, para você enviar a variante que falta, adicionar a família de novo ou diferenciar as duplicadas.

#Paleta de cores

A propriedade colorPalette de PostextConfig permite definir um conjunto reutilizável de cores com nome e referenciá-las a partir de qualquer ColorValue da configuração. É o equivalente, no Postext, às propriedades personalizadas do CSS ou ao painel de amostras do InDesign: você muda a entrada da paleta uma vez, e todas as cores que apontam para ela se atualizam no documento inteiro.

interface ColorPaletteEntry {
  id: string;       // identificador estável, referenciado por ColorValue.paletteId
  name: string;     // rótulo legível mostrado nas interfaces do Sandbox
  value: ColorValue;
}

#A paleta padrão

O Postext vem com uma paleta padrão de uma única entrada, chamada Cor principal (id: 'main-color', hex #295AA3). Vários valores padrão (a cor dos títulos, a cor de negrito/itálico do corpo, a cor de :ref, as cores dos marcadores e dos números de lista) referenciam essa entrada por meio de paletteId: 'main-color', de modo que mudar essa única amostra retinge todas as partes do documento que a usam.

Você pode inspecionar a paleta padrão, cloná-la ou comparar com ela por meio de três exportações:

import {
  DEFAULT_COLOR_PALETTE,
  cloneDefaultColorPalette,
  isDefaultColorPalette,
} from 'postext';
 
// Instantâneo somente leitura da paleta que vem com o pacote.
DEFAULT_COLOR_PALETTE;
// => [{ id: 'main-color', name: 'Main Color', value: { hex: '#295AA3', model: 'hex' } }]
 
// Cópia independente: altere esta, não DEFAULT_COLOR_PALETTE.
const palette = cloneDefaultColorPalette();
 
// Detecta se o usuário personalizou a paleta de alguma forma.
isDefaultColorPalette(palette); // true

A paleta fica no nível superior da configuração:

const config: PostextConfig = {
  colorPalette: [
    { id: 'ink',    name: 'Ink',    value: { hex: '#0a0a0a', model: 'cmyk' } },
    { id: 'accent', name: 'Accent', value: { hex: '#b8860b', model: 'hex' } },
  ],
  bodyText: { color: { hex: '#000000', model: 'cmyk', paletteId: 'ink' } },
  headings: { color: { hex: '#000000', model: 'hex', paletteId: 'accent' } },
};

#Como referenciar uma entrada da paleta

Qualquer ColorValue da configuração pode levar um campo opcional paletteId que aponta para uma entrada de colorPalette: fundo da página, cores do texto do corpo (incluída a cor de :ref), cores dos títulos, fios entre colunas, cores de listas, cores de tabelas, legendas, chips e boxes (caixa, faixa, ícone, marcador, rótulo, título, corpo), cores das marcas de corte e da grade de linhas de base, indicadores de depuração e todas as cores de um design: os cabeços, as aberturas de título e os designs na coluna, os designs e cabeços dos estilos de título, as páginas de parte e as linhas de parte do sumário (texto, fio, preenchimento e borda da caixa, contorno, capitular). Quando presente, o hex / model da entrada da paleta prevalece sobre o hex / model de reserva guardado ao lado. O valor de reserva embutido só é usado se a paleta não existir, estiver vazia ou não contiver aquele id, o que é útil ao distribuir uma configuração que será lida por uma ferramenta que não entende paletas.

Mudou no postext 1.5. Até o postext 1.4, a paleta chegava só a uma lista fixa de ajustes: as cores dos designs (cabeços, aberturas, estilos de título, partes, linhas do sumário), bodyText.referenceColor, as cores dos rótulos dos boxes e as cores de negrito / itálico do corpo dos boxes mantinham o hex guardado ao lado do seu paletteId. Um documento cujo valor guardado difere da entrada da paleta (todos os editados no Sandbox depois que a entrada mudou, e cada :ref quando a Cor principal não é #295AA3) agora imprime a cor da paleta, como o vínculo indica. Para manter uma cor como estava, remova o paletteId dela.

#Como as paletas são aplicadas

buildDocument aplica a paleta em dois momentos, para que as cores referenciadas funcionem tanto nas sobrescritas que você escreveu quanto nos valores padrão preenchidos depois:

  1. applyPaletteToConfig(config): resolve cada ColorValue da configuração bruta do usuário que traga um paletteId. Útil quando você quer inspecionar o que o motor vai de fato receber.
  2. applyPaletteToResolvedConfig(resolved, palette): roda depois que os valores padrão são resolvidos e reescreve os padrões vinculados à paleta (cor dos títulos, cor de negrito/itálico do corpo, cor de :ref, cores de listas, as cores dos designs padrão) para que acompanhem a paleta ativa.

As duas percorrem a configuração inteira, de modo que nenhuma cor vinculada à paleta fica para trás. A maioria das cores do fluxo de texto (texto do corpo, títulos, listas, tabelas, legendas, chips, a caixa, o título e o corpo do boxe) sai como valores simples. Todas as outras (as cores dos designs, a cor de :ref, os rótulos dos boxes) recebem o hex / model da paleta e mantêm seu paletteId. É esse vínculo que o atributo palette de uma parte e a sobrescrita palette de um estilo de título substituem nas suas páginas (veja Partes), por isso ele precisa sobreviver. htmlViewer.overrides fica como foi escrito: o visualizador HTML o mescla primeiro, e uma paleta que ele traga se aplica então a tudo, incluídos os designs.

Raramente você vai precisar chamá-las diretamente, mas as duas são exportadas para que você possa inspecioná-las ou reutilizá-las:

import {
  applyPaletteToConfig,
  applyPaletteToResolvedConfig,
  resolveColorValue,
} from 'postext';
 
const flat = applyPaletteToConfig(config);
// Cada ColorValue com paletteId na configuração bruta agora leva o
// hex/model da entrada da paleta (uma cor de design mantém seu paletteId).
 
// `applyPaletteToResolvedConfig` normalmente fica a cargo de buildDocument; use-a
// diretamente se você montar um ResolvedConfig por conta própria e quiser a paleta aplicada.

resolveColorValue(value, palette, fallback) é a variante para um único valor, prática quando você compõe configurações de forma imperativa e precisa resolver uma cor por vez.

#Como editar a paleta

Uma cor cujo paletteId não nomeia nenhuma entrada imprime o hex / model guardado, que pode ser mais antigo que a cor que a entrada lhe dava. Por isso, antes de remover uma entrada, reescreva cada ColorValue vinculado a ela como uma cor simples com o valor atual da entrada. A seção Paleta do Sandbox (Design → Cores) faz isso quando você exclui uma entrada, esteja a cor onde estiver (incluídos os designs e os rótulos dos boxes), e a confirmação lista cada ajuste que usa a entrada: pelo nome, ou pelo caminho na configuração (header.elements[2].color).

#Visualizador HTML

A propriedade htmlViewer controla como o renderizador HTML dispõe as páginas na tela. Ela só se aplica quando você renderiza com renderToHtml / renderToHtmlIndexed; os caminhos de canvas e PDF a ignoram por completo, pois consomem diretamente page.width, page.height e page.dpi configurados.

interface HtmlViewerConfig {
  maxCharsPerLine?: number;     // Largura de coluna desejada, em caracteres da fonte do corpo.
  columnGap?: number;            // Espaço horizontal entre colunas no modo de várias colunas (px).
  optimalLineBreaking?: boolean; // Usa Knuth–Plass no visualizador HTML em vez do algoritmo guloso.
  overrides?: HtmlViewerOverrides; // Configuração parcial só para a tela, mesclada sobre a do documento.
}
 
type HtmlViewerOverrides = Omit<PostextConfig, 'htmlViewer'>;
PropriedadeTipoPadrãoDescrição
maxCharsPerLinenumber70Medida desejada para cada coluna renderizada, expressa em caracteres da fonte do corpo. A área de visualização mede uma amostra representativa de prosa com esse comprimento para chegar à largura real em pixels, de modo que o resultado se adapta a qualquer combinação de fonte proporcional e tamanho de fonte.
columnGapnumber50Espaço horizontal, em pixels CSS, entre as colunas quando o visualizador está no modo de várias colunas. Ignorado no modo de coluna única.
optimalLineBreakingbooleanfalseAtiva a quebra de linhas Knuth–Plass no visualizador HTML. Vem desligada porque o visualizador refaz o layout a cada redimensionamento e mudança de tamanho de fonte, e o algoritmo guloso (primeiro encaixe) é rápido o bastante para parecer instantâneo. Ligue-a quando quiser as mesmas quebras ótimas que o renderizador de canvas usa.
overridesHtmlViewerOverrides—Uma configuração parcial do documento que vale só na tela. O visualizador HTML a mescla sobre a configuração do documento antes de fazer o layout (applyHtmlViewerOverrides); canvas e PDF a ignoram. Objetos se mesclam recursivamente; um array levels (títulos, listas, sumário) se mescla entrada por entrada pelo level; qualquer outro array (os elements de um espaço de design, calloutStyles, colorPalette…) substitui o array de base por inteiro. Uso típico: uma abertura de capítulo sem as faixas da versão impressa, ou uma página de parte cujo título quebra encostado no número em vez de numa largura fixa da caixa de refile. O Sandbox a edita como JSON.
const config: PostextConfig = {
  headings: { levels: [{ level: 1, span: 'page', breakBefore: { enabled: true } }] },
  htmlViewer: {
    // Na tela, os capítulos seguem corridos, sem a abertura de página inteira.
    overrides: { headings: { levels: [{ level: 1, span: 'column', breakBefore: { enabled: false } }] } },
  },
};

O resolvedor e o redutor seguem o mesmo padrão das outras seções:

import {
  DEFAULT_HTML_VIEWER_CONFIG,
  resolveHtmlViewerConfig,
  stripHtmlViewerDefaults,
} from 'postext';
 
const resolved = resolveHtmlViewerConfig(config.htmlViewer);
// => { maxCharsPerLine: 70, columnGap: 50, optimalLineBreaking: false }
 
const minimal = stripHtmlViewerDefaults(config.htmlViewer);
// => undefined quando tudo coincide com os valores padrão

Veja Como integrar o visualizador HTML abaixo para um exemplo completo.

#Geração de PDF (configuração)

A propriedade pdfGeneration controla como o renderizador de PDF gera o documento final. Esses ajustes são lidos pelo pacote postext-pdf no momento da exportação; os visualizadores de canvas e HTML os ignoram.

buildDocument os leva na VDT, como doc.config.pdfGeneration, e renderToPdf toma cada ajuste do primeiro lugar que o fornece:

  1. suas próprias opções (outlines, accessible, colorSpace);
  2. o pdfGeneration do primeiro documento que renderiza (num livro, os ajustes do primeiro capítulo valem para o arquivo inteiro);
  3. os valores padrão: marcadores e marcação de estrutura ligados, cor RGB.

Assim, renderToPdf(doc, { fontProvider }) segue a configuração, e uma opção passada a renderToPdf prevalece apenas para aquele ajuste. forceColorSpace e colorSpace juntos equivalem à opção colorSpace: o colorSpace da configuração vale enquanto forceColorSpace estiver ligado, e o PDF é RGB enquanto estiver desligado. Versões anteriores do postext-pdf liam só as opções; uma configuração que define pdfGeneration agora muda o PDF de quem chama sem passar opções.

type PdfColorSpace = 'rgb' | 'cmyk' | 'grayscale';
 
interface PdfGenerationConfig {
  outlines?: boolean;          // Gera marcadores de PDF a partir da árvore de títulos.
  forceColorSpace?: boolean;   // Converte todas as cores para `colorSpace`.
  colorSpace?: PdfColorSpace;  // Espaço de destino usado quando `forceColorSpace` é true.
  accessible?: boolean;        // Saída marcada, orientada a PDF/UA (árvore de estrutura, texto alternativo, idioma).
}
PropriedadeTipoPadrãoDescrição
outlinesbooleantrueGera os outlines (marcadores) do PDF a partir da hierarquia de títulos, para que o leitor salte direto para qualquer título pela barra lateral de um leitor de PDF. Desligue em documentos em que a árvore de títulos não faz sentido (por exemplo, cartazes de uma página).
forceColorSpacebooleanfalseQuando é true, todas as cores do PDF renderizado são convertidas para colorSpace no momento da exportação. Deixe desligado em PDFs pensados para a tela, em que as cores de entrada já estão no espaço desejado; ligue para garantir um único espaço de cor quando as fontes são misturadas.
colorSpace'rgb' | 'cmyk' | 'grayscale''cmyk'Espaço de cor de destino usado quando forceColorSpace está ligado. Use 'cmyk' para impressão offset, 'rgb' para PDFs só de tela e 'grayscale' para provas de impressão em preto e branco. Não tem efeito quando forceColorSpace é false. O CMYK é separado com o perfil de saída de print (FOGRA39 por padrão) e o seu tratamento do preto, e as imagens RGB também são convertidas; com um padrão PDF/X definido ali, o arquivo sai em CMYK diga isto o que disser.
accessiblebooleantrueGera um PDF acessível e marcado, orientado ao PDF/UA-1: uma árvore de estrutura lógica na ordem de leitura (títulos que nunca pulam um nível, parágrafos, listas, citações em bloco, boxes, tabelas com células de cabeçalho, figuras com seu texto alternativo e suas legendas, fórmulas, referências clicáveis como links, o conteúdo de um :::toc como um único TOC com um TOCI por linha: o número da linha como Lbl, o título e a página como uma Reference que contém o link), o título e o idioma do documento (o locale do nível superior), a identificação PDF/UA nos metadados XMP, e cada marca decorativa (fundo da página, fios, grade de linhas de base, cabeçalhos e rodapés correntes, marcas de corte, cabeçalhos de tabela repetidos, o título repetido e o marcador de continuação de um boxe dividido) sinalizada como artefato, para que os leitores de tela a ignorem. Uma figura sem altText usa a legenda e, na falta dela, o rótulo. Uma figura ou tabela flutuante é lida logo depois do texto que a cita pela primeira vez, ou do texto anterior à sua linha ::resource, e um boxe flutuante depois do texto anterior à sua cerca, mesmo quando o flutuante vai para uma página posterior; uma lista ou o sumário que continuam depois de um flutuante permanecem um único elemento. Desligue só em matrizes de impressão em que a estrutura extra não é desejada.
pdfGeneration: {
  outlines: true,
  accessible: true,
  forceColorSpace: true,
  colorSpace: 'cmyk',
}

O resolvedor e o redutor seguem o padrão das outras seções:

import {
  DEFAULT_PDF_GENERATION_CONFIG,
  resolvePdfGenerationConfig,
  stripPdfGenerationDefaults,
} from 'postext';
 
const resolved = resolvePdfGenerationConfig(config.pdfGeneration);
// => { outlines: true, forceColorSpace: false, colorSpace: 'cmyk', accessible: true }
 
const minimal  = stripPdfGenerationDefaults(config.pdfGeneration);
// => undefined quando tudo coincide com os valores padrão

Veja Como gerar PDFs abaixo para a receita completa de exportação.

#Produção gráfica (configuração)

A propriedade print diz como um livro vai para a gráfica: o padrão PDF/X do arquivo, o perfil de saída com que o seu CMYK é separado, como o preto é impresso e os limites do preflight. A diagramação a ignora, então mudá-la nunca move uma linha. Três coisas a leem: o postext-pdf quando grava o arquivo, preflightDocument quando verifica um documento diagramado, e a simulação de impressão dos visualizadores canvas e Folio.

type PdfXStandard = 'none' | 'pdfx1a' | 'pdfx4';
 
interface PrintConfig {
  standard?: PdfXStandard;                 // 'none': um PDF comum.
  outputProfile?: string;                  // Um id do catálogo ('fogra39', 'fogra51'…) ou 'custom'.
  customProfile?: CustomOutputProfile;     // Um arquivo .icc enviado.
  renderingIntent?: 'relative' | 'perceptual';
  blackPointCompensation?: boolean;
  convertImages?: boolean;                 // Separa as imagens RGB (o PDF/X-1a sempre separa).
  inkLimit?: number;                       // Cobertura total de tinta, em porcentagem.
  black?: PrintBlackConfig;
  preflight?: PrintPreflightConfig;
}
 
interface CustomOutputProfile {
  name: string;           // A descrição do perfil, ou o nome do arquivo.
  fileId: string;         // O arquivo .icc guardado.
  registryName?: string;  // O nome da condição no registro ICC (FOGRA51…); senão, 'Custom'.
  inkLimit?: number;
}
PropriedadeTipoPadrãoDescrição
standard'none' | 'pdfx1a' | 'pdfx4''none'A variante PDF/X do arquivo. 'pdfx1a' grava PDF/X-1a:2003: só CMYK e cinza, sem transparências, aceito por qualquer gráfica. 'pdfx4' grava PDF/X-4: mantém as transparências e o gerenciamento de cor, para os fluxos atuais. Os dois separam cada cor com o perfil de saída, diga o que disser pdfGeneration.colorSpace.
outputProfilestring'fogra39'A condição de impressão para a qual o CMYK é separado: um id do catálogo de perfis, ou 'custom' para customProfile. Um id que o catálogo não tem, ou 'custom' sem arquivo, volta ao padrão e gera um aviso de configuração.
customProfileCustomOutputProfilenenhumUm perfil de saída CMYK que você fornece, o que a sua gráfica lhe der (o PSOcoated_v3.icc da ECI, por exemplo). Os bytes dele são guardados à parte, como os de uma fonte; renderToPdf os recebe na opção outputProfile. registryName é gravado como identificador da condição na condição de saída.
renderingIntent'relative' | 'perceptual''relative'A colorimétrica relativa mantém exatas as cores que a máquina consegue imprimir e leva as outras à cor imprimível mais próxima; a perceptual comprime toda a gama, para que as cores fora dela mantenham as relações entre si.
blackPointCompensationbooleantrueCom a intenção relativa, leva o preto da tela ao preto mais escuro que a máquina imprime, para que os tons mais escuros mantenham o detalhe em vez de empastar.
convertImagesbooleantrueSepara as imagens RGB em CMYK com o perfil. O PDF/X-1a sempre separa. No PDF/X-4, false as deixa em RGB, marcadas como sRGB pelo /DefaultRGB das páginas, para que o RIP da gráfica as converta. Os JPEGs em CMYK e em cinza são sempre incorporados como estão.
inkLimitnumbero do perfilO maior total de C+M+Y+K, em porcentagem, que o preflight aceita. Por padrão, o limite para o qual o perfil separa (300 % na maioria das condições de offset, 230 % no papel jornal IFRA26).
blackPrintBlackConfigveja PretoCinzas só em K, sobreimpressão e preto composto.
preflightPrintPreflightConfigveja PreflightO que o preflight verifica e os seus limites.
print: {
  standard: 'pdfx4',
  outputProfile: 'fogra51',
  black: { richBlackColor: { c: 60, m: 40, y: 40, k: 100 } },
  preflight: { minImageResolution: 300, safeZone: { value: 5, unit: 'mm' } },
}

#Perfis de saída

O postext traz estes perfis de saída CMYK na sua pasta icc/ (postext/icc/<id>.icc em qualquer CDN do npm, e /icc/<id>.icc em postext.dev). Nenhum deles tem restrições de direitos autorais conhecidas (CC0): os perfis FOGRA, GRACoL, SWOP e de jornal do colord, gerados a partir dos dados de caracterização de cada condição, e o FOGRA51 e o FOGRA52, construídos pelo postext com o ArgyllCMS a partir dos dados da própria Fogra. Os perfis da ECI (ISO Coated v2, PSO Coated v3, PSO Uncoated v3) descrevem as mesmas condições, mas não podem ser redistribuídos; envie-os como perfil personalizado se a sua gráfica pedir um deles.

IdCondiçãoNome no registroLimite de tinta
fogra39Offset, papel couché (condição ISO Coated v2)FOGRA39300 %
fogra51Offset, couché premium (condição PSO Coated v3)FOGRA51300 %
fogra52Offset, papel sem revestimento sem madeira (condição PSO Uncoated v3)FOGRA52300 %
fogra47Offset, papel branco sem revestimento (PSO Uncoated ISO 12647)FOGRA47300 %
fogra29Offset, papel branco sem revestimentoFOGRA29300 %
fogra30Offset, papel amarelado sem revestimentoFOGRA30340 %
fogra27Offset, couché (ISO 12647-2:1996)FOGRA27300 %
fogra28Rotativa offset heatset, LWC brilhanteFOGRA28300 %
fogra45Rotativa offset heatset, LWC melhoradoFOGRA45300 %
fogra40Rotativa offset heatset, papel SCFOGRA40340 %
gracol2006GRACoL 2006, couché grau 1CGATS TR 006300 %
swop3SWOP 2006, couché grau 3CGATS TR 003300 %
swop5SWOP 2006, couché grau 5CGATS TR 005300 %
ifra26Papel jornal coldset (ISO 12647-3)IFRA26230 %
snap2007Papel jornal SNAP 2007CGATS TR 002320 %

renderToPdf lê os bytes do perfil na sua opção outputProfile; sem eles, baixa o arquivo do catálogo de profileBaseUrl (por padrão https://cdn.jsdelivr.net/npm/postext/icc/). Uma renderização PDF/X cujo perfil não pode ser carregado falha; uma renderização CMYK comum usa a fórmula simples e informa um aviso outputProfileUnavailable.

import { readFile } from 'node:fs/promises';
import { renderToPdf } from 'postext-pdf';
 
const pdf = await renderToPdf(doc, {
  fontProvider,
  print: { standard: 'pdfx1a', outputProfile: 'fogra39' },
  outputProfile: await readFile('node_modules/postext/icc/fogra39.icc'),
});

#PDF/X-1a e PDF/X-4

Os dois padrões gravam:

  • a condição de saída (GTS_PDFX), que nomeia a condição de impressão e incorpora o perfil de destino;
  • a identificação no dicionário Info (GTS_PDFXVersion, /Trapped /False, o título e as datas) e nos metadados XMP (pdfxid:GTSPDFXVersion, os ids do documento e da versão), junto com a identificação PDF/UA quando o arquivo é marcado;
  • uma TrimBox e uma BleedBox em cada página (a página inteira quando não há marcas de corte);
  • o /ID do trailer;
  • cada cor em DeviceCMYK (ou cinza) passada pelo perfil, e as marcas de corte em cor de registro;
  • nenhuma anotação de link: um arquivo para a gráfica não traz nenhuma dentro da caixa de sangria, então os links do PDF de tela ficam de fora (os marcadores ficam).

O PDF/X-1a:2003 é PDF 1.4 sem fluxos de objetos e não tem transparências: uma cor translúcida é aplicada como sairia impressa sobre o papel, o alfa de uma imagem é achatado sobre branco, e a página em negativo de depuração fica de fora (com um aviso pageNegativeIgnored). O PDF/X-4 é PDF 1.6: as transparências ficam, cada página recebe um grupo de transparência que mescla em CMYK, e as imagens RGB mantidas por convertImages: false são marcadas como sRGB pelo /DefaultRGB.

Uma matriz de impressão em PDF (svg.pdfFileId) é incorporada como está, então as cores, as fontes e as transparências dela são as suas; o preflight informa o que ela traz.

#Preto

interface PrintBlackConfig {
  kOnlyNeutrals?: boolean;      // Cinzas e preto só com tinta preta.
  overprint?: boolean;          // O preto 100 % K sobreimprime.
  richBlack?: boolean;          // Áreas pretas grandes em preto composto.
  richBlackColor?: CmykPercent; // { c, m, y, k } em porcentagem.
  richBlackMinSize?: Dimension; // O lado menor de que uma área precisa.
}
PropriedadeTipoPadrãoDescrição
kOnlyNeutralsbooleantrueUma cor neutra (#000000, #808080…) é impressa só com tinta preta, com o K escolhido para que a luminosidade coincida, nunca como um cinza de quadricromia que muda com o registro. As imagens mantêm a geração de preto do próprio perfil.
overprintbooleantrueTudo o que é pintado só em 100 % K (texto preto, fios, traços, formas pretas pequenas) sobreimprime (op/OP com OPM 1), e assim uma chapa que se desloca na máquina nunca abre uma borda branca em volta. Todo o resto vaza; imagens e degradês nunca sobreimprimem.
richBlackbooleantrueUm preenchimento preto cujo lado menor chega a richBlackMinSize (um fundo, uma faixa, uma caixa) é impresso em richBlackColor e vaza, para parecer profundo em vez de cinza-escuro. O texto nunca vira preto composto.
richBlackColorCmykPercentA receita do preto composto, em porcentagem. Mantenha o total abaixo do limite de tinta; o preflight o verifica.
richBlackMinSizeDimension6mmO lado menor que uma área preta precisa ter para ser impressa em preto composto.

#Cores definidas em CMYK

Uma cor escrita em CMYK mantém os seus valores exatos: ColorValue.cmyk (em porcentagem) é aplicado como está numa renderização de impressão, e hex é a forma como ela aparece na tela. Uma entrada da paleta definida em CMYK vale para todas as cores vinculadas a ela.

colorPalette: [
  { id: 'brand', name: 'Brand', value: { hex: '#00a0e3', model: 'cmyk', cmyk: { c: 100, m: 0, y: 0, k: 0 } } },
],

#Preflight

interface PrintPreflightConfig {
  enabled?: boolean;
  minImageResolution?: number;       // ppi no tamanho impresso.
  criticalImageResolution?: number;
  minRuleWidth?: Dimension;
  smallTextSize?: Dimension;
  safeZone?: Dimension;
  bleedSnap?: Dimension;
  checkFonts?: boolean;
}
PropriedadeTipoPadrãoDescrição
enabledbooleantrueExecuta as verificações.
minImageResolutionnumber300Um bitmap colocado com menos pixels por polegada que isso no tamanho impresso, recorte incluído, recebe um aviso: figuras, imagens em células de tabela, imagens de design, quadros de quadrinhos. Um bitmap composto no seu próprio tamanho é impresso com page.dpi, então uma página diagramada a 150 dpi imprime cada imagem assim a 150 ppi.
criticalImageResolutionnumber150Abaixo disso, o aviso é crítico. Nunca acima de minImageResolution.
minRuleWidthDimension0.25ptFios, bordas, fios entre colunas, fios de tabela e bordas de quadros de quadrinhos mais finos que isso.
smallTextSizeDimension9ptTexto menor que isso composto em mais de uma tinta (uma cor de quadricromia, preto composto): fica borrado quando as chapas se deslocam. O texto preto, só em K, nunca conta.
safeZoneDimension5mmTexto mais perto do refile que isso, onde a guilhotina pode cortá-lo.
bleedSnapDimension3mmUma caixa ou imagem que para a essa distância do refile sem chegar à sangria: leve-a para a sangria ou recue-a.
checkFontsbooleantrueInforma as fontes que um PDF inserido não incorpora (o Sandbox inspeciona cada matriz de impressão com inspectPrintMaster). O Postext incorpora todas as fontes que compõe.

preflightDocument(doc, options) executa as verificações num documento diagramado e devolve uma lista de problemas, cada um com um kind, uma severity ('critical', 'warning' ou 'info'), o pageIndex absoluto no livro, o rect do elemento em questão (px da página) e, quando o elemento tem, o seu intervalo no código-fonte. Os tipos são lowImageResolution, rgbImage, thinRule, smallProcessText, inkLimit, safeZone e nearTrim. Sem um transform, uma cor neutra conta como uma tinta e qualquer outra como três, e a cobertura não é verificada; com um, as contagens e a cobertura são exatas.

import { outputTransform, parseIccProfile, preflightDocument, resolvePrintConfig } from 'postext';
import { inspectPrintMaster } from 'postext-pdf';
 
const print = resolvePrintConfig(config.print);
const transform = outputTransform(parseIccProfile(fogra51Bytes), { intent: print.renderingIntent });
const issues = preflightDocument(doc, {
  print,
  transform,
  resources,                                    // tamanhos em pixels dos bitmaps
  imageColor: (fileId) => colourOf(fileId),     // 'rgb' | 'cmyk' | 'gray', tirado do arquivo
});
if (issues.some((i) => i.severity === 'critical')) process.exit(1);
 
const master = await inspectPrintMaster(masterBytes);
// => { nonEmbeddedFonts: ['Helvetica'], rgb: true, transparency: false }

#Simulação de impressão

Uma página do canvas pode ser pintada como vai sair impressa. createPrintPreview(transform, print, { paper, dpi }) monta a prova de cor em tela de uma configuração: cada pixel é separado com o perfil (cinzas só em K, como no PDF) e mostrado de volta na tela, sobre o branco do próprio papel quando paper é true; as áreas pretas grandes o bastante para o preto composto aparecem em preto composto. Passe-a a renderPageToCanvas como printPreview; guides acrescenta as linhas do refile, da sangria e da área de segurança, e marksFor contorna áreas de uma página (os rects do preflight). O postext-folio recebe o mesmo objeto como printPreview (com paper: false, já que o tom do papel do livro tinge as suas páginas).

import { createPrintPreview, renderPageToCanvas } from 'postext';
 
const preview = createPrintPreview(transform, print, { paper: true, dpi: doc.config.page.dpi });
renderPageToCanvas(page, doc, canvas, {
  printPreview: { ...preview, guides: { safeZonePx: 59 }, marksFor: () => issues.map((i) => i.rect!).filter(Boolean) },
});

#Motor de cor

O gerenciamento de cor que o postext usa é exportado para as suas próprias ferramentas. Ele lê perfis ICC v2 e v4 (matriz/TRC e as tabelas de consulta mft1, mft2, mAB, mBA) em TypeScript puro, sem WebAssembly.

  • parseIccProfile(bytes) lê um perfil; deviceChannels(profile) dá o seu número de canais.
  • outputTransform(profile, { intent, blackPointCompensation, preserveNeutrals }) devolve fromRgb(r, g, b) (sRGB 0..1 → CMYK 0..1), toLab(cmyk, paper?) e proof(cmyk, paper?) (CMYK → sRGB de tela).
  • cmykToLab, labToCmyk, srgbToLab, labToSrgb, deltaE e totalAreaCoverage são as conversões avulsas; buildRgbLut / sampleRgbLut criam e leem tabelas densas para o trabalho por pixel.
  • OUTPUT_PROFILES, outputProfileInfo(id) e loadOutputProfile(id, baseUrl?) dão o catálogo; srgbProfileBytes() grava o perfil sRGB com que o PDF/X-4 marca o RGB; authoredCmykColors(config) lista as cores que uma configuração define em CMYK.

O resolvedor e o redutor seguem o padrão das outras seções: resolvePrintConfig, stripPrintDefaults e profileInkLimit(config) (o limite do perfil que uma configuração nomeia, antes de qualquer inkLimit que o substitua), com DEFAULT_PRINT_CONFIG, DEFAULT_PRINT_BLACK_CONFIG, DEFAULT_PRINT_PREFLIGHT_CONFIG e DEFAULT_RICH_BLACK.

#Visualizador Folio (configuração)

A propriedade folio define como o visualizador Folio (postext-folio) apresenta o livro impresso em 3D: o ângulo de visão, o papel, a encadernação, a superfície onde o livro está e a luz. O layout a ignora, assim como as saídas de canvas, HTML e PDF. buildDocument leva os ajustes resolvidos na VDT como doc.config.folio quando a configuração define algum, de modo que um documento sem eles mantém seu hash de layout.

interface FolioConfig {
  tilt?: number;                  // Graus a partir da vista de cima, 0–70.
  yaw?: number;                   // Graus em volta do livro, −180–180.
  paper?: {
    type?: 'uncoated' | 'bookWove' | 'coatedMatte' | 'coatedSilk' | 'coatedGloss'
         | 'bible' | 'newsprint' | 'cardStock' | 'board';
    grammage?: number;            // g/m²
    bulk?: number;                // cm³/g; espessura em µm = gramatura × bulk
    finish?: 'auto' | 'uncoated' | 'matte' | 'silk' | 'gloss';
    texture?: 'auto' | 'smooth' | 'vellum' | 'wove' | 'laid' | 'linen' | 'felt';
    textureStrength?: number;     // 0–2
    shade?: ColorValue;
    showThrough?: boolean;
  };
  binding?: {
    type?: 'hardcover' | 'paperback' | 'sewn' | 'layflat' | 'saddleStitch' | 'folded';
    cover?: 'case' | 'pages';
    coverMaterial?: 'auto' | 'cloth' | 'paper' | 'leather';
    coverColor?: ColorValue;
    spineImage?: string;          // id do recurso
  };
  surface?: {
    type?: 'oak' | 'walnut' | 'linen' | 'felt' | 'leather' | 'marble' | 'plain' | 'none';
    color?: ColorValue;
  };
  lighting?: {
    environment?: 'studio' | 'daylight' | 'lamp' | 'overcast' | 'night';
    intensity?: number;           // 0.25–2
    shadows?: boolean;
  };
}
PropriedadeTipoPadrãoDescrição
tiltnumber22Ângulo da vista em relação à vertical, em graus, limitado a 0–70. Em 0, o livro aberto é visto de cima, plano; um ângulo maior aproxima o pé das páginas e mostra a espessura do miolo.
yawnumber0Quanto a vista gira em volta do livro, em graus, trazida para o intervalo −180–180. Em 0, o livro é visto a partir do pé das páginas; um ângulo positivo leva o olho para a direita do livro, um negativo para a esquerda. Junto com tilt, é a vista com que o visualizador abre e a que resetView() volta suavemente.
paper.typeFolioPaperType'uncoated'; 'newsprint' num formato de jornalO tipo de papel. Ele fornece os valores padrão dos cinco campos abaixo (veja a tabela de papéis). cardStock é cartão de capa; board é papelão rígido, como num livro cartonado infantil, e suas folhas viram sem se curvar.
paper.grammagenumbera do papelGramatura em gramas por metro quadrado, 20–2500. Um papel mais pesado é mais grosso, mais rígido e mais opaco: a folha se curva num arco mais largo e deixa ver menos do verso.
paper.bulknumbero do papelEspessura por unidade de peso (bulk), em cm³/g, 0,5–3. A espessura de uma folha em micrômetros é gramatura × bulk, e dela e do número de páginas resulta a espessura do miolo.
paper.finishFolioPaperFinish'auto'Não revestido (fibra, sem brilho), ou revestido e calandrado em fosco, acetinado (um brilho suave) ou brilhante. No Folio, uma página brilhante reflete a folha que vira por cima dela. 'auto' usa o do papel.
paper.textureFolioPaperTexture'auto'O relevo da superfície: smooth (calandrado), vellum (um grão fino), wove (a textura uniforme da maioria dos papéis de livro, formada sobre uma tela de arame tecido), laid (vergaturas próximas cruzadas por pontusais mais espaçados, deixados por um rolo bailarino), linen (um entrelaçado em relevo, como tela), felt (marcas irregulares de feltro). 'auto' usa a do papel.
paper.textureStrengthnumber1Quanto a textura aparece sob a luz, 0–2.
paper.shadeColorValueo do papelA cor do papel antes da impressão (branco, natural, creme). As páginas são impressas sobre ela.
paper.showThroughbooleantrueO verso de uma página transparece levemente através do papel fino. Depois do papel bíblia, o papel-jornal é o que mais o mostra: a tinta penetra na folha.
binding.typeFolioBindingType'hardcover'; 'folded' num formato de jornalhardcover: capa dura, com as pastas um pouco maiores que as páginas. paperback: brochura com lombada quadrada colada (fresada e colada), abre menos plana. sewn: capa flexível com cadernos costurados. layflat: abre totalmente plana, sem afundar na medianiz. saddleStitch: folhas dobradas e grampeadas pela dobra, como numa revista ou num livreto; sem lombada plana. folded (desde o postext 1.18): um jornal, folhas dobradas uma vez e encaixadas umas dentro das outras sem nada que as prenda; sem grampos, sem lombada e sem capa rígida, e a primeira página é a capa. Uma sequência :::paper com um shade imprime um caderno, as páginas de economia, digamos, em papel-jornal salmão.
binding.coverFolioCoverSource'case'As capas. 'case' desenha uma capa envolvendo as páginas. 'pages' toma a primeira página do livro como a pasta da frente e a última, quando é um verso, como a pasta de trás: o livro fica fechado até que se vire a capa, as pastas viram rígidas (num grampeado, a capa é uma folha um pouco mais pesada que as páginas e vira como elas) e nenhuma capa é desenhada.
binding.coverMaterialFolioCoverMaterial'auto''auto' é tecido numa capa dura e cartão ('paper') nas outras encadernações.
binding.coverColorColorValueazul-escuro (#2c3e57)A cor do material da capa.
binding.spineImagestringnenhumO id de um recurso bitmap ou SVG impresso na lombada: a lombada como se vê com o livro em pé, cabeça para cima e a capa da frente à direita. Ele é ajustado para cobrir a lombada, centralizado. É ignorado num grampeado e numa encadernação dobrada.
surface.typeFolioSurfaceType'oak'Onde o livro está apoiado. 'none' deixa o fundo da página que o hospeda.
surface.colorColorValuenenhumTinge a superfície; em 'plain', é a cor da superfície.
lighting.environmentFolioEnvironment'studio'O ambiente refletido pelo papel revestido e brilhante, combinado com a luz principal que projeta as sombras.
lighting.intensitynumber1Exposição, 0,25–2.
lighting.shadowsbooleantrueSombras projetadas pela luz principal.

Os papéis e os valores que eles fornecem (FOLIO_PAPER_STOCKS), típicos das fichas técnicas dos fabricantes:

PapelGramaturaBulkEspessuraAcabamentoTexturaTom
uncoated (offset sem madeira)90 g/m²1,25113 µmnão revestidowove#fcfbf8
bookWove (creme, alto bulk)80 g/m²1,6128 µmnão revestidowove#f6efdc
coatedMatte115 g/m²1,0115 µmfoscoliso#fdfdfc
coatedSilk115 g/m²0,9104 µmacetinadoliso#ffffff
coatedGloss115 g/m²0,892 µmbrilhanteliso#ffffff
bible40 g/m²1,144 µmnão revestidovellum#f9f6ee
newsprint48 g/m²1,572 µmnão revestidowove#ebe7dc
cardStock250 g/m²1,2300 µmnão revestidovellum#fbfaf6
board1.250 g/m²1,62.000 µmacetinadoliso#ffffff

Um romance em papel creme para livros, encadernado em brochura, sobre uma escrivaninha de nogueira sob uma luminária de leitura:

folio: {
  paper: { type: 'bookWove' },
  binding: { type: 'paperback', coverColor: { hex: '#8a2b1f', model: 'hex' } },
  surface: { type: 'walnut' },
  lighting: { environment: 'lamp' },
}

Uma página num formato de jornal (page.sizePreset 'broadsheet', 'berliner', 'tabloid' ou 'compact') é mostrada como jornal quando a configuração não nomeia papel nem encadernação: papel newsprint e encadernação folded (desde o postext 1.18). Um papel ou uma encadernação que a configuração nomeie é mantido, de modo que paper: { type: 'uncoated' } imprime um tabloide em papel offset. Os campos de papel definidos sem tipo de papel (uma grammage, um shade) se aplicam ao papel-jornal. O resolvedor e o redutor recebem o formato como segundo argumento, e folioForTrim(folio, sizePreset) grava os dois valores padrão na configuração:

resolveFolioConfig({ tilt: 30 }, 'tabloid');
// => { tilt: 30, paper: { type: 'newsprint', grammage: 48, bulk: 1.5, … }, binding: { type: 'folded', coverMaterial: 'paper', … }, … }
 
stripFolioDefaults({ paper: { type: 'newsprint' }, binding: { type: 'folded' } }, 'tabloid');
// => undefined

As cores seguem os vínculos de paleta como qualquer outra cor da configuração (paletteId). O resolvedor e o redutor seguem o padrão das outras seções; o redutor descarta os valores de papel iguais aos do tipo de papel escolhido:

import { FOLIO_PAPER_STOCKS, DEFAULT_FOLIO_CONFIG, resolveFolioConfig, stripFolioDefaults } from 'postext';
 
resolveFolioConfig({ paper: { type: 'bible' } }).paper;
// => { type: 'bible', grammage: 40, bulk: 1.1, finish: 'uncoated', texture: 'vellum', textureStrength: 1, shade: { hex: '#f9f6ee', … }, showThrough: true }
 
stripFolioDefaults({ paper: { type: 'bible', grammage: 40 } });
// => { paper: { type: 'bible' } }

No Sandbox, esses ajustes formam o grupo Folio do painel Design (Design → Folio → Visualizador Folio (3D)), e a aba Folio os mostra à medida que você os muda, sem refazer o layout do livro. Veja Um livro em 3D para o próprio visualizador, e Formato do documento › :::paper para uma sequência de páginas em outro papel.

#Depuração

A propriedade debug reúne dois tipos de recursos de autoria: sobreposições visuais que mantêm o texto-fonte e o layout renderizado em sincronia, e um conjunto de avisos que apontam problemas tipográficos ou estruturais no painel Verificações do Sandbox. Nenhum deles afeta a saída exportada.

PropriedadeTipoDescrição
cursorSyncSyncIndicatorConfigCursor espelhado no layout renderizado; veja Sobreposições visuais.
selectionSyncSyncIndicatorConfigSeleção do texto-fonte destacada na página; veja Sobreposições visuais.
looseLineHighlightLooseLineHighlightConfigSobreposição nas linhas justificadas frouxas; veja Sobreposições visuais.
pageNegativeNegativo de alto contraste da página; veja Sobreposições visuais.
warningsWarningsToggleConfigUm booleano por tipo de aviso de autoria mostrado no editor; veja Avisos.

#Sobreposições visuais

PropriedadeTipoPadrãoDescrição
cursorSync.enabledbooleantrueMostra no layout renderizado um cursor que espelha a posição do cursor no texto-fonte.
cursorSync.colorColorValue#2563ebCor desse cursor.
selectionSync.enabledbooleantrueDestaca o trecho renderizado que corresponde à seleção no texto-fonte.
selectionSync.colorColorValue#fde04780Cor do destaque; por padrão, um amarelo translúcido.
looseLineHighlight.enabledbooleanfalsePinta uma sobreposição nas linhas justificadas cujo espaçamento entre palavras passa de threshold vezes a largura normal do espaço.
looseLineHighlight.colorColorValue#ff000040Cor dessa sobreposição.
looseLineHighlight.thresholdnumber3Multiplicador da largura normal do espaço acima do qual uma linha justificada conta como frouxa. O aviso looseLines usa o mesmo limite. Uma linha justificada cujos espaços teriam de esticar além de 3× é composta alinhada à esquerda, por isso, com o valor padrão, a sobreposição e o aviso quase não encontram nada no texto corrido; baixe-o (1,5 ou 2) para ver linhas que estão frouxas mas ainda justificadas.
pageNegative.enabledbooleanfalseRenderiza uma sobreposição em negativo de alto contraste sobre a página, útil para conferir visualmente, de relance, a forma geral de uma página dupla (densidade do texto, equilíbrio das colunas, espaços em branco), sem se distrair com o detalhe dos glifos.

Cada SyncIndicatorConfig é { enabled: boolean; color?: ColorValue }. LooseLineHighlightConfig é { enabled: boolean; color?: ColorValue; threshold?: number }. pageNegative é uma chave mínima { enabled: boolean }.

debug: {
  cursorSync: { enabled: true, color: { hex: '#ff0066', model: 'hex' } },
  selectionSync: { enabled: false, color: { hex: '#fde04780', model: 'hex' } },
  looseLineHighlight: { enabled: true, color: { hex: '#ff000040', model: 'hex' }, threshold: 3 },
  pageNegative: { enabled: true },
}

Essas sobreposições são desenhadas pelo Sandbox sobre sua visualização em canvas. Elas não fazem parte da página: renderPage, a saída HTML e o PDF nunca as pintam.

#Linhas frouxas no seu próprio canvas

O motor exporta o destaque de linhas frouxas como dois auxiliares, para uma página que você mesmo pinta:

import { buildDocument, renderPageToCanvas, drawLooseLines, findLooseLines } from 'postext';
 
const doc = buildDocument(content, config);
const canvas = document.querySelector('canvas')!;
renderPageToCanvas(doc.pages[0], doc, canvas, { scale: 0.5 });
drawLooseLines(canvas.getContext('2d')!, doc.pages[0], doc, { threshold: 2.5 });
 
// As mesmas linhas como dados: um relatório, uma sobreposição SVG, uma contagem por página.
for (const { ratio, line, block } of findLooseLines(doc, { threshold: 2.5 })) {
  console.log(`page ${block.pageIndex + 1}: ${ratio.toFixed(2)}× — ${line.text}`);
}
  • findLooseLines(doc, { threshold?, pageIndex? }) devolve, em ordem de leitura, cada linha justificada cujo justifiedSpaceRatio passa de threshold: { block, line, ratio, x, y, width, height }. O retângulo, em pixels da página, é a faixa que o destaque cobre: a largura inteira do bloco ao longo da linha. São as linhas que o Sandbox destaca e relata como looseLine no painel Verificações.
  • drawLooseLines(ctx, page, doc, { threshold?, color? }) preenche essas faixas em uma página e devolve as linhas que pintou. Ele desenha em pixels da página sob a transformação atual do contexto, então chame-o logo depois de renderPage ou renderPageToCanvas no mesmo canvas: os dois deixam o contexto na escala da página. color é qualquer estilo de preenchimento de canvas.
  • Valores padrão. Os dois auxiliares usam o limiar padrão (3) e a cor padrão (#ff000040), não o debug.looseLineHighlight do documento: essa configuração pertence ao Sandbox. Para seguir uma configuração, passe resolveDebugConfig(config.debug).looseLineHighlight.threshold e .color.hex.

#Avisos

debug.warnings controla quais problemas de autoria aparecem no painel Verificações do Sandbox (editado em Design → Avançado → Avisos). Cada chave é uma opção booleana independente; defina uma como false para silenciar aquele aviso específico sem desativar os outros.

Essas opções filtram apenas o painel do Sandbox. Os avisos que o próprio motor registra (boxes que transbordam a coluna, em doc.warnings; ids de recurso, diretivas, inserções e ids de estilo desconhecidos e grades de tabela irregulares, em doc.contentWarnings) estão lá seja qual for o valor das opções, e os renderizadores relatam as imagens que pintam como marcadores de posição; veja Avisos no documento.

interface WarningsToggleConfig {
  missingFont?: boolean;
  looseLines?: boolean;
  headingHierarchy?: boolean;
  consecutiveHeadings?: boolean;
  listAfterHeading?: boolean;
  designIssues?: boolean;
}
PropriedadeTipoPadrãoDescrição
missingFontbooleantrueAvisa quando uma fonte citada pela configuração não carregou no navegador. Pega cedo erros de digitação em fontFamily e pacotes @fontsource/... ausentes, antes que apareçam no resultado como substituições silenciosas por uma fonte alternativa.
looseLinesbooleantrueAvisa sobre linhas justificadas cujo espaçamento entre palavras passa de debug.looseLineHighlight.threshold. Trabalha junto com a sobreposição: o aviso as enumera no painel, a sobreposição as mostra no lugar.
headingHierarchybooleantrueAvisa sobre níveis de título que pulam um grau, por exemplo um H1 seguido diretamente de um H3. Saltos na estrutura de títulos costumam indicar um erro de digitação na profundidade do título ou um mal-entendido sobre a estrutura do documento.
consecutiveHeadingsbooleanfalseAvisa quando um título é seguido imediatamente por outro, sem parágrafo nem lista entre eles. Vem desativado porque títulos empilhados são legítimos em muitos modelos (título e subtítulo, capítulo e epígrafe); ative-o em originais em que cada título deve introduzir texto corrido.
listAfterHeadingbooleanfalseAvisa quando uma lista começa logo depois de um título, sem parágrafo introdutório. Vem desativado porque obras de referência fazem isso o tempo todo; ative-o em textos narrativos em que toda lista deve ser apresentada por um parágrafo.
designIssuesbooleantrueAvisa sobre problemas de integridade nos slots de design: cabeçalhos e rodapés de página, a abertura de parte e o verso em branco que a segue, as linhas de parte do sumário, os slots de design avançado dos títulos, e o design e os cabeços de seção de cada estilo de título. Cobre cadeias de âncoras circulares e referências de âncora pendentes (um elemento ancorado a um #id que já não existe), um título de página inteira com breakBefore desativado, e um design avançado ativado cujos elementos nunca mostram .
debug: {
  warnings: {
    missingFont: true,
    looseLines: true,
    headingHierarchy: true,
    consecutiveHeadings: true,
    listAfterHeading: false,
    designIssues: true,
  },
}

Além desses, o painel sempre lista os avisos que a própria diagramação gera (VDTDocument.warnings), como um boxe que transborda a coluna (calloutOverflow), e os valores de configuração que o motor substituiu (VDTDocument.configWarnings, ou collectConfigWarnings(config); veja Avisos de configuração abaixo).

#Avisos de configuração

Sete erros na própria configuração nunca passam em silêncio, e nenhuma opção os oculta. O motor não falha em nenhum deles; ele substitui o valor, ou ignora a configuração, e avisa:

  • Formato de numeração desconhecido: um numberFormat de lista numerada, um page.pageNumbering.format ou o counterFormat de um tipo de recurso que não é nenhuma das grafias de formato de numeração. A numeração sai em decimal.
  • Pilha de fontes em uma família: um fontFamily (ou qualquer …FontFamily) que contém uma pilha de fontes CSS. O texto é composto na primeira família da pilha (veja Uma família por fontFamily).
  • Coluna lateral sem espaço: um sideColumnPercent de um layout 'oneAndHalf' (o do documento, ou o do layout próprio de um estilo de título) que deixaria uma das colunas com menos de 1% da largura do conteúdo, ou que não é um número. As colunas são cortadas no valor mais próximo que as duas aceitam, e used informa qual é (sideColumnPercentClamped; veja o layout 'oneAndHalf').
  • Número de colunas fora do intervalo: um columnCount de um layout 'multiple' (o do documento, ou o do layout próprio de um estilo de título) que não é um número inteiro de 3 a 8. A página é dividida no número válido mais próximo (3 para um valor que não é número), e used informa qual é (columnCountClamped; veja o layout 'multiple').
  • Grade de caracteres grande demais: uma cjk.grid com mais caracteres por linha ou linhas por página do que as margens permitem. A grade é montada com o máximo que cabe, e used informa esse número (cjkGridClamped; veja Grade de caracteres).
  • Configuração de título desconhecida: uma chave que headings, headings.balancing, um nível de título, um estilo de título ou um estilo de parágrafo não tem: um letterSpacng com erro de digitação, um tracking emprestado de outra ferramenta, um level em um estilo de título, um fontStyle: 'italic' em um estilo de parágrafo (que usa italic: true). O motor a ignora (até o postext 1.4, fazia isso sem dizer nada). value é a chave, used fica vazio e suggestion indica a configuração mais próxima, quando há uma a uma ou duas letras de distância ou que difere só em maiúsculas e minúsculas (unknownConfigKey).
  • Valor de configuração desconhecido: uma configuração que aceita uma entre poucas palavras contendo outra, como direction: 'right' (ela aceita auto, ltr ou rtl). O motor lê o valor padrão no lugar, e used informa no que ele resultou: para direction, a direção do idioma do documento (unknownConfigValue). As palavras das configurações de quadrinhos também são verificadas (Quadrinhos › Avisos de quadrinhos): used é o valor em que a configuração se resolveu (o padrão próprio de um estilo de balão, no caso de um estilo embutido), e suggestion indica a palavra mais próxima do valor, quando há uma próxima.

O Sandbox os lista no painel Verificações com o caminho da configuração. No código, buildDocument os coloca no documento como configWarnings (ausente quando a configuração está limpa), e collectConfigWarnings(config) os devolve sem diagramar nada:

import { buildDocument, collectConfigWarnings } from 'postext';
 
// JavaScript puro: em TypeScript, 'roman' nem passa na verificação de tipos.
const config = { bodyText: { fontFamily: 'EB Garamond, serif' }, orderedLists: { numberFormat: 'roman' } };
const doc = buildDocument({ markdown }, config);
doc.configWarnings;
// [{ kind: 'fontFamilyStack', path: 'bodyText.fontFamily', value: 'EB Garamond, serif', used: 'EB Garamond' },
//  { kind: 'unknownNumberFormat', path: 'orderedLists.numberFormat', value: 'roman', used: 'arabic' }]
collectConfigWarnings(config); // a mesma lista

Toda configuração parcial aninhada também é verificada: estilos de título, as listas dentro das partes, htmlViewer.overrides, elementos de design.

formatWarning (veja Avisos no documento) também os descreve, com o caminho da configuração na frente (bodyText.fontFamily: font stack "EB Garamond, serif" — set in "EB Garamond", headingStyles[0].letterSpacng: unknown setting "letterSpacng" — ignored (did you mean "letterSpacing"?)), de modo que um host pode registrar em um único laço as três listas que uma compilação devolve.

#Uso programático

Caminho recomendado: use o Web Worker. No navegador, a grande maioria das integrações deve conduzir o pipeline de layout por createLayoutWorker() de postext/worker, não chamando buildDocument diretamente na thread principal. O worker mantém a interface responsiva durante as compilações, guarda em cache as medidas de texto entre recompilações incrementais e já vem com cancelamento do tipo “a última vence”, de modo que uma nova tecla interrompe qualquer compilação obsoleta em andamento. Vá direto para Executar o layout em um Web Worker para ver a receita canônica. Todo o resto desta seção (buildDocument direto, resolvedores, removedores, caches) continua útil, já que o worker expõe exatamente as mesmas entradas e saídas, mas para código de interface o ponto de partida correto é o wrapper do worker. Só recorra a buildDocument na thread principal para exportações avulsas, renderização no servidor (Node) ou testes.

#Compilar um documento

A função buildDocument executa o pipeline de layout completo e devolve uma Árvore de Documento Virtual (VDT) com coordenadas precisas para cada elemento. É o ponto de entrada de mais baixo nível; código de interface deve preferir o wrapper do Web Worker, que chama buildDocument em uma thread de worker dedicada, com os mesmos argumentos.

import { buildDocument } from 'postext';
 
const content = {
  markdown: '# Chapter One\n\nThe story begins here...',
};
 
const config = {
  page: { sizePreset: '17x24' },
  layout: { layoutType: 'double' },
  bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 9, unit: 'pt' } }, // 9 pt substitui o padrão de 8 pt
};
 
// Compila o layout: produz uma VDT com uma entrada por página em `vdt.pages`
const vdt = buildDocument(content, config);
console.log(`Document has ${vdt.pages.length} pages`);

#Avisos no documento

buildDocument não para diante de uma referência errada ou de um estilo desconhecido: ele aplica uma alternativa e registra o que fez em doc.contentWarnings. Os boxes que a diagramação teve de forçar ficam em doc.warnings, que mantém o formato que tinha no postext 1.4: cada entrada ali é um calloutOverflow com seu pageIndex, columnIndex e overflowPx. Cada campo fica ausente quando não há nada a relatar. Toda entrada tem um kind. Os tipos de conteúdo trazem o intervalo de origem da construção (sourceStart / sourceEnd, deslocamentos no markdown que você passou, frontmatter incluído) e, quando a construção caiu em uma página, seu pageIndex.

TipoGerado quandoO que o resultado faz
calloutOverflowUm boxe :::callout não cabe em nenhuma coluna e nenhum corte consegue dividi-lo.Ele é colocado mesmo assim, transbordando a coluna em overflowPx (em pageIndex / columnIndex). É o único tipo listado em doc.warnings; os tipos abaixo ficam em doc.contentWarnings.
unknownResourceIdUma inserção ::resource (usage: 'embed'), uma :ref em linha ('ref') ou a imagem de uma célula de tabela ('cellImage') cita um id que nenhum recurso tem.A inserção é omitida, a referência imprime ? (ou seu rótulo text=) sem número nem link, a célula fica só com texto. inResource indica o recurso cuja legenda, nota ou célula contém a referência.
unknownDirectiveUma linha :::name cujo nome não é nem diretiva nem contêiner.A linha é composta como texto.
malformedEmbedUma linha ::name que não é uma inserção bem formada e isolada: ::resource com um id sem aspas ou entre aspas simples, ou com outro atributo, ou uma linha colada sob um parágrafo sem linha em branco.A linha é composta como texto.
fullwidthMarkupUma linha contém marcação digitada com um método de entrada chinês ou japonês: uma cerca :::, um título #, um marcador de nota [^…], atributos {…} depois de uma cerca ou de um título, ou negrito **…**. typed é a marcação como foi escrita, ascii a forma que deve ser digitada. Um por linha.A linha é composta como texto; nada é convertido.
attributeKeyInvalidUma chave de atributo contém letras fora do ASCII (作者=曹雪芹); aponta para a chave.O atributo é ignorado.
unknownParagraphStyle:::paragraphsstyle cita um estilo de parágrafo que não existe.Os parágrafos são compostos como texto corrido.
unknownCalloutType:::callouttype não cita nenhum dos calloutStyles; só é gerado quando há algum configurado.O boxe recebe o primeiro estilo de boxe.
unknownChipStyle:chip[…]style cita um estilo de chip que não existe.O chip recebe o primeiro estilo de chip.
undefinedFootnoteUm marcador de nota [^id] que nenhum parágrafo [^id]: define (id é o da nota).O número é impresso; a nota fica vazia.
unusedFootnoteUma definição de nota [^id]: que nenhum marcador cita.A nota não é composta.
indexMarkInvalidUma marca de índice sem termo: :index, ou atributos sem term em uma marca sem texto entre colchetes.A marca não indexa nada.
indexSeeUnknownUm destino de see ou seealso (target) que não é nenhuma entrada do seu índice (index, '' para o principal). Aponta para a linha :::index.A referência cruzada é impressa mesmo assim.
indexRangeUnclosedUma marca range="start" sem o range="end" correspondente, ou o contrário (missing diz qual extremo falta; term indica a entrada). Aponta para a linha :::index.O intervalo imprime sua única página.
unknownHeadingStyleO style="…" de um título cita um estilo de título que não existe (level é o do título).O título e sua seção mantêm as configurações do próprio nível.
unknownTableStyleO table.styleId de um recurso de tabela cita uma entrada que não existe em tableStyles.A tabela é composta em tableStyle.
raggedTableGridA grade de uma tabela não é retangular depois de contadas as mesclagens (veja Montar modelos de tabela).Células se deslocam sobre uma mesclagem ou deixam um buraco. reason ('spanOverlap' / 'missingCells'), row e col localizam o primeiro problema; count diz quantos são.

Os avisos sobre um recurso (seu estilo de tabela, sua grade, uma referência dentro da legenda, da nota ou das células) apontam para a primeira inserção ou referência do recurso no texto, e cada um aparece uma vez por recurso. Só são verificados os recursos que o documento usa: um capítulo de um livro relata as tabelas que cita, não todas as tabelas do livro.

import { buildDocument, formatWarning } from 'postext';
 
const doc = buildDocument({ markdown: 'See :ref{id="fig-map"}.\n\n:::sidebar\nNotes.' }, config);
for (const w of [...(doc.warnings ?? []), ...(doc.contentWarnings ?? []), ...(doc.configWarnings ?? [])]) console.warn(formatWarning(w));
// Unknown resource id "fig-map" in :ref — it prints "?" (or its text= label), with no number or link (page 1, offset 4)
// Unknown directive ":::sidebar" — the line is set as text (page 1, offset 25)
 
// Filtre por `kind` para ler os campos de um tipo.
const missing = (doc.contentWarnings ?? []).flatMap((w) => (w.kind === 'unknownResourceId' ? [w.resourceId] : []));

formatWarning(w) devolve uma descrição de uma linha em inglês. Um host que traduz suas mensagens escolhe a mensagem pelo kind (e mantém um ramo padrão, já que versões menores podem acrescentar tipos). collectContentWarnings(markdown, config, resources) devolve os avisos de conteúdo sem diagramar nada (a lista que a compilação acrescenta, sem pageIndex), para um editor que verifica o texto enquanto ele é digitado. collectHeadingDesignCuts(doc) examina uma diagramação pronta em busca de designs de título cujo texto passa do pé da página ou da coluna (kind: 'headingDesignCut'; veja Altura reservada), algo que a própria diagramação não relata, e formatWarning também descreve esses resultados. O painel Verificações do Sandbox lista todos eles.

Os renderizadores relatam o que não conseguem pintar como foi pedido por meio de uma opção onWarning: renderPageToCanvas, renderPage e renderToCanvas (RenderPageOptions), renderToHtml e renderToHtmlIndexed (RenderHtmlOptions), e renderToPdf (RenderToPdfOptions, também pelo worker de PDF). Hoje há um único tipo de renderização, missingImage: uma imagem (uma figura, a imagem de uma célula de tabela, um ícone de boxe, uma imagem de design) sem nada para desenhar é pintada como um marcador de posição neutro e relatada, uma vez por fileId e por chamada de renderização, com seu pageIndex, o resourceId quando o pintor o conhece (figuras e imagens de célula) e, no PDF, o documentIndex de uma renderização de vários documentos. Nada para desenhar significa nenhum registerResourceImage para o fileId no canvas, nenhuma URL vinda de resourceImageUrl no HTML, e nenhum byte vindo de resourceBytes (ou bytes que não se decodificam) no PDF. Um recurso bitmap ou SVG que não cita nenhum fileId não tem o que pedir: ele é desenhado como marcador de posição sem relato. Os avisos de renderização não ficam guardados na VDT: o que um host pode fornecer muda depois da diagramação.

import { buildDocument, renderPage, type RenderWarning, type Resource } from 'postext';
 
const map: Resource = {
  id: 'fig-map', typeId: 'figure', kind: 'bitmap', caption: 'The route.', createdAt: 0, updatedAt: 0,
  bitmap: { fileId: 'map-file', format: 'png', width: 1200, height: 800 },
};
const doc = buildDocument({ markdown: 'See :ref{id="fig-map"}.', resources: [map] }, config);
 
const warnings: RenderWarning[] = [];
const canvas = renderPage(doc.pages[0], doc, { onWarning: (w) => warnings.push(w) });
// Até 'map-file' ser registrado com registerResourceImage:
// [{ kind: 'missingImage', fileId: 'map-file', resourceId: 'fig-map', pageIndex: 0 }]

#Renderizar uma página como bitmap

Cada página pode ser rasterizada de forma independente. Use renderPage(page, doc) para obter um HTMLCanvasElement de uma página: o canvas é um bitmap com o tamanho exato da página em pixels (na resolução configurada), então você pode exibi-lo, exportá-lo ou mandá-lo para qualquer pipeline de imagem:

import { buildDocument, renderPage } from 'postext';
 
const vdt = buildDocument(content, config);
 
// Renderiza a página 3 (índice a partir de zero) em um canvas bitmap
const pageNumber = 2;
const page = vdt.pages[pageNumber];
if (!page) throw new Error(`Page ${pageNumber} does not exist`);
 
const canvas = renderPage(page, vdt);
// canvas.width / canvas.height são o tamanho do bitmap da página em pixels
 
// Exibe no DOM
document.body.appendChild(canvas);
 
// …ou exporta como data URL PNG
const pngDataUrl = canvas.toDataURL('image/png');
 
// …ou obtém um Blob para baixar / enviar
canvas.toBlob((blob) => {
  if (blob) saveAs(blob, `page-${pageNumber + 1}.png`);
}, 'image/png');
 
// …ou pega os pixels RGBA brutos
const ctx = canvas.getContext('2d')!;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

Se você prefere desenhar em um canvas que já tem (por exemplo, um preso ao DOM com um layout específico), use renderPageToCanvas(page, doc, canvas): ele redimensiona e pinta o canvas que você passa, em vez de criar um novo.

Para renderizar todas as páginas, percorra vdt.pages:

const bitmaps = vdt.pages.map((page) => renderPage(page, vdt));

Exemplo ao vivo: uma página como imagem

Tudo o que foi visto acima, rodando no navegador. O pen importa a versão mais recente do postext de um CDN, espera as fontes web, diagrama um documento curto em duas colunas, pinta a primeira página em um canvas e oferece esse bitmap como PNG. Clique em Executar no CodePen para carregar o editor e mudar o markdown ou a configuração; a página é repintada a cada edição.

Postext · renderizar uma página como imagem
import { buildDocument, renderPage } from 'https://esm.sh/postext';
 
const markdown = `# The Lantern
 
The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.
 
## Two columns
 
Postext lays this text out in **two columns**, breaking each paragraph with the *Knuth–Plass* algorithm and hyphenating with TeX patterns. Widows and orphans are avoided, and the columns are balanced on the last page.
 
The light it gave was small, but it was enough to find the step. The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.
 
The light it gave was small, but it was enough to find the step. The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.`;
 
const config = {
  // 150 dpi: crisp enough for a preview, light enough to paint instantly.
  page: { sizePreset: '17x24', dpi: 150 },
  layout: { layoutType: 'double' },
  bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 10, unit: 'pt' } },
};
 
// Postext measures text with the fonts the browser has loaded,
// so wait for every face the document uses before laying it out.
await Promise.all([
  document.fonts.load('16px "EB Garamond"'),
  document.fonts.load('bold 16px "EB Garamond"'),
  document.fonts.load('italic 16px "EB Garamond"'),
  document.fonts.load('bold 16px "Open Sans"'), // the default heading face
]);
 
// The whole layout: one entry per page in doc.pages, with exact coordinates.
const doc = buildDocument({ markdown }, config);
 
// Rasterise the first page. The canvas is sized to the page at the configured dpi.
const canvas = renderPage(doc.pages[0], doc);
document.getElementById('page').replaceChildren(canvas);
document.getElementById('status').textContent =
  `${doc.pages.length} page(s) · page 1 is ${canvas.width} × ${canvas.height} px`;
 
// The same bitmap as a PNG file.
canvas.toBlob((blob) => {
  const link = document.getElementById('download');
  link.href = URL.createObjectURL(blob);
  link.hidden = false;
}, 'image/png');
index.html
<p id="status">Laying out…</p>
<a id="download" download="page-1.png" hidden>Download page 1 as PNG</a>
<div id="page"></div>
style.css
body {
  margin: 16px;
  font-family: system-ui, sans-serif;
  background: #e8e8e8;
}
#page canvas {
  display: block;
  max-width: 100%;
  height: auto;
  margin-top: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

Carrega um editor interativo de codepen.io. O exemplo importa a versão mais recente do postext de uma CDN.

#React

postext/react exporta createLayout(content, config?): um componente que diagrama o documento uma vez, ao ser montado, e mostra cada página como um <canvas> dentro de uma <div>.

import { createLayout } from 'postext/react';
 
const Article = createLayout(
  { markdown: '# Hello\n\nThe first paragraph of the article.' },
  { page: { sizePreset: '17x24' } },
);
 
export function ArticlePage() {
  return <Article className="pages" style={{ maxWidth: 480 }} />;
}
  • Thread principal, uma vez. As páginas são pintadas na resolução do documento e escaladas para a largura do contêiner. content e config ficam fixos quando você chama createLayout; crie outro componente para mostrar outra coisa. Para uma visualização ao vivo, compile no Web Worker e pinte com renderPageToCanvas, como no exemplo de React daquela seção.
  • Primeiro as fontes e as imagens. Carregue as fontes web do documento antes de o componente ser montado e registre as imagens com registerResourceImage. Quando o markdown contém um $, o próprio componente inicia o motor de matemática.
  • O React fica fora da entrada principal. O postext nunca importa o React; só o postext/react importa. createLayout continua exportado pelo postext para que o código existente siga funcionando, mas está obsoleto: ele carrega o postext/react quando você o chama, e o componente fica suspenso até isso chegar (o React o renderiza de novo sozinho). Importe-o de postext/react.
  • O componente obsoleto suspende. Até o postext/react chegar, o createLayout do postext precisa de uma raiz concorrente (createRoot) ou de uma fronteira <Suspense> acima dele. Em uma raiz legada ReactDOM.render, ou em renderToString, sem fronteira, o React relata um erro. react continua sendo uma dependência peer obrigatória, para que os bundlers consigam resolver essa importação tardia.

#Resolver valores padrão

As funções resolvedoras preenchem os valores padrão de objetos de configuração parciais. Isso é útil quando você precisa de uma configuração completa para inspecionar ou comparar:

import { resolvePageConfig, resolveBodyTextConfig } from 'postext';
 
const fullPage = resolvePageConfig({ sizePreset: '21x28' });
// => { sizePreset: '21x28', width: { value: 21, unit: 'cm' }, height: { value: 28, unit: 'cm' },
//      margins: { top: { value: 2, unit: 'cm' }, ... }, dpi: 300, cutLines: { enabled: false, ... }, ... }
 
const fullBody = resolveBodyTextConfig({ fontFamily: 'Inter' });
// => { fontFamily: 'Inter', fontSize: { value: 8, unit: 'pt' }, lineHeight: { value: 1.5, unit: 'em' }, ... }

Resolvedores disponíveis, um por seção de primeiro nível: resolvePageConfig, resolveLayoutConfig, resolveBodyTextConfig, resolveHeadingsConfig, resolveHeadingStylesConfig, resolveTocConfig, resolvePartsConfig, resolveUnorderedListsConfig, resolveOrderedListsConfig, resolveMathConfig, resolveTableStyleConfig, resolveCaptionStyleConfig, resolveDiagramStyleConfig, resolveParagraphStylesConfig, resolveCalloutStylesConfig, resolveHeaderFooterConfig, resolveDebugConfig, resolveHtmlViewerConfig, resolvePdfGenerationConfig, além de resolveDesignSlot para um único slot de design. As paletas de cores são aplicadas à parte, com applyPaletteToConfig(config), applyPaletteToResolvedConfig(resolved, palette) e resolveColorValue(value, palette, fallback); veja Paleta de cores.

Os resolvedores cujos valores padrão vêm em cascata de outra seção recebem essa seção, já resolvida, como argumento adicional. resolveUnorderedListsConfig e resolveOrderedListsConfig recebem o texto do corpo resolvido, porque os padrões de fontFamily e color das listas vêm dele; resolveCalloutStylesConfig recebe o texto do corpo, os títulos e as listas não numeradas resolvidos (veja o exemplo em Estilos de boxe), e resolveHeadingStylesConfig recebe a página, o texto do corpo e as duas seções de listas resolvidos. Consulte as declarações de tipo do pacote para ver a assinatura exata de cada um:

import { resolveBodyTextConfig, resolveUnorderedListsConfig } from 'postext';
 
const body = resolveBodyTextConfig({ fontFamily: 'Inter' });
const lists = resolveUnorderedListsConfig({ bulletChar: '—' }, body);
// => lists.fontFamily === 'Inter' (herdado)

Os conjuntos estáticos de valores padrão (os valores usados quando não há cascata) também são exportados: DEFAULT_PAGE_CONFIG, DEFAULT_CUT_LINES, DEFAULT_PAGE_NUMBERING, PAGE_SIZE_PRESETS, DEFAULT_LAYOUT_CONFIG, DEFAULT_COLUMN_RULE, DEFAULT_COLUMN_BALANCING, DEFAULT_BODY_TEXT_CONFIG, DEFAULT_HYPHENATION_CONFIG, DEFAULT_HEADINGS_CONFIG, DEFAULT_UNORDERED_LISTS_STATIC, DEFAULT_ORDERED_LISTS_STATIC, DEFAULT_PARAGRAPH_STYLES, DEFAULT_CALLOUT_STYLES, DEFAULT_CALLOUT_STYLE_STATIC, DEFAULT_PARTS_CONFIG, DEFAULT_HEADING_STYLES, DEFAULT_TOC_CONFIG, DEFAULT_MATH_CONFIG, DEFAULT_DIAGRAM_STYLE_CONFIG, DEFAULT_DEBUG_CONFIG, DEFAULT_HTML_VIEWER_CONFIG, DEFAULT_PDF_GENERATION_CONFIG, DEFAULT_COLOR_PALETTE, DEFAULT_MAIN_COLOR, DEFAULT_MAIN_COLOR_ID, DEFAULT_MAIN_COLOR_NAME, DEFAULT_MAIN_COLOR_HEX, além dos padrões dos elementos de cabeçalho e rodapé (DEFAULT_HEADER_FOOTER_SLOT, DEFAULT_HEADER_SLOT, DEFAULT_FOOTER_SLOT, DEFAULT_TEXT_ELEMENT, DEFAULT_RULE_ELEMENT, DEFAULT_BOX_ELEMENT) e de defaultResourceTypes(locale), que depende do idioma (veja Tipos de recurso).

#Remover valores padrão

Ao persistir a configuração (por exemplo, em localStorage ou em um arquivo), use stripConfigDefaults para remover os valores iguais aos padrões. Assim as configurações guardadas ficam mínimas, só com as alterações intencionais:

import { stripConfigDefaults } from 'postext';
 
const minimal = stripConfigDefaults(fullConfig);
// Só restam as propriedades que diferem dos padrões

Também há removedores individuais, um por resolvedor: stripPageDefaults, stripLayoutDefaults, stripBodyTextDefaults, stripHeadingsDefaults, stripHeadingStylesDefaults, stripTocDefaults, stripPartsDefaults, stripUnorderedListsDefaults, stripOrderedListsDefaults, stripMathDefaults, stripTableStyleDefaults, stripCaptionStyleDefaults, stripDiagramStyleDefaults, stripParagraphStylesDefaults, stripCalloutStylesDefaults, stripHeaderFooterDefaults, stripDesignSlotDefaults, stripDebugDefaults, stripHtmlViewerDefaults, stripPdfGenerationDefaults.

#Análise do markdown

O motor expõe seu tokenizador de markdown e seu leitor de frontmatter. Use-os para inspecionar um documento antes de compilá-lo, ou para alimentar outras ferramentas com a mesma estrutura de blocos que o Postext enxerga:

import { parseMarkdown, extractFrontmatter } from 'postext';
 
const source = '---\ntitle: Chapter One\n---\n\n# Opening\n\nThe story begins here.';
 
const { metadata, content } = extractFrontmatter(source);
// metadata.title === 'Chapter One'
 
const blocks = parseMarkdown(content);
// => [ { type: 'heading', level: 1, text: 'Opening', … },
//      { type: 'paragraph', text: 'The story begins here.', … } ]

Veja a página Formato do documento para a lista completa das construções de markdown que o Postext reconhece.

#Cache de medidas

A medição de texto é a etapa cara do layout. Dois tipos de cache a mantêm barata, e eles são limpos de maneiras diferentes:

  • Um cache de blocos que é seu. createMeasurementCache() devolve um MeasurementCache que guarda cada parágrafo medido, com chave formada por texto, fontes, largura, opções de quebra de linha e dicionário de hifenização ativo. Passe-o como terceiro argumento de buildDocument (ou de buildDocumentAsync) para reaproveitar as medidas entre as passadas de convergência e entre compilações: um editor que diagrama o documento a cada tecla passa a medir só os parágrafos que mudaram. Sem ele, cada passada mede todos os blocos de novo. Um parágrafo lido do cache é idêntico a um medido do zero, então uma compilação com cache compõe cada linha como uma compilação sem cache; no postext 1.4.1, um parágrafo vindo do cache perdia a marca de última linha curta, e o aperto dessas linhas e o balanceamento de colunas podiam então quebrá-lo de outro jeito. O worker de layout mantém um cache entre compilações e o substitui quando as fontes mudam.
  • Caches globais de larguras. As larguras das palavras ficam em cache por string de fonte, em estado de módulo compartilhado por todas as compilações da página, e o pretext mantém um cache próprio. Esses caches ficam desatualizados quando uma fonte web chega depois que o texto foi medido com uma fonte alternativa.
import { buildDocument, createMeasurementCache, clearMeasurementCache } from 'postext';
import type { MeasurementCache } from 'postext';
 
let cache: MeasurementCache = createMeasurementCache();
let doc = buildDocument(content, config, cache);
 
// Uma fonte web terminou de carregar: as larguras medidas com a fonte alternativa estão desatualizadas.
await document.fonts.ready;
clearMeasurementCache();           // sem argumento: limpa os caches globais de larguras
cache = createMeasurementCache();  // um cache de blocos não tem clear(); crie um novo
doc = buildDocument(content, config, cache);

clearMeasurementCache() não recebe argumentos e não mexe em um MeasurementCache: os blocos dele também foram medidos com as larguras antigas, então descarte-o e crie um novo. Recompilar depois que uma fonte carrega, sem limpar os caches, dá as mesmas quebras de linha, medidas com a fonte alternativa.

Para aplicações que medem texto pedaço por pedaço, cachedMeasureBlock(text, font, maxWidthPx, lineHeightPx, options, cache) e cachedMeasureRichBlock(spans, normalFont, boldFont, italicFont, boldItalicFont, maxWidthPx, lineHeightPx, options, cache) recebem os argumentos de measureBlock e de measureRichBlock mais o cache, por último.

#Estado global compartilhado em uma página

Parte do estado do Postext fica em variáveis de módulo. Tudo o que importa postext no mesmo realm de JavaScript o compartilha: uma página e seus scripts compartilham uma cópia, enquanto cada iframe e cada worker têm a sua. Com um documento por página, isso passa despercebido. Com vários documentos em uma página (duas visualizações ao vivo, uma galeria de exemplos), não:

  • Imagens de recursos. registerResourceImage(fileId, image) preenche um único registro com chave fileId, que renderPage e renderPageToCanvas leem. Dois documentos que registram figure.svg dividem essa entrada: o último registro vale, para os dois. Dê aos ids de arquivo um prefixo por documento e chame unregisterResourceImage(fileId) ou clearResourceImages() quando um documento deixar de existir. Os rasters que o renderizador de canvas guarda em cache usam a mesma chave e são descartados junto com a imagem.
  • Medidas de texto. As larguras medidas ficam em cache por string de fonte e texto, para o realm inteiro. Um texto medido antes de uma fonte web terminar de carregar mantém as larguras da fonte alternativa, para todos os documentos, até que clearMeasurementCache() (sem argumentos) esvazie os caches: chame-a quando as fontes tiverem carregado e compile de novo.
  • Configurações resolvidas. Cada objeto de configuração é resolvido uma vez, e o resultado fica em cache associado a esse objeto. Uma configuração alterada no próprio objeto e compilada de novo é diagramada com as configurações antigas: passe um objeto novo a cada mudança ({ ...config, … } ou structuredClone(config)).
  • Idioma de hifenização. Cada compilação define o idioma de hifenização global do processo como o bodyText.hyphenation.locale do seu documento. As funções exportadas hyphenateText(text) e layoutDesignSlot usam o idioma da última compilação, a menos que você passe um: chame hyphenateText(text, 'es').
  • Motor de matemática. Há um único motor MathJax e um único cache de fórmulas renderizadas para o realm; initMathEngine() o inicia para todos.

O isolamento mais simples é um realm por documento: um iframe por exemplo ao vivo (uma inserção do CodePen é um iframe), ou um worker de layout por documento para as medidas e a hifenização (as imagens continuam sendo registradas na página).

#Executar o layout em um Web Worker

Esta é a forma recomendada de usar o Postext no navegador. Se você está construindo algo interativo (uma visualização ao vivo, um editor, um visualizador que reage ao redimensionamento ou um ambiente de testes no estilo do Sandbox), conduza o pipeline por createLayoutWorker() de postext/worker. Não chame buildDocument diretamente na thread principal em código de interface.

Chamar buildDocument na thread principal executa o pipeline inteiro (análise, medição, sete passadas, até cinco iterações de convergência) na thread que o chamou. Para uma exportação avulsa, tudo bem. Para uma interface interativa, é a thread errada: um layout de 150 ms bloqueia os eventos de entrada, as teclas se acumulam e a rolagem trava. O worker leva cada um desses milissegundos para uma thread em segundo plano.

O Postext traz um ponto de entrada dedicado para Web Worker, postext/worker, que tira o pipeline da thread principal. É o caminho que esperamos que a maioria das integrações use: as áreas de visualização Canvas, HTML e PDF do Sandbox compartilham o mesmo handle createLayoutWorker() por meio de um único hook useLayoutWorker (packages/postext-sandbox/src/worker/useLayoutWorker.ts) e o conduzem com cancelamento do tipo “a última vence”: uma nova tecla interrompe a compilação em andamento antes mesmo de ela terminar.

Em linhas gerais, a integração canônica segue estes passos:

  1. Criar um worker uma vez por área de visualização com createLayoutWorker().
  2. Registrar as fontes uma vez por família, enviando ArrayBuffers transferíveis com registerFonts(payloads).
  3. Compilar com build(content, config, { signal }), passando um AbortSignal novo a cada chamada para que compilações obsoletas possam ser canceladas.
  4. Substituir qualquer compilação anterior abortando o sinal dela antes de iniciar a próxima: esse é o padrão “a última vence”.
  5. Descartar o worker quando o componente que o possui for desmontado.

O mesmo VDTDocument que volta de build(...) alimenta todos os renderizadores seguintes: renderPage/renderPageToCanvas para canvas, renderToHtmlIndexed para HTML e renderToPdf (de postext-pdf) para PDF. Você compila uma vez no worker e rasteriza quantas vezes a interface precisar na thread principal.

#O que o worker oferece

  • A thread principal fica livre. A análise, a medição e o laço de convergência de sete passadas rodam todos dentro do worker. A thread principal só é tocada quando o VDTDocument pronto é enviado de volta.
  • Cancelamento “a última vence”. build(content, config, { signal }) leva um AbortSignal até o worker. Abortar antes do fim gera um AbortError no lado principal; dentro do worker, o pipeline lança um BuildCancelledError no próximo ponto de verificação de cancelamento por bloco e para na hora.
  • Cache de medidas por worker. O worker mantém um único MeasurementCache durante toda a sua vida. As compilações seguintes que compartilham fonte, texto e largura reaproveitam as medidas de linha em cache: digitar um único caractere em um documento longo só volta a medir os blocos cuja entrada de fato mudou.
  • Métricas idênticas às da thread principal. As fontes são enviadas ao worker como ArrayBuffers transferíveis e registradas com new FontFace(...) no FontFaceSet do próprio worker. O worker mede com as mesmas métricas de fonte do canvas que a thread principal usaria, então as quebras de linha e as alturas das colunas são idênticas byte a byte.
  • O cache de rasters de matemática sobrevive às compilações no worker. O renderizador de matemática traz um cache de rasters com chave pelo conteúdo, ao lado do que usa a identidade como chave; sem ele, a clonagem estruturada de um MathRender ao atravessar a fronteira do worker perderia o cache por identidade a cada recompilação.

#API pública

O cliente do worker fica no subcaminho postext/worker e se resume a poucos nomes:

  • createLayoutWorker(opts?): LayoutWorkerHandle: cria um worker dedicado (ou envolve um que você passa em opts.worker, ou inicia a entrada do worker em opts.url) e devolve um handle tipado. Veja Carregar o worker de um CDN.
  • LayoutWorkerHandle.registerFonts(faces: FontPayload[]): Promise<void>: envia os bytes das fontes para o worker. Os buffers são transferidos, então guarde uma cópia nova na thread principal se precisar reenviá-los depois.
  • LayoutWorkerHandle.build(content, config?, { signal? }): Promise<VDTDocument>: executa o pipeline. Abortar o sinal cancela a compilação em andamento.
  • LayoutWorkerHandle.dispose(): void: encerra o worker e rejeita qualquer compilação pendente com AbortError.
  • FontPayload: { family, weight, style, unicodeRange?, buffer: ArrayBuffer }. weight é uma string de peso CSS ('700', 'bold'); registerFonts também o aceita como número (700). O buffer é transferido para o worker quando você chama registerFonts.
  • BuildCancelledError (reexportado de postext): o que buildDocument lança internamente quando options.shouldCancel devolve true. Normalmente você não o vê na thread principal: o protocolo do worker o converte em um AbortError antes que chegue ao seu código.

O pacote também publica um caminho postext/worker/entry que aponta para o script compilado do worker. createLayoutWorker() resolve essa URL automaticamente; você só precisa citá-la explicitamente quando o seu bundler exige uma chamada new Worker(new URL(...), { type: 'module' }) montada à mão, ou quando você mesmo serve a entrada (opts.url).

#Integração mínima

import { createLayoutWorker } from 'postext/worker';
import type { FontPayload, LayoutWorkerHandle } from 'postext/worker';
import type { PostextConfig, VDTDocument } from 'postext';
 
// 1. Crie o worker uma vez e guarde o handle durante toda a vida da sua área de visualização.
const layout: LayoutWorkerHandle = createLayoutWorker();
 
// 2. Registre as fontes uma vez por família (ArrayBuffers transferíveis).
//    getConfigFontFamilies(config) é um auxiliar que lista as famílias que a sua configuração vai renderizar.
const payloads: FontPayload[] = await collectFontPayloadsForFamilies([
  'EB Garamond',
  'Open Sans',
]);
await layout.registerFonts(payloads);
 
// 3. Conduza as compilações com cancelamento "a última vence": aborte o sinal anterior
//    antes de iniciar uma nova compilação. Uma compilação obsoleta é descartada dentro do worker.
let pending: AbortController | null = null;
 
async function rebuild(
  markdown: string,
  config: PostextConfig,
): Promise<VDTDocument | null> {
  pending?.abort();
  pending = new AbortController();
  try {
    return await layout.build({ markdown }, config, { signal: pending.signal });
  } catch (err) {
    if ((err as { name?: string } | null)?.name === 'AbortError') return null;
    throw err;
  }
}
 
// 4. Descarte quando o componente dono do worker for desmontado.
//    As compilações pendentes são rejeitadas com AbortError.
layout.dispose();

Dentro de um componente React, o formato é este:

import { useEffect, useRef } from 'react';
import { createLayoutWorker } from 'postext/worker';
import type { LayoutWorkerHandle } from 'postext/worker';
import { renderPageToCanvas } from 'postext';
import type { PostextConfig } from 'postext';
 
export function CanvasPreview({
  markdown,
  config,
}: {
  markdown: string;
  config: PostextConfig;
}) {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const workerRef = useRef<LayoutWorkerHandle | null>(null);
  const pendingRef = useRef<AbortController | null>(null);
 
  // Montagem: cria o worker e envia as fontes uma vez.
  useEffect(() => {
    const handle = createLayoutWorker();
    workerRef.current = handle;
    (async () => {
      const payloads = await collectFontPayloadsForFamilies(
        getConfigFontFamilies(config),
      );
      await handle.registerFonts(payloads);
    })();
    return () => {
      pendingRef.current?.abort();
      handle.dispose();
    };
  }, []); // fontes registradas uma vez; registre de novo só quando o conjunto de famílias mudar
 
  // A cada tecla ou mudança de configuração: substitui a compilação em andamento e dispara uma nova.
  useEffect(() => {
    const handle = workerRef.current;
    if (!handle) return;
    pendingRef.current?.abort();
    const ac = new AbortController();
    pendingRef.current = ac;
    (async () => {
      try {
        const vdt = await handle.build({ markdown }, config, { signal: ac.signal });
        const canvas = canvasRef.current;
        if (!canvas || !vdt.pages[0]) return;
        renderPageToCanvas(vdt.pages[0], vdt, canvas); // rasteriza na thread principal
      } catch (err) {
        if ((err as { name?: string } | null)?.name !== 'AbortError') throw err;
      }
    })();
  }, [markdown, config]);
 
  return <canvas ref={canvasRef} />;
}

O padrão é sempre o mesmo: criar uma vez, registrar as fontes uma vez, compilar com AbortSignal quantas vezes for preciso, descartar ao desmontar.

#Carregar o worker de um CDN

Um script de worker precisa vir da mesma origem da página, então uma cópia de postext/worker servida por um CDN não consegue iniciar o arquivo layout.worker.js ao lado dela. createLayoutWorker() resolve isso:

  • esm.sh, sem opções. Quando o próprio postext/worker foi carregado do esm.sh (a URL do módulo é algo como https://esm.sh/postext@1.5.0/es2022/worker.mjs), o cliente inicia o https://esm.sh/postext@1.5.0/worker/entry correspondente por meio de um módulo blob de uma linha, da mesma origem, que o importa. O mesmo vale para importações que acrescentam ?deps=, ?external= ou ?alias=, e para a forma https://esm.sh/*postext@1.5.0/worker. O worker sempre recebe o build simples daquela versão, porque um worker não tem import map para resolver dependências externas.
  • Qualquer outro servidor, com url. Outros CDNs, como jsDelivr (/+esm) ou unpkg, não são detectados. createLayoutWorker({ url }) inicia o módulo de entrada do worker em url: diretamente, se a URL for da mesma origem; pelo mesmo wrapper blob, se for de outra origem. Esse servidor precisa permitir requisições de outras origens (CORS).
  • Bundlers não mudam nada. Com Vite, webpack ou Next.js, continue chamando createLayoutWorker() sem opções: eles emitem o worker como um chunk da sua aplicação.
import { createLayoutWorker } from 'https://esm.sh/postext/worker';
 
const layout = createLayoutWorker();
const face = async (weight, style) => ({
  family: 'EB Garamond',
  weight,
  style,
  buffer: await (await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/eb-garamond@5/files/eb-garamond-latin-${weight}-${style}.woff2`)).arrayBuffer(),
});
await layout.registerFonts(await Promise.all([face(400, 'normal'), face(700, 'normal'), face(400, 'italic')]));
const doc = await layout.build({ markdown }, { bodyText: { fontFamily: 'EB Garamond' } });

Um worker não enxerga as fontes da página. Ele tem um conjunto de fontes próprio, só com as faces enviadas por registerFonts e as fontes instaladas no sistema. Quando uma compilação compõe texto em uma família que o worker não encontra, esse texto é medido com uma fonte alternativa, e as quebras de linha não vão coincidir com as da página. O cliente então imprime um aviso no console por família ("EB Garamond" is not available inside the layout worker…) e lista as famílias em BuildStats.missingFonts, que o callback onStats de build recebe.

#Coleta de fontes (Fontsource / Google Fonts)

registerFonts recebe os bytes brutos das fontes. A thread principal é o lugar certo para buscá-los, porque o Google Fonts só devolve WOFF2 para strings de User-Agent de navegador, e porque um cache central permite que várias instâncias de worker compartilhem os mesmos bytes.

O collectFontPayloadsForFamilies do Sandbox (packages/postext-sandbox/src/controls/fontLoader.ts) é uma implementação de referência pronta para usar. Ele:

  1. Consulta https://api.fontsource.org/v1/fonts/{family-id} para descobrir os pesos disponíveis e se a família traz um eixo variável.
  2. Monta uma URL CSS2 do Google Fonts que cobre todos os pesos e estilos que a família anuncia.
  3. Baixa a folha de estilo @font-face gerada, extrai cada declaração src: url(...) format('woff2') e baixa os bytes brutos.
  4. Devolve um FontPayload[] em que buffer é um ArrayBuffer novo a cada chamada, o que importa porque registerFonts transfere o buffer e deixa a cópia de quem envia desanexada.

Combine-o com getConfigFontFamilies(config) para obter a lista de famílias que uma PostextConfig vai de fato renderizar (corpo, títulos, marcadores de lista, números de lista numerada).

#Cancelamento cooperativo dentro do motor

Se você mesmo conduz buildDocument (por exemplo, dentro de um worker personalizado), o pipeline expõe um gancho shouldCancel que você pode usar diretamente:

import { buildDocument, BuildCancelledError } from 'postext';
 
let superseded = false;
try {
  const vdt = buildDocument(content, config, cache, {
    shouldCancel: () => superseded,
  });
} catch (err) {
  if (err instanceof BuildCancelledError) return; // uma compilação mais nova assumiu
  throw err;
}

shouldCancel é chamado uma vez por bloco de primeiro nível durante o posicionamento. O gancho é cooperativo de propósito: não consegue interromper no meio de uma linha a chamada de layout do próprio pretext, mas mantém a granularidade do cancelamento pequena o bastante (milissegundos) para que quem digita rápido nunca espere por uma compilação obsoleta.

#Conduzir a exportação de PDF a partir do worker

O renderizador de PDF recebe um VDTDocument pronto e o transforma em bytes de PDF. Ele não executa o layout de novo. Por isso o fluxo canônico de PDF no navegador combina bem com o worker: compile a VDT no worker (fora da thread principal, cancelável, reaproveitando o cache) e depois chame renderToPdf na thread principal sobre a mesma VDT.

import type { LayoutWorkerHandle } from 'postext/worker';
import { renderToPdf } from 'postext-pdf';
import type { PostextConfig } from 'postext';
import { createPdfFontProvider } from './pdfFontProvider';
 
const fontProvider = createPdfFontProvider();
 
export async function exportPdf(
  layout: LayoutWorkerHandle,
  markdown: string,
  config: PostextConfig,
): Promise<Uint8Array> {
  // 1. Compila a VDT no worker: a interface continua responsiva durante as passadas de layout.
  const vdt = await layout.build({ markdown }, config);
 
  // 2. Rasteriza em PDF na thread principal. renderToPdf é rápido quando a VDT já existe,
  //    porque percorre coordenadas pré-calculadas, sem medir o texto de novo.
  return renderToPdf(vdt, {
    fontProvider,
    // a configuração pdfGeneration em `vdt.config` é respeitada automaticamente.
  });
}

Se você já mantém um handle de worker para a visualização ao vivo, reaproveite-o na exportação em vez de criar um segundo worker: o cache de medidas dentro do worker torna praticamente gratuita uma exportação de PDF feita logo depois de uma visualização na tela.

Em um livro longo, escrever o próprio PDF também leva segundos; o postext-pdf/worker executa essa etapa em um worker próprio (veja Renderizar o PDF em um worker).

#Quando usar o worker e quando não usar

Use o worker para:

  • Visualizações ao vivo, editores e playgrounds. Qualquer caso em que o documento é reconstruído em resposta ao que o usuário digita ou faz.
  • Visualizadores HTML que acompanham o redimensionamento e refazem o layout a cada disparo do ResizeObserver.
  • Exportação de PDF no navegador acionada a partir de uma interface que já tem visualização ao vivo: reaproveite o handle do worker existente para que a exportação use o mesmo cache de medição.
  • Várias abas de saída que precisam do mesmo VDT (as áreas de visualização Canvas / HTML / PDF do Sandbox compartilham um handle de worker por montagem de área de visualização).

Dispense o worker para:

  • Geração no servidor: o Node não tem o FontFaceSet do navegador, e você já controla a thread de qualquer forma.
  • Exportações avulsas e isoladas (uma CLI, um script de exportação headless, uma Cloud Function) em que não existe interface interativa para bloquear. Chamar buildDocument diretamente é mais simples e evita o custo da transferência inicial de fontes.

#Integração do visualizador HTML

O visualizador HTML é o renderizador do Postext voltado para a tela. Em vez de rasterizar as páginas em um bitmap, ele emite nós DOM com posicionamento absoluto, cuja geometria vem do mesmo pipeline que produz a saída impressa. Por isso é a escolha certa quando você quer, no navegador, tipografia legível, selecionável e que acompanha o redimensionamento (um app de leitura, uma visualização dentro de um produto ou uma página de documentação incorporada) sem depender de um visualizador de PDF.

As peças principais da API pública:

  • buildDocument(content, config, cache?): executa o pipeline de layout completo e devolve um VDTDocument.
  • renderToHtmlIndexed(doc, options): transforma o VDT em uma única string HTML, mais um detalhamento por página e por bloco. Esse detalhamento permite remendar o DOM a baixo custo quando só alguns blocos mudaram entre duas renderizações.
  • resolveHtmlViewerConfig(partial): preenche os valores padrão do visualizador HTML (maxCharsPerLine, columnGap, optimalLineBreaking).
  • buildFontString + measureGlyphWidth + dimensionToPx: primitivas de medição usadas para obter a largura real da coluna em pixels a partir de um número de caracteres desejado.
  • createMeasurementCache / clearMeasurementCache: caches plugáveis, para reaproveitar medições entre um layout e outro.

#Exemplo ao vivo: uma string HTML

O percurso completo em JavaScript puro, antes da integração com React mais abaixo: construir o documento, entregar o VDTDocument a renderToHtml e inserir a string em um contêiner. mode: 'single' empilha as páginas na vertical; background dá uma cor a elas, já que as páginas são transparentes por padrão. O pen também imprime a marcação gerada, para você ver as linhas com posicionamento absoluto que o renderizador emite: o navegador as pinta, mas nunca refaz o fluxo delas.

Postext · renderizar um documento em HTML
import { buildDocument, renderToHtml } from 'https://esm.sh/postext';
 
const markdown = `# The Lantern
 
The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.
 
## Two columns
 
Postext lays this text out in **two columns**, breaking each paragraph with the *Knuth–Plass* algorithm and hyphenating with TeX patterns. Widows and orphans are avoided, and the columns are balanced on the last page.
 
The light it gave was small, but it was enough to find the step. The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.
 
The light it gave was small, but it was enough to find the step. The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.`;
 
const config = {
  // 96 dpi: page pixels are CSS pixels, so the HTML shows at its real size.
  page: { sizePreset: '17x24', dpi: 96 },
  layout: { layoutType: 'double' },
  bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 10, unit: 'pt' } },
};
 
// Postext measures text with the fonts the browser has loaded,
// so wait for every face the document uses before laying it out.
await Promise.all([
  document.fonts.load('16px "EB Garamond"'),
  document.fonts.load('bold 16px "EB Garamond"'),
  document.fonts.load('italic 16px "EB Garamond"'),
  document.fonts.load('bold 16px "Open Sans"'), // the default heading face
]);
 
const doc = buildDocument({ markdown }, config);
 
// One HTML string for the whole document. Every line is an absolutely
// positioned element, so the browser never reflows the text.
const html = renderToHtml(doc, { mode: 'single', background: '#ffffff' });
 
document.getElementById('viewer').innerHTML = html;
document.getElementById('source').textContent = html;
document.getElementById('status').textContent =
  `${doc.pages.length} page(s) · ${(html.length / 1024).toFixed(1)} KB of HTML`;
index.html
<p id="status">Laying out…</p>
<div id="viewer"></div>
<details>
  <summary>Generated HTML</summary>
  <pre id="source"></pre>
</details>
style.css
body {
  margin: 16px;
  font-family: system-ui, sans-serif;
  background: #e8e8e8;
}
/* The page is wider than this pane: let it scroll instead of clipping it.
   The renderer centres pages with an inline style, hence the !important. */
#viewer {
  overflow: auto;
}
#viewer .pt-doc {
  align-items: flex-start !important;
}
/* Each page is a .pt-page block; the renderer positions every line inside it. */
#viewer .pt-page {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
details {
  margin-top: 16px;
}
#source {
  max-height: 240px;
  overflow: auto;
  padding: 8px;
  background: #fff;
  font-size: 11px;
  white-space: pre-wrap;
  word-break: break-all;
}

Carrega um editor interativo de codepen.io. O exemplo importa a versão mais recente do postext de uma CDN.

#Integração mínima

O trecho abaixo é a menor integração útil: constrói o documento no tamanho atual da área de visualização, renderiza-o em um contêiner e repete o processo a cada redimensionamento.

import { useEffect, useRef } from 'react';
import {
  buildDocument,
  renderToHtmlIndexed,
  resolveHtmlViewerConfig,
  buildFontString,
  measureGlyphWidth,
  dimensionToPx,
  createMeasurementCache,
} from 'postext';
import type { PostextConfig, MeasurementCache } from 'postext';
 
// DPI adequado para tela: a 144 DPI, um corpo de 8pt equivale a 16 px.
const HTML_DPI = 144;
const PADDING_PX = 24;
 
// Amostra de prosa usada para medir a largura de coluna desejada. Com fontes
// proporcionais, "N × largura média" não é confiável, então medimos uma string representativa.
const SAMPLE =
  'The quick brown fox jumps over the lazy dog. Sphinx of black quartz, judge my vow.';
 
function sampleForChars(n: number): string {
  let s = SAMPLE;
  while (s.length < n) s += ' ' + SAMPLE;
  return s.slice(0, n);
}
 
export function PostextHtmlViewer({
  markdown,
  config,
  mode = 'multi',
}: {
  markdown: string;
  config: PostextConfig;
  mode?: 'single' | 'multi';
}) {
  const hostRef = useRef<HTMLDivElement | null>(null);
  const cacheRef = useRef<MeasurementCache>(createMeasurementCache());
 
  useEffect(() => {
    const host = hostRef.current;
    if (!host) return;
 
    const relayout = () => {
      const rect = host.getBoundingClientRect();
      if (rect.width === 0 || rect.height === 0) return;
 
      const viewer = resolveHtmlViewerConfig(config.htmlViewer);
      const fontFamily = config.bodyText?.fontFamily ?? 'EB Garamond';
      const fontWeight = config.bodyText?.fontWeight ?? 400;
      const fontSize = config.bodyText?.fontSize ?? { value: 8, unit: 'pt' as const };
      const fontSizePx = dimensionToPx(fontSize, HTML_DPI);
 
      // Mede a largura *real* da coluna para N caracteres de texto corrido.
      const targetColumnPx = measureGlyphWidth(
        sampleForChars(viewer.maxCharsPerLine),
        buildFontString(fontFamily, fontSizePx, String(fontWeight), 'normal'),
      );
 
      const inner = Math.max(rect.width - PADDING_PX * 2, 100);
      let columnWidthPx: number;
      if (mode === 'single') {
        columnWidthPx = Math.min(targetColumnPx, inner);
      } else {
        // Encaixa quantas colunas couberem na largura desejada.
        const count = Math.max(
          1,
          Math.floor((inner + viewer.columnGap) / (targetColumnPx + viewer.columnGap)),
        );
        columnWidthPx = (inner - viewer.columnGap * (count - 1)) / count;
      }
      columnWidthPx = Math.max(Math.floor(columnWidthPx), 80);
 
      // O modo single usa uma única página muito alta; o modo multi usa a altura
      // da área de visualização, de modo que cada "página" do VDT vira uma coluna.
      const pageHeightPx =
        mode === 'single' ? Math.max(rect.height * 20, 200_000) : Math.max(rect.height - PADDING_PX * 2, 400);
 
      const override: PostextConfig = {
        ...config,
        page: {
          ...config.page,
          dpi: HTML_DPI,
          width: { value: columnWidthPx, unit: 'px' },
          height: { value: pageHeightPx, unit: 'px' },
          margins: {
            top: { value: 0, unit: 'px' },
            bottom: { value: 0, unit: 'px' },
            left: { value: 0, unit: 'px' },
            right: { value: 0, unit: 'px' },
          },
        },
        layout: { ...config.layout, layoutType: 'single' },
        bodyText: {
          ...config.bodyText,
          optimalLineBreaking: viewer.optimalLineBreaking,
        },
      };
 
      const doc = buildDocument({ markdown }, override, cacheRef.current);
      const { html } = renderToHtmlIndexed(doc, {
        mode,
        columnGap: viewer.columnGap,
        padding: PADDING_PX,
        background: 'transparent',
      });
 
      host.innerHTML = html;
    };
 
    relayout();
 
    const ro = new ResizeObserver(() => relayout());
    ro.observe(host);
 
    // Mede de novo quando as fontes web chegam, para que as larguras dos glifos não venham das fontes substitutas.
    const onFontsDone = () => relayout();
    document.fonts?.addEventListener?.('loadingdone', onFontsDone);
 
    return () => {
      ro.disconnect();
      document.fonts?.removeEventListener?.('loadingdone', onFontsDone);
    };
  }, [markdown, config, mode]);
 
  return <div ref={hostRef} style={{ width: '100%', height: '100%', overflow: 'auto' }} />;
}

Algumas observações sobre o que esse exemplo faz:

  • Mede a coluna em vez de estimá-la. Como maxCharsPerLine é um alvo expresso em caracteres, a largura real em pixels depende da fonte do texto corrido. measureGlyphWidth faz uma medição de verdade com a fonte escolhida, o que mantém a medida constante quando a fonte muda.
  • Reescreve a página. O visualizador HTML trata cada “página” do VDT como uma coluna na tela. O exemplo substitui page.width pela largura de coluna medida, zera as margens (o espaçamento interno fica fora da página, na div .pt-doc que a envolve) e usa HTML_DPI = 144 para que um texto de 8pt resulte em 16px.
  • Leva em conta o carregamento das fontes. document.fonts.loadingdone dispara quando chega uma fonte web recém-solicitada. Sem refazer o layout, a primeira renderização usa as métricas de uma fonte substituta e salta quando a fonte verdadeira chega.
  • Reaproveita o cache de medição. Criar o cache uma vez por componente faz com que redimensionamentos e mudanças de escala da fonte reaproveitem as medições da renderização anterior, em vez de medir de novo cada parágrafo.

#Indo além

O exemplo acima é propositalmente simples. Integrações em produção costumam acrescentar:

  • Isolamento com Shadow DOM: renderize em host.attachShadow({ mode: 'open' }) para que nenhum CSS da página externa vaze para o visualizador.
  • Remendos incrementais: renderToHtmlIndexed devolve pages[i].blocks, cada um com um id estável e o HTML externo do bloco. Quando só alguns blocos diferem entre duas renderizações, você pode substituir esses invólucros no lugar, em vez de reconstruir o innerHTML.
  • Sobreposições: uma camada SVG com posicionamento absoluto sobre cada .pt-page, para cursores, seleções ou a grade de linhas de base.
  • Links: as palavras de um link Markdown são envolvidas em <a href="…" rel="noopener noreferrer">, que assume a cor do texto e não tem sublinhado; veja Formato do documento › Links. Em um visualizador que funcione como editor, intercepte os cliques em a[href] que não comecem por # e abra-os em uma nova aba (as âncoras de :ref apontam para dentro do documento).
  • Imagens em tinta única: com diagramStyle.singleInk ativado, os <img> SVG recebem um filtro CSS, a menos que você passe singleInk: false para URLs que já foram recoloridas; veja Tinta única no canvas e no HTML.

O componente HtmlPreview do Sandbox (packages/postext-sandbox/src/viewport/HtmlPreview/index.tsx) implementa tudo isso sobre a mesma API mostrada aqui e pode servir de referência. Ele também encaminha cada build por um worker de layout compartilhado (veja Executar o layout em um Web Worker), para que edições ao vivo e redimensionamentos nunca bloqueiem a thread principal. Troque a chamada direta buildDocument(...) do trecho acima por layoutWorker.build(...) quando quiser tirar o layout da thread principal.

#Como a saída HTML difere do canvas e do PDF

renderToHtml posiciona cada linha, figura e elemento de design exatamente onde o canvas e o PDF os posicionam, mas pinta menos coisas ao redor deles:

RecursoCanvas (renderPage)HTML (renderToHtml)PDF (renderToPdf)
Fundo da páginaBranco, com page.backgroundColor sobre o refile e a sangria.Transparente, a menos que você passe background ou defina page.backgroundColor (que então preenche a caixa da página inteira, área de slug incluída).Branco, com page.backgroundColor sobre o refile e a sangria.
Grade de linhas de base (page.baselineGrid)DesenhadaNão desenhadaDesenhada
Fio entre colunas (layout.columnRule)DesenhadoNão desenhadoDesenhado
Marcas de corte (page.cutLines)DesenhadasNão desenhadas; a caixa da página continua incluindo a área de slug ao redor do refile.Desenhadas
Negativo da páginaOpção pageNegativeNão disponívelOpção pageNegative
TextoPixelsTexto selecionável em elementos com posicionamento absoluto, composto nas famílias de fonte do CSS: a página precisa carregar as mesmas fontes.Fontes incorporadas a partir do seu fontProvider; selecionável, pesquisável e com tags.
Texto vertical (layout.writingMode: 'vertical-rl')Caracteres pintados uma célula por vez, girados de volta à posição em pé; formas verticais por meio de uma fonte gêmea (loadVerticalAlternates).O fluxo em uma caixa girada um quarto de volta; cada linha girada de volta à posição em pé e composta com writing-mode: vertical-rl, de modo que o navegador usa as formas verticais e põe os caracteres em pé; números curtos em text-combine-upright: all; um travessão, reticências, um ponto médio ou um til ondulado em uma caixa da sua célula (o navegador o avançaria pela largura horizontal), e um travessão esticado para preenchê-la por flow.dashAdvances.Caracteres em pé por meio de uma gêmea Identity-V de cada fonte; veja Texto vertical no PDF.
ImagensregisterResourceImageA opção resourceImageUrl(fileId); sem ela, uma caixa cinza no lugar.A opção resourceBytes(fileId).
FórmulasCaminhos vetoriais<svg> embutidoCaminhos vetoriais
LinksNenhumAs citações :ref apontam para o seu recurso; as linhas do sumário, não.As citações :ref e as linhas do sumário, além dos marcadores (bookmarks).

A página transparente faz diferença em um site escuro: uma visualização sem background mostra texto preto sobre o fundo escuro do site. Passe renderToHtml(doc, { background: '#ffffff' }) ou dê ao documento um page.backgroundColor.

Os estilos de texto da página hospedeira ficam de fora. Cada linha é composta nas larguras que o motor mediu, então um letter-spacing, word-spacing, text-transform ou font-variant que a saída herdasse da página ao redor alargaria os trechos de glifos e faria as linhas se sobreporem. Por isso a raiz .pt-doc redefine as propriedades de texto herdadas (espaçamento entre letras e entre palavras, caixa, recuo, espaço em branco, estilo, variante, peso, largura, recursos e kerning da fonte, entrelinha, alinhamento, sombra e ênfase do texto, hifenização, direção, modo de escrita, contorno e preenchimento do texto e o ampliamento de texto em celulares) antes das suas próprias declarações de layout, de modo que a saída tem a mesma aparência dentro de uma shadow root ou sob um elemento estilizado. A lista é exportada como HTML_TEXT_RESET, uma string de declarações CSS: um hospedeiro que monta o innerHtml das páginas (de renderToHtmlIndexed) em contêineres próprios a aplica à raiz deles. Até o postext 1.4 a raiz não redefinia nada; a solução era um invólucro com all: initial.

#Geração de PDF

A saída em PDF fica em um pacote separado, postext-pdf, para que integrações apenas web não paguem o custo de pdf-lib e @pdf-lib/fontkit. O renderizador de PDF não mede o texto de novo: consome exatamente o mesmo VDTDocument que você passaria a renderToCanvas ou renderToHtml e converte as coordenadas em pixels para pontos de PDF. Por isso as três saídas sempre coincidem nas quebras de linha, nas alturas das colunas e na posição dos recursos.

No navegador, construa o VDT pelo Web Worker. renderToPdf em si é rápido depois que o VDT existe; a parte cara é o pipeline de layout que o produziu. Executar esse pipeline no worker mantém a interface responsiva e permite que uma exportação de PDF reaproveite o mesmo cache de medição que a visualização ao vivo já aqueceu. Veja Conduzir a exportação de PDF a partir do worker para o fluxo recomendado. Os exemplos na thread principal abaixo são a referência do que os argumentos significam; em código de interface, construa primeiro o VDT no worker e chame diretamente apenas renderToPdf.

#Instalação

npm install postext postext-pdf

postext é uma peer dependency de postext-pdf. Cada versão de postext-pdf precisa do postext com que foi lançada, ou de um posterior da mesma versão major (o seu intervalo de peer é ^ aquela versão, ^1.5.0 para a 1.5.0), porque importa funções que o postext acrescentou naquela versão. Atualize os dois juntos e, em uma CDN, fixe os dois na mesma versão.

#API pública

O pacote expõe um único ponto de entrada e alguns tipos:

  • renderToPdf(doc, options): Promise<Uint8Array>: recebe um VDTDocument (ou os capítulos de um livro como uma lista deles) e devolve os bytes brutos do PDF.
  • PdfFontProvider: a assinatura de callback (family, weight, style, request?) => Promise<Uint8Array | Uint8Array[]> que renderToPdf usa para pedir os bytes de uma fonte quando precisa incorporar uma nova combinação de família/peso/estilo. request.codePoints contém os caracteres que as páginas compõem naquela variante; a resposta é um arquivo, ou vários que juntos formam a variante (veja Fontes chinesas, japonesas e coreanas).
  • RenderToPdfOptions: { fontProvider, resourceBytes?, outlines?, accessible?, colorSpace?, pageNegative?, characterGrid?, onProgress?, onWarning?, rasterizeSvg?, harfbuzzWasm?, print?, outputProfile?, profileBaseUrl? }. outlines, accessible e colorSpace assumem o pdfGeneration do documento quando omitidos (veja Geração de PDF (configuração)). resourceBytes é descrito em Bytes de recursos e matrizes de impressão; onWarning, em Quais variantes são pedidas ao provedor e em Avisos no documento. characterGrid: true imprime a grade que cjk.grid.show desenha na tela e que, de outro modo, o PDF deixa de fora (veja Grade de caracteres). harfbuzzWasm diz de onde carregar o harfbuzz.wasm do HarfBuzz (uma URL, relativa à página, ou os bytes do arquivo) para um documento com texto da direita para a esquerda ou de letras ligadas; se omitido, usa a cópia ao lado do módulo do postext-pdf e, depois, a mesma versão do harfbuzzjs no jsDelivr e no esm.sh. print recebe as configurações de produção gráfica (padrão PDF/X, perfil de saída, preto, verificação de pré-impressão) e, se omitido, assume o print do documento; com um padrão PDF/X, ou com colorSpace: 'cmyk', cada cor passa pela separação de cores com o perfil ICC de saída, cujos bytes outputProfile fornece (senão, ele é baixado de profileBaseUrl, por padrão a cópia da pasta icc/ do postext na CDN do npm).
  • PdfWarning: um problema não fatal informado por onWarning; distinga pelo kind: 'fontFallback' (PdfFontFallbackWarning), uma variante composta em outro corte da sua família; 'missingGlyph' (PdfMissingGlyphWarning), caracteres para os quais nenhum arquivo de uma variante tem glifo; 'variableFontDefaultInstance' (PdfVariableFontWarning), uma fonte variável pedida em um peso diferente da sua instância padrão; 'cffEmbeddedWhole' (PdfCffEmbeddedWholeWarning), uma fonte CFF de mais de 2 MB incorporada inteira; 'complexShapingUnavailable' (PdfComplexShapingWarning), texto da direita para a esquerda ou de letras ligadas desenhado sem o HarfBuzz, que não pôde ser carregado (reason lista onde ele foi procurado), de modo que os sinais do árabe ficam mal posicionados; 'outputProfileUnavailable' (PdfPrintWarning), uma renderização em CMYK cujo perfil de saída não pôde ser carregado, convertida com a fórmula simples; 'pageNegativeIgnored' (PdfPrintWarning), a página em negativo deixada de fora de um arquivo PDF/X-1a; ou 'missingImage', uma imagem sem bytes desenhada como espaço reservado (informado apenas a um onWarning que você passe; sem ele, os avisos de fontes vão para console.warn).
  • decompressWoff2(bytes): Uint8Array: função auxiliar que converte um arquivo WOFF2 em bytes TTF, o formato que pdf-lib consegue incorporar diretamente.
  • createPdfWorker(options?), de postext-pdf/worker: a mesma renderização em um Web Worker; veja Renderizar o PDF em um worker.

#Exemplo mínimo

import { buildDocument } from 'postext';
import { renderToPdf } from 'postext-pdf';
 
const vdt = buildDocument(
  { markdown: '# Chapter One\n\nThe story begins here…' },
  {
    page: { sizePreset: '17x24' },
    layout: { layoutType: 'double' },
    bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 9, unit: 'pt' } }, // 9 pt substitui o padrão de 8 pt
  },
);
 
const pdfBytes = await renderToPdf(vdt, {
  fontProvider: async (family, weight, style) => {
    // Devolve os bytes TTF desta família/peso/estilo.
    // Veja a seção "Provedor de fontes" abaixo para uma implementação real.
    const res = await fetch(`/fonts/${family}-${weight}${style === 'italic' ? 'i' : ''}.ttf`);
    return new Uint8Array(await res.arrayBuffer());
  },
});
 
// `pdfBytes` é um Uint8Array: salve, baixe ou transmita por streaming.
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
window.open(url);

#Por que um provedor de fontes?

pdf-lib incorpora arquivos de fonte reais ao PDF: as fontes instaladas no navegador não estão disponíveis na hora da renderização, e uma fonte que você carregou só para medir na tela não basta, sozinha, para produzir um PDF autossuficiente. renderToPdf percorre as páginas em busca de cada variante que elas pintam (uma por combinação family|weight|style; veja Quais variantes são pedidas ao provedor) e chama o seu provedor uma vez por combinação única. O provedor devolve um Uint8Array com bytes TTF ou OTF, ou uma lista deles para uma variante servida em vários arquivos (veja Fontes chinesas, japonesas e coreanas); pdf-lib gera subconjuntos dos contornos TrueType e incorpora inteiros os arquivos CFF (.otf). Variantes às quais o provedor responde com o mesmo arquivo, como um corte regular que substitui o negrito que falta a uma família, compartilham uma única fonte incorporada. Uma variante com que nenhuma página acaba desenhando, como a do texto de uma figura SVG quando a figura é desenhada como imagem, fica fora do arquivo.

Use fontes estáticas por peso, não uma única fonte variável. O Google Fonts muitas vezes serve um único WOFF2 variável por família, cobrindo todo o eixo de pesos. pdf-lib só consegue incorporar a instância padrão de um arquivo variável, então um parágrafo em negrito sairia com peso regular. O Fontsource publica arquivos WOFF2 estáticos por peso, que resolvem isso de forma limpa; é o padrão que o Sandbox usa. Um arquivo variável pedido em um peso diferente da sua instância padrão é informado com um aviso variableFontDefaultInstance.

Cada palavra do texto cai onde o layout a colocou. Em parágrafos, itens de lista, citações, boxes e outros textos corridos, cada palavra começa na posição que o VDT mediu, de modo que uma diferença entre as larguras do navegador e as da fonte incorporada nunca se acumula ao longo de uma linha. Uma linha sem formatação inline é pintada como um único objeto de texto que move a pena entre as palavras; linhas justificadas, centralizadas e com formatação são pintadas palavra por palavra. Um caractere para o qual a fonte não tem glifo, que o navegador mediu em outra fonte e que o PDF pinta como a caixa de glifo ausente da fonte, não desloca nenhuma das palavras seguintes e é informado uma vez por variante como aviso missingGlyph. Um espaço que a fonte não tem, como o espaço estreito sem quebra ou o espaço de algarismo, assume a largura que o navegador lhe deu, e caracteres invisíveis como o word joiner e o espaço de largura zero não são desenhados. Um hífen sem quebra (U+2011) que a fonte não tem é desenhado com o hífen da fonte (U+2010), ou com o hífen-menos quando ela também não tem hífen, como o navegador o mostra; Open Sans e Outfit, entre outras, não têm nenhum dos dois. Nenhum desses casos conta como glifo ausente. Há duas exceções. Uma linha com letras da direita para a esquerda continua sendo pintada como um único trecho (veja Idiomas e escritas). O texto posicionado por um design (cabeços e rodapés, aberturas, títulos de boxes e outros elementos de design) é composto com as larguras da própria fonte incorporada, de modo que ali um glifo que falta à fonte ainda desloca o resto da linha.

#Quais variantes são pedidas ao provedor

renderToPdf percorre as páginas do mesmo jeito que as pinta e pede ao provedor apenas as variantes que elas desenham:

  • a variante regular de um bloco que compõe qualquer linha, e a negrito, itálica ou negrito-itálica de cada trecho efetivamente composto assim;
  • trechos de chips, marcadores de lista e o texto dos espaços de design (cabeços, fólios, faixas de abertura e de parte);
  • o texto de legenda, de nota e de células de tabela de cada recurso;
  • as variantes que o <text> de uma figura SVG nomeia, quando a figura é incorporada. Uma família que o provedor não consegue fornecer de jeito nenhum passa para a família seguinte da lista font-family do SVG.

Assim, uma família de títulos que ninguém inclina nunca tem o seu itálico pedido, e uma figura sem nota nunca precisa das variantes de nota.

Quando o provedor recusa uma variante, a renderização continua. Outra variante da mesma família é incorporada no lugar e um PdfWarning é informado. A substituta é a primeira variante que carregar, testando os nove pesos padrão (100 a 900) na ordem que a correspondência de fontes do CSS usa, que também é a variante que o navegador mostra na visualização:

  1. primeiro o mesmo estilo. Para um peso de 400 a 500, vêm primeiro os pesos até 500, depois os mais leves, do mais próximo para baixo, depois os mais pesados, de 600 para cima. Para um peso abaixo de 400, vêm primeiro os pesos mais leves, do mais próximo para baixo, depois os mais pesados. Para um peso acima de 500, vêm primeiro os pesos mais pesados, depois os mais leves;
  2. depois o outro estilo, itálico para o redondo e redondo para o itálico, no peso pedido e nos outros pesos, na mesma ordem.

Por isso, uma família sem itálicos compõe os seus trechos em itálico em redondo, uma família que só traz 400 e 700 toma um 600 como 700, e uma família com um único corte compõe tudo nele. O provedor recebe um pedido de variante por vez, nessa ordem, e nunca duas vezes para a mesma variante, então uma variante que nada usa nunca é incorporada. Uma família que o provedor não consegue servir de jeito nenhum recebe pedidos para cada uma dessas 18 variantes antes de a renderização falhar.

const bytes = await renderToPdf(doc, {
  fontProvider,
  onWarning: (w) => {
    // { kind: 'fontFallback', family: 'Oswald', weight: 700, style: 'italic',
    //   fallback: { weight: 700, style: 'normal' }, reason: '…', message: '…' }
    console.info(w.message);
  },
});

Sem onWarning, a mensagem vai para console.warn. O texto mantém as suas posições, que vêm do VDT e foram medidas com as variantes que o navegador tinha, então uma substituta com larguras diferentes pode parecer apertada ou frouxa. Forneça a variante verdadeira para corrigir isso. Uma renderização só falha (postext-pdf: failed to load font(s): …) quando o provedor não consegue fornecer nenhuma variante de uma família em um peso padrão, redonda ou itálica.

#Provedor de fontes no navegador (Fontsource + WOFF2)

O Sandbox traz createPdfFontProvider() (packages/postext-sandbox/src/viewport/pdfFontProvider.ts), que você pode copiar para qualquer app de navegador. O essencial:

import type { PdfFontProvider } from 'postext-pdf';
import { decompressWoff2 } from 'postext-pdf';
 
const bytesCache = new Map<string, Promise<Uint8Array>>();
 
function fontsourceId(family: string): string {
  return family.toLowerCase().replace(/\s+/g, '-');
}
 
function fontsourceWoff2Url(
  family: string,
  weight: number,
  style: 'normal' | 'italic',
): string {
  const id = fontsourceId(family);
  return `https://cdn.jsdelivr.net/npm/@fontsource/${id}@latest/files/${id}-latin-${weight}-${style}.woff2`;
}
 
export function createPdfFontProvider(): PdfFontProvider {
  return async (family, weight, style) => {
    const key = `${family}|${weight}|${style}`;
    const cached = bytesCache.get(key);
    if (cached) return cached;
 
    const promise = (async (): Promise<Uint8Array> => {
      const url = fontsourceWoff2Url(family, weight, style);
      const res = await fetch(url, { mode: 'cors' });
      if (!res.ok) throw new Error(`font fetch failed: ${res.status} ${url}`);
      // O pdf-lib precisa de bytes TTF, então descompactamos o invólucro WOFF2 no cliente.
      return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
    })();
 
    bytesCache.set(key, promise);
    return promise;
  };
}

Uma versão de produção também deveria:

  • Consultar os pesos disponíveis (via https://api.fontsource.org/v1/fonts/{id}) e ajustar o peso pedido ao mais próximo que a família realmente oferece, para que um pedido de weight: 600 em uma família que só tem {400, 700} ainda funcione.
  • Recorrer do itálico ao normal quando uma família não tem corte itálico para o peso pedido, em vez de fazer falhar a renderização inteira.
  • Reaproveitar o cache entre renderizações (mantenha bytesCache no escopo do módulo, não por chamada), para que gerar de novo o PDF depois de uma mudança de configuração saia praticamente de graça.

#Fontes chinesas, japonesas e coreanas

Uma fonte CJK não vem em um único arquivo pequeno. O Fontsource distribui a Noto Serif SC em cerca de cem arquivos por peso, cada um com uma parte dos caracteres e declarado na folha de estilo da família com o seu unicode-range (@fontsource/noto-serif-sc/400.css); o navegador baixa os arquivos que o texto de uma página toca. O arquivo latin que o provedor acima busca não tem nenhum caractere Han, e os subconjuntos nomeados estão incompletos: o chinese-simplified da Noto Serif SC não tem 釵, e o chinese-traditional da Noto Serif TC não tem nenhuma das marcas de largura total (),!?:;.

Por isso, um provedor pode responder a uma variante com vários arquivos. renderToPdf passa a ele os caracteres que as páginas compõem naquela variante (request.codePoints), reunidos de todos os capítulos antes de qualquer coisa ser desenhada, e o provedor devolve os arquivos que os contêm, na ordem em que o navegador os consulta. Cada arquivo é incorporado como um subconjunto próprio, e cada caractere é desenhado a partir do primeiro arquivo que tiver um glifo para ele: um capítulo que toca 60 fatias incorpora 60 subconjuntos pequenos. Uma variante pedida de novo, para o texto de uma figura SVG, por exemplo, só é pedida para os caracteres que os seus arquivos não têm. Um provedor que devolve um único Uint8Array funciona como antes. O provedor do Sandbox lê a folha de estilo do Fontsource do peso e do estilo e busca os arquivos cujos intervalos contêm o texto; o núcleo dele:

import type { PdfFontProvider } from 'postext-pdf';
import { decompressWoff2 } from 'postext-pdf';
 
type Slice = { url: string; ranges: Array<[number, number]> };
 
async function fontsourceSlices(family: string, weight: number, style: 'normal' | 'italic'): Promise<Slice[]> {
  const id = family.toLowerCase().replace(/\s+/g, '-');
  const cssUrl = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/${weight}${style === 'italic' ? '-italic' : ''}.css`;
  const css = await (await fetch(cssUrl)).text();
  return [...css.matchAll(/@font-face\s*{([^}]*)}/g)].map(([, rule]) => ({
    url: new URL(/url\(\.?\/?([^)]+\.woff2)\)/.exec(rule)![1], cssUrl).href,
    ranges: /unicode-range:\s*([^;]+);/.exec(rule)![1].split(',').map((part) => {
      const [lo, hi = lo] = part.trim().slice(2).split('-');
      return [parseInt(lo, 16), parseInt(hi, 16)] as [number, number];
    }),
  }));
}
 
export const sliceFontProvider: PdfFontProvider = async (family, weight, style, request) => {
  // Onde os intervalos se sobrepõem, o navegador tenta primeiro a última regra.
  const slices = (await fontsourceSlices(family, weight, style)).reverse();
  const picked = new Set<Slice>();
  for (const cp of request?.codePoints ?? []) {
    const slice = slices.find((s) => s.ranges.some(([lo, hi]) => cp >= lo && cp <= hi));
    if (slice) picked.add(slice);
  }
  if (picked.size === 0) picked.add(slices[0]!);
  return Promise.all(slices.filter((s) => picked.has(s)).map(async (s) =>
    decompressWoff2(new Uint8Array(await (await fetch(s.url)).arrayBuffer()))));
};

Uma família latina passa pelo mesmo código: um texto em inglês recebe só o seu arquivo latin, um texto em tcheco recebe latin e latin-ext.

Os caracteres para os quais nenhum arquivo da variante tem glifo são desenhados com o glifo .notdef da fonte (uma caixa vazia na maioria das fontes), e renderToPdf os informa uma vez por variante depois de desenhar as páginas:

// { kind: 'missingGlyph', family: 'Noto Serif TC', weight: 400, style: 'normal',
//   characters: [',', '!', '?'], message: '…' }

O Sandbox lista esses avisos, e os dois descritos abaixo, no seu painel Verificações depois de cada PDF que gera. Quando o livro, as suas configurações ou os seus recursos mudam, eles passam a ser marcados como vindos de um PDF anterior até que o próximo os substitua; abrir outro livro os apaga.

  • O negrito precisa de um arquivo estático por peso. O Fontsource serve cada peso da Noto Serif SC e TC como arquivos estáticos separados, então o negrito funciona no Sandbox. Os arquivos do Google Fonts (NotoSerifSC[wght].ttf, 25 MB) são fontes variáveis: o pdf-lib incorpora a instância padrão delas, de modo que uma variante 700 sai impressa em 400, e renderToPdf informa isso como variableFontDefaultInstance. Para um pacote, gere uma instância estática por peso com o fontTools (fonttools varLib.instancer NotoSerifSC[wght].ttf wght=700) e reduza-a aos caracteres do livro com pyftsubset.
  • Use as versões TrueType. A Source Han Serif e os arquivos .otf da Noto Serif CJK têm contornos CFF, que o postext-pdf incorpora inteiros, de 8 a 25 MB por peso; uma fonte CFF com mais de 2 MB é informada como cffEmbeddedWhole. As versões TrueType (Google Fonts, Fontsource) são reduzidas aos glifos usados. Para japonês, a Noto Serif JP e a Noto Sans JP (Google Fonts, ou as fatias numeradas do Fontsource), a Shippori Mincho, a Zen Old Mincho e a BIZ UDMincho vêm em TrueType; a Source Han Serif JP e os arquivos .otf JP da Noto Serif CJK são CFF.
  • As formas japonesas de uma fonte pan-CJK. Um mesmo ponto de código de Han, de pontuação ou de aspas pode ser desenhado de um jeito no Japão e de outro na China, e uma fonte pan-CJK (Source Han, Noto CJK) contém os dois. O PDF faz o shaping de um documento japonês (locale: 'ja'), e de um isolamento em japonês (:ltr[…]{lang=ja}) em qualquer documento, com o sistema de idioma OpenType JAN , de modo que o recurso locl da fonte imprime as formas japonesas que o canvas e o HTML imprimem por meio de lang. Um isolamento em outro idioma dentro de um livro japonês passa pelo shaping com as formas daquele idioma (as formas padrão da fonte, no caso do chinês) e é marcado como um Span com o seu /Lang. Documentos em chinês e em outros idiomas passam pelo shaping com as formas padrão da fonte, como antes. Uma fonte feita para o japonês, como a Noto Serif JP, tem formas padrão japonesas; mesmo assim, ela compõe as “ ” do texto japonês nas suas formas JAN .

Um caractere que falta em uma família não é tomado de outra: a Noto Serif TC não empresta da Noto Serif SC. Resolva a cobertura quando gerar os arquivos de fonte; a vitrine 红楼梦 copia da fonte SC os glifos que faltam aos seus subconjuntos TC.

#Provedor de fontes no servidor (Node, arquivos locais)

No Node você pode pular totalmente a etapa do WOFF2 e ler arquivos TTF/OTF do disco:

import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import type { PdfFontProvider } from 'postext-pdf';
 
const FONT_DIR = '/path/to/fonts';
 
function filename(family: string, weight: number, style: 'normal' | 'italic'): string {
  const slug = family.replace(/\s+/g, '');
  const styleSuffix = style === 'italic' ? 'Italic' : '';
  const weightName =
    weight >= 700 ? 'Bold'
    : weight >= 600 ? 'SemiBold'
    : weight >= 500 ? 'Medium'
    : weight >= 300 ? 'Light'
    : 'Regular';
  return `${slug}-${weightName}${styleSuffix}.ttf`;
}
 
export const localFontProvider: PdfFontProvider = async (family, weight, style) => {
  const buf = await readFile(join(FONT_DIR, filename(family, weight, style)));
  return new Uint8Array(buf);
};

#Bytes de recursos e matrizes de impressão

resourceBytes(fileId) devolve os bytes brutos de uma imagem, e o renderizador identifica o formato:

  • PNG, JPEG, GIF e WebP são incorporados como imagens;
  • a marcação SVG é desenhada como caminhos vetoriais, ou rasterizada a 600 dpi no navegador quando usa recursos fora do subconjunto vetorial;
  • de um PDF, a primeira página é incorporada tal como está, como um form XObject.

Cada imagem é guardada no arquivo uma única vez, por mais vezes que seja desenhada. Um SVG desenhado como caminhos vetoriais vira um form XObject que todas as páginas pintam, de modo que uma moldura ou um logotipo no design de página de um documento de trinta páginas é escrito uma vez, não trinta; cada página a mais acrescenta algumas centenas de bytes. Até o postext-pdf 1.4, cada página levava a sua própria cópia dos caminhos.

Uma figura SVG pode nomear uma matriz de impressão em svg.pdfFileId: um PDF de uma página com a mesma figura, em geral o original do qual o SVG foi exportado. renderToPdf pede primeiro a resourceBytes o id da matriz. Ele incorpora essa página no lugar do SVG, com as suas fontes, degradês e espaços de cor intactos, em todo lugar onde o SVG é desenhado: como figura, como imagem de uma célula de tabela (TableCell.image), como imagem de design ou como ícone de boxe (o seu marker também). O VDT leva o id da matriz em cada um desses usos (svg.pdfFileId no recurso da figura, pdfFileId em uma imagem de célula e em um bloco de imagem de design), então, em um livro, todos os capítulos recebem a matriz. Os renderizadores de canvas e HTML continuam desenhando o SVG. Os bytes do próprio SVG são usados no lugar em três casos: a matriz está faltando, não é um PDF ou a tinta única está ativada (diagramStyle.singleInk só recolore marcação SVG).

const resources: Resource[] = [{
  id: 'map', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0,
  svg: { fileId: 'map.svg', width: 800, height: 600, pdfFileId: 'map.pdf' },
}];
const files = new Map([['map.svg', svgBytes], ['map.pdf', masterPdfBytes]]);
const pdf = await renderToPdf(buildDocument({ markdown, resources }, config), {
  fontProvider,
  resourceBytes: (fileId) => files.get(fileId),
});

Um hospedeiro também pode devolver os bytes da matriz para o id do próprio SVG, como faz bundleResourceBytes; as duas formas funcionam.

#Texto vertical no PDF

Uma página vertical (layout.writingMode: 'vertical-rl') é desenhada por meio de um referencial girado um quarto de volta, como o canvas a pinta, e o seu texto é composto coluna abaixo:

  • Os caracteres em pé são mostrados por meio de uma segunda fonte Type0 do mesmo arquivo incorporado: a mesma CIDFont, as mesmas larguras e o mesmo mapa ToUnicode, com Encoding /Identity-V (modo vertical). Uma sequência de caracteres é um único objeto de texto cujos glifos avançam sozinhos um eme coluna abaixo (DW2 [880 −1000]), de modo que os leitores de PDF selecionam e extraem uma coluna como uma única linha. Os glifos passam pelo shaping com os recursos OpenType vert e fwid, que dão as formas verticais a parênteses, aspas, vírgulas de enumeração do chinês continental, reticências e travessões; um caractere que já fica em pé como é mantém o seu glifo horizontal. Nada da fonte é incorporado duas vezes.
  • Palavras latinas e números longos correm de lado com a fonte horizontal; um número em uma célula fica em pé, comprimido até a largura de um eme quando é mais largo; uma marca para a qual a fonte não tem forma vertical é girada ou deslocada, como no canvas.
  • O tracking entre caracteres é escrito como números de TJ, que no modo vertical movem a pena coluna abaixo.
  • Cada linha vertical é marcada com um /ActualText do seu texto, para que a cópia e a extração de texto a leiam como foi escrita. O pdftotext e o pdf.js leem as colunas de cima para baixo, da direita para a esquerda; o pdf.js começa uma nova linha em um número composto em uma célula.
  • Links, marcadores e destinos são mapeados sobre a folha: um link sobre uma linha vertical é um retângulo alto e estreito, e um marcador abre a página no topo da coluna do seu título.
  • Um PDF com tags declara o modo de escrita no seu elemento Document (o atributo de Layout WritingMode /TbRl, que todos os elementos herdam); a validação PDF/UA-1 (veraPDF) passa em um capítulo vertical.
  • Visualizadores: Acrobat, Preview, Chrome (PDFium), pdf.js e Poppler renderizam as fontes verticais. Um livro com encadernação à direita (page.binding) também pede aos visualizadores que mostrem as suas páginas duplas da direita para a esquerda (/Direction /R2L, /PageLayout /TwoPageRight); Acrobat e Foxit seguem isso, o Chrome não.

Um capítulo de 43 páginas composto em Noto Serif TC (os caracteres do livro, TrueType) tem cerca de 820 KB, a maior parte nos dois subconjuntos de fonte.

As palavras de um link Markdown (veja Formato do documento › Links) viram anotações de link URI, uma por trecho de palavras com link em uma linha. Cada uma cobre a caixa da linha e não tem borda. Em uma renderização acessível, cada trecho é um elemento Link cujo /Contents é o seu texto. Só recebem link os destinos absolutos http:, https:, mailto:, tel: e ftp:, porque uma URL relativa não tem base dentro de um PDF. Os caracteres fora do ASCII imprimível são codificados com porcentagem. As citações :ref e as linhas do sumário mantêm os seus links dentro do documento.

#Exemplo completo no navegador: construir, renderizar, baixar

Juntando tudo: construir o VDT, renderizar em PDF e disparar o download a partir do navegador:

import { buildDocument, createMeasurementCache } from 'postext';
import { renderToPdf } from 'postext-pdf';
import { createPdfFontProvider } from './pdfFontProvider';
 
const fontProvider = createPdfFontProvider();
 
export async function downloadPdf(markdown: string, config: PostextConfig) {
  const cache = createMeasurementCache();
  const vdt = buildDocument({ markdown }, config, cache);
 
  const bytes = await renderToPdf(vdt, { fontProvider });
 
  const blob = new Blob([bytes.slice().buffer], { type: 'application/pdf' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'document.pdf';
  document.body.appendChild(a);
  a.click();
  a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

Importante: chame ensureConfigFontsLoaded(config) (ou equivalente) antes de buildDocument quando a sua configuração fizer referência a fontes web. O layout é medido com as métricas de fonte que o navegador tiver naquele momento para a família; se a fonte verdadeira ainda não chegou, o VDT é medido com uma substituta e o PDF não vai coincidir com a saída do canvas ou do HTML. O Sandbox faz isso explicitamente antes de cada renderização (veja packages/postext-sandbox/src/viewport/PdfViewport.tsx).

#Exemplo ao vivo: um PDF no navegador

O fluxo completo acima, rodando no navegador: o pen importa postext e postext-pdf de uma CDN, carrega as fontes web, constrói o documento, incorpora os cortes do Fontsource pelo provedor de fontes e entrega os bytes a um link que abre o arquivo em uma nova aba e a um link de download. O PDF resultante tem as mesmas quebras de linha que a saída do canvas e do HTML, fontes realmente incorporadas e marcadores de estrutura.

Postext · gerar um PDF no navegador
import { buildDocument } from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';
 
const markdown = `# The Lantern
 
The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.
 
## Two columns
 
Postext lays this text out in **two columns**, breaking each paragraph with the *Knuth–Plass* algorithm and hyphenating with TeX patterns. Widows and orphans are avoided, and the columns are balanced on the last page.
 
The light it gave was small, but it was enough to find the step. The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.`;
 
const config = {
  page: { sizePreset: '17x24' },
  layout: { layoutType: 'double' },
  bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 10, unit: 'pt' } },
};
 
// The PDF embeds real font files. Fontsource publishes one static WOFF2 per
// weight and style; decompress it to the TTF bytes pdf-lib can embed.
const fontProvider = async (family, weight, style) => {
  const id = family.toLowerCase().replace(/\s+/g, '-');
  const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@latest/files/${id}-latin-${weight}-${style}.woff2`;
  const res = await fetch(url);
  if (!res.ok) throw new Error(`font fetch failed: ${res.status} ${url}`);
  return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
};
 
// Layout is measured with the browser's fonts, so load them before building:
// otherwise the PDF would not match the canvas or HTML output.
await Promise.all([
  document.fonts.load('16px "EB Garamond"'),
  document.fonts.load('bold 16px "EB Garamond"'),
  document.fonts.load('italic 16px "EB Garamond"'),
  document.fonts.load('bold 16px "Open Sans"'), // the default heading face
]);
 
const doc = buildDocument({ markdown }, config);
 
// Same VDT, now translated to PDF points: identical line breaks and placement.
const bytes = await renderToPdf(doc, { fontProvider });
 
// A PDF viewer cannot run inside this sandboxed result frame,
// so hand the file to a new tab and to a download link.
const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' }));
document.getElementById('open').href = url;
document.getElementById('download').href = url;
document.getElementById('links').hidden = false;
document.getElementById('status').textContent =
  `${doc.pages.length} page(s) · ${(bytes.length / 1024).toFixed(0)} KB PDF`;
index.html
<p id="status">Rendering…</p>
<p id="links" hidden>
  <a id="open" target="_blank" rel="noopener">Open lantern.pdf in a new tab</a> ·
  <a id="download" download="lantern.pdf">Download it</a>
</p>
style.css
body {
  margin: 16px;
  font-family: system-ui, sans-serif;
}

Carrega um editor interativo de codepen.io. O exemplo importa a versão mais recente do postext de uma CDN.

#Renderizar o PDF em um worker

postext-pdf/worker tira renderToPdf da thread principal. O worker escreve o texto, as figuras vetoriais, a árvore de estrutura e o próprio arquivo. Duas tarefas precisam da página, então o worker as pede à thread principal: buscar as fontes e rasterizar um SVG por meio de um <img>. Em um livro de centenas de páginas, a renderização leva segundos que, de outro modo, travariam a página; para poucas páginas, chamar renderToPdf diretamente é mais simples.

import { createPdfWorker } from 'postext-pdf/worker';
 
const pdfWorker = createPdfWorker();
const bytes = await pdfWorker.render(docs, {
  fontProvider,                                     // roda nesta thread
  resourceBytes: new Map([['map.svg', svgBytes]]),  // um Map; os seus buffers passam para o worker
  onProgress: ({ phase, pages, totalPages }) => showProgress(phase, pages, totalPages),
  onWarning: (w) => console.info(w.message),
});
pdfWorker.dispose();
  • render(docs, options) recebe um documento ou a lista de documentos de capítulos de um livro. Aceita as opções de renderToPdf, com duas diferenças. resourceBytes é um Map<string, Uint8Array> cujos buffers são transferidos, então passe cópias dos bytes que você quer manter. rasterizeSvg, quando informado, roda na thread principal; por padrão, o Image e o canvas da própria página fazem o trabalho.
  • Um handle renderiza um documento por vez. dispose() encerra o worker e rejeita qualquer renderização ainda pendente.
  • createPdfWorker({ worker }) aceita um Worker criado por você, para ferramentas de build que controlam as URLs de workers. Esse worker precisa executar postext-pdf/worker/entry.

A partir de uma CDN. Por padrão, o script do worker é carregado da URL do próprio pacote (new URL('./pdf.worker.js', import.meta.url)). Uma página em outra origem pode não conseguir iniciá-lo: importado do esm.sh, createPdfWorker() lança Failed to construct 'Worker': Script at 'https://esm.sh/postext-pdf@…/pdf.worker.js' cannot be accessed from origin …. Em vez disso, inicie um worker de módulo da mesma origem que importe o ponto de entrada (se fixar uma versão, fixe a mesma nas duas URLs):

import { createPdfWorker } from 'https://esm.sh/postext-pdf/worker';
 
const entry = URL.createObjectURL(new Blob(
  ["import 'https://esm.sh/postext-pdf/worker/entry';"],
  { type: 'text/javascript' },
));
const pdfWorker = createPdfWorker({ worker: new Worker(entry, { type: 'module' }) });

O worker de layout de postext/worker precisa do mesmo invólucro ao redor de postext/worker/entry (veja Executar o layout em um Web Worker).

#PDFs prontos para impressão

Para fluxos de produção gráfica, ajuste estas opções de configuração antes de renderizar:

  • page.cutLines.enabled: true: acrescenta a área de sangria e as marcas de corte ao redor do refile, e dá a cada página uma TrimBox e uma BleedBox. Veja Marcas de corte.
  • print: { standard: 'pdfx4', outputProfile: 'fogra51' } (ou 'pdfx1a'): um arquivo PDF/X com a condição de saída, a identificação e as caixas que uma gráfica confere; cada cor e cada imagem RGB separadas com o perfil ICC, o preto 100 % K sobreimpresso e as áreas pretas grandes em preto composto. Veja Produção gráfica (configuração).
  • colorSpace: 'cmyk' (ou pdfGeneration: { forceColorSpace: true, colorSpace: 'cmyk' }): a mesma separação, sem a identificação PDF/X (as marcas de corte ficam sempre na cor de registro). As matrizes de impressão em PDF são incorporadas como estão.
  • page.dpi: 300: os px por polegada da diagramação; um bitmap composto no seu próprio tamanho é impresso com essa resolução. O preflight informa as imagens abaixo de 300 ppi no tamanho impresso.
  • ColorValue.cmyk: uma cor definida em CMYK é impressa com os seus valores exatos.
  • { pageNegative: true } em RenderToPdfOptions: inverte a área de refile com um modo de mesclagem Difference (as marcas de corte não são invertidas). Útil em verificações de pré-impressão de tipografia escura sobre fundo claro.

#Implementação de referência

O componente PdfViewport do Sandbox (packages/postext-sandbox/src/viewport/PdfViewport.tsx) liga as peças acima em uma visualização ao vivo com botões para gerar de novo, baixar e imprimir, e é um bom ponto de partida para qualquer integração de PDF no navegador. Ele constrói o VDT pelo worker de layout compartilhado (veja Executar o layout em um Web Worker), para que clicar em Regenerar não congele a interface enquanto o pipeline roda; a thread principal só cuida de renderToPdf (que já é rápido depois que o VDT existe).

#Um livro em 3D (postext-folio)

postext-folio apresenta na tela um documento diagramado como um livro impresso aberto sobre uma mesa: páginas duplas segundo a regra do recto e folhas que o leitor vira com os botões ‹ ›, as setas do teclado, um deslizar do dedo, um clique em uma página ou pegando uma página pela borda e arrastando-a. Cada folha se curva em three.js de acordo com o seu papel e projeta uma sombra real sobre as páginas de baixo. O canvas WebGL desenha o livro parado e virando da mesma forma, então uma página nunca muda de aparência ao assentar. É o visualizador das Receitas e da aba Folio do Sandbox.

npm install postext postext-folio three
import { buildDocument } from 'postext';
import { createFolioFromDocument } from 'postext-folio';
 
const doc = buildDocument({ markdown }, config);
const book = createFolioFromDocument(document.getElementById('book')!, doc, {
  onChange: ({ pages }) => console.log('showing pages', pages),
});
 
// Depois de uma edição: o mesmo visualizador, na mesma página.
book.setDocument(buildDocument({ markdown: edited }, config));
  • As páginas são pintadas conforme são necessárias. createFolioFromDocument pinta cada página com renderPageToCanvas exatamente nos pixels de dispositivo de um espaço de página (o WebGL então a mostra texel por pixel, tão nítida quanto a visualização em canvas), e apenas as páginas duplas em torno da que está aberta (window, três de cada lado por padrão). As páginas que saem dessa janela são liberadas, então um livro de mil páginas custa a memória de algumas poucas. Um salto para uma página distante pinta primeiro aquela página dupla. Até dez páginas de distância, as folhas viram uma a uma; mais longe que isso, o bloco de páginas intermediárias se levanta como uma única placa, tão grossa quanto essas páginas (a soma das suas espessuras), e assenta do outro lado. setDocument mantém a pintura de cada página que fica igual no novo layout ({ repaint: true } pinta todas de novo, depois que uma imagem chegou).
  • O documento define o livro. A sua primeira página abre sozinha à direita quando é um recto (pageIndexOffset par), um livro com encadernação à direita (page.binding: 'right', ou um documento vertical) aparece espelhado e vira as páginas para a esquerda, as páginas em branco assumem a cor de fundo da página, e a largura de refile da página (pageWidthMm) dá a escala da espessura do papel e das capas. Um capítulo diagramado com uma continuation conta as outras páginas do livro (pageIndexOffset antes dele, bookPageCount depois dele) na espessura dos dois blocos de páginas sem desenhá-las (extraPages).
  • O documento define a aparência. O papel, a encadernação, a mesa e a luz são as configurações folio do documento (doc.config.folio). Uma página composta dentro de uma sequência :::paper leva o seu próprio papel (VDTPage.paper), e a sua folha é desenhada com a cor, a superfície, a espessura e a rigidez desse papel. Com binding.cover: 'pages', a primeira página é a capa da frente e a última, quando é um verso, a capa de trás (covers). Um formato de jornal ('broadsheet', 'berliner', 'tabloid', 'compact') cujas configurações não nomeiam papel nem encadernação aparece como papel-jornal dobrado, também quando o hospedeiro passa o seu próprio folio.
  • O contêiner define o tamanho. O livro o ocupa por inteiro, com os botões e a contagem de páginas nas margens, então dê uma altura ao contêiner; um redimensionamento pinta as páginas de novo no novo tamanho. Abaixo de 560 px de largura, ele mostra uma página por vez (mode: 'auto'; 'single' e 'double' forçam um ou outro): a lombada fica ao longo da borda interna da página e a folha vira sobre ela, arrastar em direção à lombada avança, deslizar para longe dela volta, e um toque vira a página.
  • O que o ponteiro faz. interaction (e, depois, setInteraction) define o que o botão esquerdo, um dedo ou uma caneta fazem sobre o livro: 'hand' (o padrão) pega e vira as páginas, 'orbit' gira a vista como faz o arrastar com o botão direito (para trackpads e tablets), 'select' deixa o ponteiro para o hospedeiro, para selecionar texto, por exemplo. pageAt(event) dá a página sob um ponteiro e o ponto dela ({ page, x, y }, frações da página a partir do canto superior esquerdo), no livro tal como é visto, inclinado ou girado; pointOnScreen(point) faz o caminho inverso, para desenhar um cursor de texto ou uma seleção sobre a página. refreshPage(src) volta a mostrar um canvas de página que o hospedeiro redesenhou no mesmo lugar. O Sandbox usa todos eles para selecionar texto e acompanhar o cursor do editor nas páginas em 3D.
  • Uma lupa para letras miúdas. interaction: 'magnify' mantém sobre o livro, onde está o ponteiro, uma lente redonda com aro preto (um dedo a mantém acima de si enquanto toca a tela). Ela mostra o livro como o olho do leitor o vê, iluminado e curvado como está, com o aumento máximo no centro e curvando-se em direção ao aro. A roda do mouse, + e − mudam o aumento (setMagnification(zoom), de 1,5 a 10; o padrão mostra a página a cerca de 5,5 px CSS por milímetro) e Esc a guarda; magnifier: { zoom, diameter } define os dois desde o início. createFolioFromDocument pinta de novo as páginas sob a lente, nítidas o bastante para o centro dela, de modo que o corpo de texto de um jornal fica legível; createFolio obtém essas pinturas de detail: { paint(index, deviceWidth), release() }. O Sandbox a coloca no botão Lupa (M). A lupa também seleciona texto: sobre as páginas, o cursor é o de texto, pageAt devolve o ponto sob o centro da lente (acima do dedo em uma tela sensível ao toque), e no Sandbox um clique ali posiciona o cursor de texto e um arrasto seleciona.
  • O leitor pode olhar ao redor. Arrastar com o botão direito gira a vista em órbita ao redor do livro (até 70° a partir da vertical), também enquanto as folhas viram; resetView() a devolve suavemente à tilt e ao yaw das configurações, e getView() dá a vista tal como é vista agora ({ tilt, yaw }, em graus) para guardá-la como essas configurações. O Sandbox os coloca em dois botões, Redefinir visualização e Salvar como visualização padrão.
  • Os vídeos tocam nas páginas. Um clique no pôster de um vídeo o reproduz na página, em qualquer modo de interaction, e ele continua tocando enquanto a sua folha vira; outro clique o pausa, e ele para quando o livro fica parado em uma página dupla que não o mostra. Um vídeo com player.autoplay começa sozinho na primeira vez que a sua página dupla aparece; um que também toca junto com os outros (player.exclusive: false) começa, sem som, toda vez, e para quando a página dupla é virada, vários ao mesmo tempo. Opções videos, videoUrl e onVideo, e stopVideo() no visualizador; veja Formato do documento › Vídeos nas páginas do Folio.
  • Fontes e imagens primeiro. Como no caso de renderPage, as fontes que o documento usa precisam estar carregadas em document.fonts e as suas imagens de recursos registradas com registerResourceImage antes de as páginas serem pintadas.
  • Acessível. O visualizador é um grupo focável que responde a ←/→ (espelhadas em um livro com encadernação à direita), Page Up/Down, Home e End; os seus botões e a contagem de páginas têm rótulos (labels os traduz), e cada canvas de página leva um texto alt (alt: (index) => …).
  • Sem WebGL2, ou quando o leitor pede movimento reduzido, as páginas duplas simplesmente mudam. Livros em WebGL são pesados para um celular (uma textura por lado de página): o Sandbox só oferece a aba Folio onde há WebGL2 e o lado menor da tela mede pelo menos 600 px. canFlip() diz se as folhas vão virar em 3D ali: com WebGL2 e sem movimento reduzido.

#Aparência

A opção appearance, e depois setAppearance, substituem o que o documento diz. O que ficar de fora mantém o valor do documento:

const book = createFolioFromDocument(container, doc, {
  appearance: {
    folio: {
      tilt: 22,
      paper: { type: 'bookWove', texture: 'laid' },
      binding: { type: 'hardcover', coverColor: { hex: '#5a1f1f', model: 'hex' } },
      surface: { type: 'walnut' },
      lighting: { environment: 'lamp' },
    },
    // Mesas fotografadas: uma pasta organizada como /folio/textures/ do postext.dev
    // (manifest.json e uma pasta por mesa). Sem ela, mapas procedurais.
    textureBaseUrl: '/folio/textures',
    // A imagem de `folio.binding.spineImage`: a URL do recurso,
    // ou um canvas ou uma imagem já desenhados.
    spineImage: spineUrl,
  },
});
 
// Um painel de configurações: o livro redesenhado no lugar, sem pintar nada de novo.
book.setAppearance({ folio: { ...folio, lighting: { environment: 'daylight' } } });
book.resetView();
CampoO que faz
folioAs configurações folio: inclinação, papel, encadernação, superfície, iluminação. Substitui as do documento quando informado.
pageWidthMmA largura de refile de uma página em mm, que dá a escala da espessura do papel e das capas. A partir do documento: a página refilada no seu dpi. Padrão 150 para createFolio.
extraPages: páginas do livro além das informadas, contadas na espessura dos blocos de páginas e nunca desenhadas.
covers: a primeira página informada é a capa da frente, a última, a capa de trás (quando cai em um verso). Elas viram como capas rígidas e nenhuma caixa de capa é desenhada. A partir do documento: binding.cover: 'pages' em um livro que começa na sua primeira página e termina na última.
spineImageA imagem impressa na lombada, como URL, canvas ou imagem. createFolioFromDocument não busca recursos: passe a imagem do recurso que folio.binding.spineImage nomeia. Ignorada em uma encadernação canoa (grampeada).
textureBaseUrlOnde são servidas as texturas fotografadas da mesa. Até elas carregarem, ou sem ela, a mesa é desenhada com mapas procedurais.

createFolio(container, { pages }) é o mesmo visualizador sobre quaisquer páginas: URLs de imagens, elementos <img> ou <canvas>, e "" para uma página em branco; uma página pode ser { src, alt, paper }, com paper um papel no estilo de :::paper para aquela folha. PageFlipper é só o motor em three.js, para um hospedeiro que monta o DOM das suas próprias páginas duplas; FlatPageFlipper é o virar de página plano anterior ao livro em 3D, mantido para a mesa de luz das Receitas. A lista completa de opções está no README do pacote.

#Exemplo ao vivo: um livro em 3D

O pen importa postext e postext-folio de uma CDN, diagrama um documento curto e o abre como um livro. Pegue a página da direita pela borda e arraste-a.

Postext · um documento como livro em 3D
import { buildDocument } from 'https://esm.sh/postext';
import { createFolioFromDocument } from 'https://esm.sh/postext-folio';
 
const paragraph = `The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved. The light it gave was small, but it was enough to find the step.`;
 
// Thirty-six short sections: about ten pages to turn.
const markdown = ['# The Lantern']
  .concat(Array.from({ length: 36 }, (_, i) => `## Evening ${i + 1}\n\n${paragraph} ${paragraph}\n\n${paragraph}`))
  .join('\n\n');
 
const config = {
  page: { sizePreset: '17x24', dpi: 150 },
  layout: { layoutType: 'double' },
  bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 10, unit: 'pt' } },
};
 
// Postext measures text with the fonts the browser has loaded,
// so wait for every face the document uses before laying it out.
await Promise.all([
  document.fonts.load('16px "EB Garamond"'),
  document.fonts.load('bold 16px "EB Garamond"'),
  document.fonts.load('italic 16px "EB Garamond"'),
  document.fonts.load('bold 16px "Open Sans"'), // the default heading face
]);
 
const doc = buildDocument({ markdown }, config);
const status = document.getElementById('status');
 
// The book: drag a page by its edge, click it, or use ← → and the buttons.
// Pages are painted at the size they are shown, around the open spread only.
createFolioFromDocument(document.getElementById('book'), doc, {
  onChange: ({ pages }) => {
    status.textContent = `${doc.pages.length} pages · open at ${pages.map((i) => i + 1).join('–')}`;
  },
});
status.textContent = `${doc.pages.length} pages · drag a page by its edge to turn it`;
index.html
<p id="status">Laying out…</p>
<div id="book"></div>
style.css
body {
  margin: 0;
  font-family: system-ui, sans-serif;
  color: #eee;
  background: radial-gradient(ellipse 70% 75% at 50% 42%, #272b34 0%, #1a1d23 58%, #121418 100%);
  min-height: 100vh;
}
#status {
  margin: 12px 16px 0;
  font-size: 14px;
  opacity: 0.8;
}
/* The viewer fits the book into its container: give it a height. */
#book {
  height: calc(100vh - 48px);
  --postext-folio-accent: #f0b35a;
}

Carrega um editor interativo de codepen.io. O exemplo importa a versão mais recente do postext de uma CDN.

#Exemplo ao vivo: imagens de página

createFolio com páginas desenhadas em canvas, uma última página em branco e a cor do papel.

Postext · um livro de imagens em 3D
import { createFolio } from 'https://esm.sh/postext-folio';
 
// Any pages will do: image URLs, <img> or <canvas> elements, and "" for a
// blank page. Here, eight pages drawn on canvases.
function drawPage(n) {
  const canvas = document.createElement('canvas');
  canvas.width = 600;
  canvas.height = 840;
  const ctx = canvas.getContext('2d');
  ctx.fillStyle = '#fbf8f1';
  ctx.fillRect(0, 0, 600, 840);
  ctx.fillStyle = `hsl(${n * 45} 45% 45%)`;
  ctx.fillRect(60, 80, 480, 320);
  ctx.fillStyle = '#222';
  ctx.font = 'bold 56px Georgia, serif';
  ctx.fillText(`Plate ${n}`, 60, 480);
  ctx.font = '22px Georgia, serif';
  for (let line = 0; line < 8; line++) ctx.fillRect(60, 530 + line * 30, line === 7 ? 260 : 480, 3);
  ctx.textAlign = 'center';
  ctx.fillText(String(n), 300, 800);
  return { src: canvas, alt: `Plate ${n}` };
}
 
const pages = Array.from({ length: 8 }, (_, i) => drawPage(i + 1));
// A blank page at the end, drawn as paper.
pages.push('');
 
const status = document.getElementById('status');
createFolio(document.getElementById('book'), {
  pages,
  firstPageRecto: true, // page 1 opens alone, on the right
  binding: 'left', // 'right' lays a right-to-left book mirrored
  paper: '#fbf8f1',
  onChange: (state) => {
    status.textContent = `Showing ${state.pages.map((i) => i + 1).join('–')} of ${pages.length}`;
  },
});
status.textContent = 'Drag a page by its edge, click it, or use ← →';
index.html
<p id="status">Drawing pages…</p>
<div id="book"></div>
style.css
body {
  margin: 0;
  font-family: system-ui, sans-serif;
  color: #eee;
  background: radial-gradient(ellipse 70% 75% at 50% 42%, #272b34 0%, #1a1d23 58%, #121418 100%);
  min-height: 100vh;
}
#status {
  margin: 12px 16px 0;
  font-size: 14px;
  opacity: 0.8;
}
/* The viewer fits the book into its container: give it a height. */
#book {
  height: calc(100vh - 48px);
  --postext-folio-accent: #f0b35a;
}

Carrega um editor interativo de codepen.io. O exemplo importa a versão mais recente do postext de uma CDN.

#Livros EPUB (postext-epub)

postext-epub grava um livro diagramado como arquivo EPUB 3.3, no navegador ou no Node, sem servidor. Ele lê os mesmos documentos de capítulo que renderToPdf recebe para um livro, de modo que números de página, notas, citações, referências cruzadas, o sumário e o índice remissivo chegam resolvidos, e devolve o arquivo como bytes. É o gerador por trás da aba EPUB 3 do Sandbox.

npm install postext postext-epub

postext é uma peer dependency, como no caso do postext-pdf: atualize os dois juntos e, em uma CDN, fixe os dois na mesma versão.

#Layout fixo e refluível

O EPUB 3 define duas apresentações, escolhidas pela propriedade rendition:layout do pacote; layout escolhe uma:

layout: 'fixed'layout: 'reflowable'
Nome no EPUBpre-paginated (layout fixo, FXL)reflowable, o padrão do EPUB
Documentos de conteúdoUm documento XHTML por página impressa, no tamanho da página refilada em px CSSUm documento XHTML por capítulo (uma parte abre um próprio)
O que mantémA página: colunas, flutuantes, cabeços, aberturas, quebras e posições de linha, nas fontes incorporadas. O texto continua sendo texto de verdade: selecionável, pesquisável, lido em voz altaO texto e a sua estrutura: títulos, parágrafos reconstruídos a partir das linhas, listas, boxes como asides, figuras e tabelas depois do texto que as cita, notas, links, marcadores de página impressa. Uma folha de estilo derivada da configuração
Do que abre mãoDa escolha do leitor de tipo, tamanho e margens; em uma tela pequena a página é reduzidaDas colunas, dos cabeços, do design de página e das quebras de linha exatas
Páginas duplas e direçãopage-spread-left / page-spread-right a partir da paridade e da encadernação; um livro com encadernação à direita é lido da direita para a esquerdaDireção de leitura a partir da encadernação; o chinês vertical mantém vertical-rl, o árabe é dir="rtl"
Indicado paraPáginas com design: livros ilustrados, livros didáticos, catálogos, revistas; telas grandesTexto corrido: romances, ensaios, relatórios; celulares e leitores de tinta eletrônica

As duas apresentações levam a mesma navegação: um sumário a partir dos títulos e das páginas de parte, uma lista de páginas com os rótulos de página impressos, landmarks (capa, sumário impresso, início do corpo do texto) e um NCX para sistemas de leitura mais antigos.

#Gravar um livro

import { openBundle, buildBundle } from 'postext';
import { renderToEpub } from 'postext-epub';
 
const bundle = await openBundle(fileBytes);
const docs = buildBundle(bundle); // um VDTDocument por capítulo, na ordem do livro
 
const bytes = await renderToEpub(docs, {
  layout: 'reflowable',
  metadata: { title: 'Lantern', creators: ['Ada Lovelace'], language: 'en' },
  fonts: bundle.fonts.map((f) => ({ family: f.family, weight: f.weight, style: f.style, bytes: new Uint8Array(f.bytes), format: f.format })),
  resourceBytes: (fileId) => {
    const data = bundle.files.get(fileId);
    return data ? { bytes: data, mediaType: '' } : undefined;
  },
  onWarning: (w) => console.warn(w),
});

Um documento único é um livro de um capítulo: renderToEpub([doc], options).

  • renderToEpub(docs, options): Promise<Uint8Array> grava o arquivo. options é { layout, metadata, fonts?, resourceBytes?, cover?, onProgress?, onWarning?, signal? }.
  • metadata: title e language (uma tag BCP 47) são obrigatórios; subtitle, creators, identifier, date, publisher, rights, description e modified são opcionais. Um ISBN sozinho vira urn:isbn:…. Sem um identifier, o livro recebe um urn:uuid: derivado do título, dos criadores e do idioma, para que uma nova versão do mesmo livro mantenha o seu lugar na biblioteca do leitor. Passe também modified para obter uma saída idêntica byte a byte.
  • fonts: as variantes a incorporar, { family, weight, style, bytes, format, unicodeRange? }, com format igual a woff2, woff, ttf ou otf. Cada variante vira um arquivo e uma regra @font-face; vários arquivos com o seu unicodeRange formam uma variante (as fatias do Google Fonts). Uma família, peso ou estilo que as páginas usam sem variante incorporada é informado como missingFont, e os sistemas de leitura o substituem pelos seus. Incorpore apenas fontes cuja licença permita isso.
  • resourceBytes(fileId): as imagens que as páginas posicionam, de forma síncrona ou assíncrona, como { bytes, mediaType }; um mediaType vazio é deduzido dos bytes. Entregue os bitmaps como estão armazenados e os SVGs como o seu código-fonte, não a matriz de impressão em PDF (svg.pdfFileId). Cada imagem é guardada uma única vez. Um livro em tinta única (diagramStyle.singleInk) tem os seus SVGs recoloridos no arquivo. Uma imagem sem bytes é informada como missingImage e fica como um quadro vazio.
  • cover: { bytes, mediaType, alt? }, uma imagem JPEG, PNG, WebP ou SVG. O livro então abre em um documento de capa que a contém, e ela é a cover-image do pacote (a miniatura na biblioteca). Sem ela, o layout fixo nomeia a primeira página como capa e o livro refluível fica sem imagem de capa.
  • onProgress({ phase, done, total }): resources (fontes e imagens), documents (páginas em um layout fixo, capítulos em um refluível) e, por fim, package. signal interrompe entre as etapas.
  • readEpub(bytes) lê um arquivo de volta para um visualizador, sem DOMParser: layout, metadados, direção de leitura, todos os arquivos por caminho, o manifesto, a spine, o sumário, a lista de páginas, o viewport do layout fixo e a capa. O leitor do Sandbox é construído sobre ele.

As duas apresentações levam metadados do EPUB Accessibility 1.1 (modos de acesso, recursos como o sumário e os números de página impressos, riscos e um resumo) e, por padrão, não declaram conformidade com as WCAG. A lista completa de opções e as limitações estão no README do pacote.

#Verificar um arquivo com o EPUBCheck

O W3C EPUBCheck é o validador de referência para EPUB; as lojas de e-books verificam com ele os arquivos que recebem. Com ele instalado (brew install epubcheck, ou a versão em Java), epubcheck book.epub lista erros, avisos e notas de uso; as notas de uso que a saída do Postext deixa (CSS-028, OBS-001, HTM_062) são apenas informativas. No repositório do Postext, pnpm --filter postext-epub epubcheck verifica os livros de exemplo do conjunto de testes, node packages/postext-epub/scripts/epubcheck.mjs book.postext --layout both diagrama um arquivo .postext ou uma pasta de predefinição e verifica as duas apresentações, e pnpm --filter postext-epub validate executa a matriz inteira de livros (o guia, as predefinições de vitrine, livros em chinês, em árabe e das Receitas), e cada um deles passa sem erros nem avisos.

#Pacotes (arquivos .postext)

Um arquivo .postext é um livro inteiro em um único arquivo: um arquivo zip com um manifesto preset.json, um arquivo Markdown por capítulo, os conteúdos dos recursos (bitmaps, SVGs, matrizes de impressão em PDF) e os arquivos das fontes que a configuração nomeia. O Sandbox o exporta e importa, e a skill para agentes o entrega. O pacote postext também consegue criá-lo e abri-lo, de modo que um livro pode passar entre essas ferramentas e o seu próprio programa sem perder nada.

my-book.postext
├── preset.json            manifest: name, locale, chapters, config, resources, fonts
├── chapters/01-dusk.md
├── chapters/02-night.md
├── resources/lantern.svg
└── fonts/ebgaramond-400-normal.woff2

O manifesto é descrito campo a campo no apêndice Formato do pacote de predefinição do Sandbox. Um arquivo também pode levar layouts.json, as contagens de páginas do Sandbox, para que o livro já abra paginado ali, ou um por edição de um livro multilíngue (layouts.zh-Hant.json, lido primeiro). openBundle os ignora.

A API é exportada pelo próprio postext e pelo subcaminho postext/bundle, que acrescenta as funções auxiliares de baixo nível. Importe de postext quando também for renderizar. Assim, os adaptadores de pacote e os renderizadores compartilham uma única instância do módulo, o que importa em uma CDN como o esm.sh, onde cada ponto de entrada é um build separado.

#Abrir um pacote

openBundle recebe os bytes do arquivo (um Uint8Array, um ArrayBuffer ou um Blob / File de um <input type="file">) e devolve tudo de que o motor e os seus renderizadores precisam:

import { openBundle } from 'postext';
 
const bundle = await openBundle(await file.arrayBuffer(), { locale: 'es' });
 
bundle.chapters;   // [{ title, file, markdown }, …] na ordem do livro
bundle.config;     // PostextConfig, pronto para buildDocument
bundle.resources;  // Resource[]
bundle.files;      // Map<path, Uint8Array>: todos os arquivos do pacote
CampoO que contém
manifestO preset.json validado.
id, name, descriptionDo manifesto.
locale, localesO idioma em que o conteúdo foi lido, e todos os idiomas que um pacote bilíngue traz. options.locale escolhe um: primeiro a tag exata, depois o idioma base e, por fim, o idioma do próprio pacote.
chapters{ title, file, markdown } por capítulo. Um capítulo sem título no manifesto recebe o texto do seu primeiro título #.
configA paleta de cores padrão e os tipos de recurso no idioma do pacote, depois o config do manifesto e, por fim, as substituições do idioma. O idioma do pacote é o locale acima, então um pacote em um único idioma recebe os seus próprios rótulos, seja qual for o options.locale pedido. Um manifesto que não nomeia idioma usa o que o seu config define (locale e, depois, o idioma de hifenização) ou, na falta dele, options.locale. customFonts lista as famílias de fontes do pacote. É a mesma configuração com que o Sandbox abre o pacote.
resourcesOs recursos, com as legendas do idioma escolhido. Um tamanho que falta no manifesto é lido do arquivo.
fontsUma entrada por variante: { family, weight, style, format, file, bytes }.
filesTodos os arquivos do zip, indexados pelo caminho.
thumbnail, canvasScopeO caminho da imagem de capa e o modo como o pacote pede para ser visto: o view do manifesto, com o localized[…].view do idioma servido por cima.
warningsProblemas não fatais: um arquivo de fonte sem suporte, uma matriz de impressão faltando.

O fileId de um conteúdo é o seu caminho dentro do pacote. resource.svg.fileId, resource.bitmap.fileId e o fileId de cada variante de customFonts podem ser buscados diretamente em bundle.files. openBundle lança um erro quando os bytes não são um zip, quando não há um preset.json válido (na raiz ou dentro de uma única pasta de primeiro nível) ou quando falta um arquivo que o manifesto nomeia.

Pacotes gravados pelo postext 1.4 ou anterior

Todo manifesto que createBundle e o Sandbox gravam leva configVersion: 8: as regras de configuração para as quais o seu config foi escrito. Um manifesto sem isso foi gravado pelo postext 1.4 ou anterior, que resolvia treze coisas de outra maneira:

  • Quebras de título (regras 3): até a 1.4, um objeto headings sem quebra de H1 não tinha nenhuma (veja Sobrescritas por nível).
  • O tamanho das fórmulas (regras 4): até a 1.4, as fórmulas saíam 1,131 vez maiores do que diz fontSizeScale (veja Tamanho das fórmulas).
  • O espaço abaixo de um recurso inline (regras 5): até a 1.4, o texto depois de uma figura ou tabela com placement.position: 'here' continuava na linha seguinte da grade, sem o espaço de flutuantes abaixo dela (veja layout.inlineResourceGap em Diagramação).
  • O espaço em volta de um recurso inline dentro de um boxe (regras 6): até a 1.4, esse recurso ficava encostado no texto do boxe que o envolve (veja layout.inlineResourceGapInBoxes em Diagramação).
  • Marcações inline nos títulos (regras 6): até a 1.4, um título imprimia as palavras do seu *italic*, **bold** e outras marcações no seu próprio estilo simples (veja headings.inlineMarks em Títulos).
  • O tamanho de uma capitular (regras 6): até a 1.4, o dropCap de um texto de design sem fontSize era tão alto quanto todas as caixas de linha que abrange, com o topo acima da primeira linha (veja dropCap em Elementos de texto).
  • O espaço abaixo de uma linha com dois-pontos (regras 6): até a 1.4, keepColonWithList considerava suficiente para a lista uma linha de espaço abaixo da linha terminada em dois-pontos, e um primeiro item de duas linhas que as regras de órfãs e viúvas mantêm inteiro passava para a coluna seguinte sem ela (veja bodyText.colonListRoom).
  • As linhas que o corte de um boxe deixa (regras 6): até a 1.4, um boxe que se dividia dentro de um parágrafo ou item de lista podia deixar uma linha dele de um lado, desde que cada lado do boxe tivesse as suas splitMinLines linhas no total (veja layout.boxChildSplitMinLines em Diagramação).
  • Quebras de linha em um travessão (regras 7): até a 1.4, o Knuth-Plass nunca terminava uma linha depois de um travessão ou meia-risca colados entre palavras (say—that’s), e o quebrador linha a linha do texto formatado só entre duas letras (veja bodyText.breakAfterDashes em Texto do corpo).
  • Texto alinhado à esquerda (regras 7): até a 1.4, o texto corrido em bandeira era composto linha a linha, preenchendo cada linha antes da seguinte, independentemente do que dissesse optimalLineBreaking (veja bodyText.optimalRagged em Texto do corpo).
  • A divisão abaixo de um título (regras 8): até a 1.4, o parágrafo abaixo de um título no pé de uma coluna mantinha ali tantas linhas quantas coubessem, por poucas que passassem para a coluna seguinte (veja headings.keepWithNextSplit em Títulos).
  • O espaço abaixo de um contêiner :::paragraphs (regras 8): até a 1.4, o espaço do estilo era aplicado abaixo do último parágrafo antes do ajuste à grade, o espaço acima do bloco seguinte (o marginTop de um título) era somado abaixo dele, e o espaçamento entre parágrafos do texto ficava de fora (veja bodyText.paragraphContainerSpacing em Texto do corpo).
  • Quebras de linha no hífen de uma palavra composta (regras 8): até a 1.4, o Knuth-Plass nunca terminava uma linha justificada depois de um hífen entre duas letras (well-known) em um parágrafo sem formatação inline, ao passo que o fazia em um parágrafo com formatação (veja bodyText.breakAfterHyphens em Texto do corpo).

openBundle e readBundle leem o config de um manifesto assim, e a configuração localized de cada idioma, por meio de migrateConfig, que grava explicitamente as quebras que a 1.4 compunha e multiplica a escala das fórmulas por 1,131 (com as margens de exibição em em divididas por esse fator). Um manifesto marcado de 3 a 7, gravado por uma pré-versão da 1.5, recebe apenas as fixações das regras posteriores à sua marca. Com 3, isso é o tamanho das fórmulas, o espaço inline, as cinco fixações das regras 6, as duas das regras 7 e as três das regras 8; com 4, o espaço inline e as fixações das regras 6, 7 e 8; com 5, as fixações das regras 6, 7 e 8; com 6, as das regras 7 e 8; com 7, só as das regras 8. A divisão abaixo de um título (pinLegacyHeadingSplit) é gravada como headings.keepWithNextSplit: 'fill' no headings que as camadas deixam em vigor, quando algum capítulo lido tem um título e a configuração não nomeia um valor próprio, mantém headings.keepWithNext ativado e não desativa bodyText.avoidOrphans. As quebras em compostos (pinLegacyHyphenBreaks) são gravadas como bodyText.breakAfterHyphens: false no bodyText em vigor, quando um capítulo lido tem um hífen entre duas letras e a configuração nem já o define nem desativa optimalLineBreaking. O espaço abaixo dos contêineres (pinLegacyParagraphContainerSpacing) é gravado como bodyText.paragraphContainerSpacing: 'add' no bodyText em vigor, quando a configuração declara um estilo de parágrafo (em paragraphStyles ou nas substituições do visualizador HTML), um capítulo lido abre um contêiner :::paragraphs em uma linha própria e a configuração ainda não o define. As quebras em travessões (pinLegacyDashBreaks) são gravadas como bodyText.breakAfterDashes: false no bodyText em vigor, quando um capítulo lido tem um travessão ou meia-risca colados entre palavras (antes dele uma letra, um algarismo ou uma pontuação de fechamento, e depois dele uma letra, um algarismo ou um parêntese ou aspa de abertura; aspas antes dele contam quando uma letra, um algarismo, uma pontuação de fechamento ou um espaço sem quebra vem antes delas, como em "no"—and, mas não em said "—Hola; uma marcação inline encostada no travessão ou nas aspas, como os ** de **riddles.**—I, conta dos dois lados) e a configuração ainda não o define. A quebra em bandeira (pinLegacyRaggedBreaking) é gravada como bodyText.optimalRagged: false no bodyText em vigor, quando a configuração deixa algum texto corrido em bandeira (um textAlign diferente de 'justify' no texto do corpo, em um estilo de parágrafo, no corpo de um boxe, no corpo de uma parte ou no corpo de um estilo de seção (headingStyles[].bodyStyle), ou nas substituições do visualizador HTML), ainda não o define e não desativa optimalLineBreaking. O espaço em boxes (pinLegacyBoxResourceGap) é gravado como layout.inlineResourceGapInBoxes: false no layout em vigor, quando um recurso está incorporado em uma linha própria dentro de um :::callout dos capítulos lidos e a configuração ainda não o define. O corte de boxes (pinLegacyBoxChildCut) é gravado como layout.boxChildSplitMinLines: 1 no layout em vigor, quando um capítulo lido abre um :::callout em uma linha própria e a configuração ainda não o define. As marcações dos títulos (pinLegacyHeadingMarks) são gravadas como headings.inlineMarks: false no headings que as camadas deixam em vigor, quando um título dos capítulos lidos tem uma marcação (*, _, ^, ~, :smallcaps[ ou um link no seu texto) e a configuração não nomeia um valor próprio. As capitulares (pinLegacyDropCapSize) têm o seu tamanho da 1.4 gravado explicitamente como dropCap.fontSize, onde quer que estejam: na unidade da entrelinha do elemento quando ela é um comprimento, senão na unidade do seu tamanho de fonte. O espaço da linha com dois-pontos (pinLegacyColonListRoom) é gravado como bodyText.colonListRoom: 'line' no bodyText em vigor, quando uma lista dos capítulos lidos vem depois de uma linha terminada em dois-pontos (com linhas em branco permitidas entre elas) e a configuração nem nomeia um espaço nem desativa keepColonWithList. O espaço inline (pinLegacyInlineGap) é gravado como layout.inlineResourceGap: 'above' no layout que as camadas deixam em vigor, quando uma linha dos capítulos lidos incorpora um recurso (::resource{id="…"} sozinho na sua linha, como o parser o lê: uma menção no texto corrido ou em um trecho de código não conta) e a configuração não nomeia um espaço próprio. O tamanho é fixado no math que as camadas deixam em vigor (o math próprio de um idioma substitui o compartilhado), e somente quando os capítulos lidos têm um $: um pacote sem fórmulas mantém o seu config como foi escrito. Quando nem o manifesto nem o idioma nomeiam um math, o que está em vigor é o baseConfig de readBundle (o do próprio leitor), e ele também é fixado, já que a 1.4 compunha as fórmulas do pacote nesse tamanho: um fontSizeScale: 1.5 de base é lido como 1,5 × 1,1312. As quebras de título da base são tomadas como estão. Assim, um pacote antigo mantém o que essas regras diagramavam, e bundle.config mostra as quebras, o tamanho das fórmulas, os espaços, as marcações dos títulos, os tamanhos das capitulares, o espaço da linha com dois-pontos, o corte de boxes, as quebras em travessões, as quebras em compostos, a quebra em bandeira, a divisão abaixo de um título e o espaço abaixo de contêineres com que ele é diagramado. As correções de layout da 1.5 não têm fixação e se aplicam a ele como a qualquer livro, então uma página que elas afetam ainda pode mudar (veja Tamanho das fórmulas para a lista). Um preset.json escrito à mão para as regras de hoje define "configVersion": 8; marcar com ele o manifesto de um pacote antigo é também o jeito de uma linha de lê-lo com as regras de hoje (um pacote sem versão perde então também a sua fixação das quebras de título).

import { CONFIG_VERSION, migrateConfig } from 'postext/bundle';
 
migrateConfig({ headings: { fontFamily: 'Georgia' } }, undefined, { content: 'A book with no maths.' });
// => { headings: { fontFamily: 'Georgia', levels: [{ level: 1, breakBefore: { enabled: false } }] } }
migrateConfig({ math: { fontSizeScale: 1.2 } }, 3);
// => { math: { fontSizeScale: 1.35746…, marginTop: { value: 0.7072, unit: 'em' }, marginBottom: { value: 0.7072, unit: 'em' } },
//      layout: { inlineResourceGap: 'above', inlineResourceGapInBoxes: false, boxChildSplitMinLines: 1 },
//      headings: { inlineMarks: false, keepWithNextSplit: 'fill' }, bodyText: { colonListRoom: 'line', breakAfterDashes: false, breakAfterHyphens: false } }
migrateConfig({ layout: { layoutType: 'single' } }, 4, { content: 'Text.\n\n::resource{id="fig"}' });
// => { layout: { layoutType: 'single', inlineResourceGap: 'above' } }
migrateConfig({ layout: { layoutType: 'single' } }, 5, { content: ':::callout\nText.\n\n::resource{id="fig"}\n:::' });
// => { layout: { layoutType: 'single', inlineResourceGapInBoxes: false, boxChildSplitMinLines: 1 } }
migrateConfig({ bodyText: { textAlign: 'left' } }, 6, { content: 'I say—that is all.' });
// => { bodyText: { textAlign: 'left', breakAfterDashes: false, optimalRagged: false } }
migrateConfig({ paragraphStyles: [{ id: 'verse' }] }, 7, { content: ':::paragraphs{style="verse"}\nA line.\n:::' });
// => { paragraphStyles: [{ id: 'verse' }], bodyText: { paragraphContainerSpacing: 'add' } }
migrateConfig({ bodyText: { fontFamily: 'Georgia' } }, 7, { content: 'A well-known tale.' });
// => { bodyText: { fontFamily: 'Georgia', breakAfterHyphens: false } }
migrateConfig(config, CONFIG_VERSION); // regras de hoje: o próprio `config`

content é o Markdown que a configuração diagrama (uma string ou uma lista de capítulos). Sem ele, o tamanho das fórmulas é fixado sempre que as fórmulas estão ativadas, o espaço abaixo dos contêineres sempre que a configuração declara um estilo de parágrafo, e os dois espaços, as marcações dos títulos, o espaço da linha com dois-pontos, o corte de boxes, as quebras em travessões, a divisão abaixo de um título e as quebras em compostos sempre, já que o motor não tem como saber se o livro tem uma fórmula, um contêiner :::paragraphs, uma figura inline, um título com marcações, uma lista introduzida por dois-pontos, um boxe, um travessão colado, um título ou uma palavra composta. A quebra em bandeira é fixada só pela configuração, com ou sem conteúdo. Migre uma configuração armazenada uma única vez e armazene-a de novo com CONFIG_VERSION: a fixação das fórmulas multiplica a escala, então uma configuração migrada duas vezes cresceria duas vezes.

#Diagramar e renderizar um pacote

Quatro funções auxiliares ligam um pacote aberto ao motor e aos renderizadores:

  • loadBundleFonts(bundle) registra as variantes do pacote em document.fonts. Aguarde-a antes de diagramar, porque o layout mede o texto com as fontes que o navegador tem. As famílias que o pacote nomeia mas não traz (Google Fonts) ainda precisam ser carregadas por você, como em qualquer outro documento.
  • registerBundleImages(bundle) decodifica as imagens para o renderizador de canvas (renderPage, renderToCanvas), pôsteres de vídeo incluídos. bundleImageUrl(bundle) é o resolvedor resourceImageUrl para renderToHtml, e bundleVideoUrl(bundle) o seu resolvedor resourceVideoUrl para os arquivos de vídeo que um pacote traz. Os dois recolorem as figuras SVG quando diagramStyle.singleInk está ativado, uma única vez: recolorem a marcação e marcam as imagens para que nenhum renderizador as tinja de novo (veja Tinta única no canvas e no HTML).
  • buildBundle(bundle) diagrama os capítulos em ordem e devolve um VDTDocument por capítulo. Cada capítulo continua o anterior: contadores de títulos e de recursos, a parte aberta, a paridade de página e a numeração de páginas. Um capítulo que imprime o sumário (:::toc) ou o índice remissivo (:::index) recebe a estrutura do livro inteiro. Aceita as mesmas opções que buildDocument, mais config para substituir a configuração do pacote, cache para compartilhar um cache de medição e metadata (veja abaixo).
  • bundleResourceBytes(bundle) e bundleFontProvider(bundle, { decodeWoff2, fallback }) são as opções resourceBytes e fontProvider do renderToPdf do postext-pdf. O provedor de fontes escolhe no pacote o peso mais próximo do estilo pedido. Para uma variante .woff2 ele precisa de decompressWoff2, e para uma família que o pacote não traz ele chama fallback com os argumentos do renderizador, request incluído, e repassa o que ele devolver. Um fallback que busca só o arquivo latin de uma família imprime como caixas vazias uma família chinesa que o pacote não incorpora; um que responde com fatias, como sliceFontProvider em Fontes chinesas, japonesas e coreanas, a imprime inteira.
import { openBundle, loadBundleFonts, registerBundleImages, buildBundle, renderPage,
  bundleResourceBytes, bundleFontProvider } from 'postext';
import { renderToPdf, decompressWoff2 } from 'postext-pdf';
 
const bundle = await openBundle(bytes);
await loadBundleFonts(bundle);
await registerBundleImages(bundle);
 
const docs = buildBundle(bundle);                        // um VDTDocument por capítulo
const firstPage = renderPage(docs[0].pages[0], docs[0]); // um <canvas>
 
const pdf = await renderToPdf(docs, {                    // o livro inteiro
  fontProvider: bundleFontProvider(bundle, { decodeWoff2: decompressWoff2, fallback: fontsource }),
  resourceBytes: bundleResourceBytes(bundle),
});

Para diagramar você mesmo um único capítulo, passe bundle.chapters[i].markdown, bundle.resources e bundle.config para buildDocument, como faria com qualquer documento.

Os metadados do livro. Como no Sandbox, o front matter do primeiro capítulo é o do livro: buildBundle entrega o seu title, o seu author e o resto a todos os capítulos, de modo que os cabeços {title} e {author} se mantêm em todas as páginas e o doc.metadata de cada capítulo os traz. Um bloco de front matter no início de um capítulo posterior é ignorado: ele é encontrado pelas suas linhas --- e apagado sem ser analisado, então um YAML que o parser rejeitaria não causa dano. options.metadata fornece os valores que o front matter não define (o front matter prevalece). A contagem de páginas do livro também chega a todos os capítulos: {bookTotalPages} a imprime, enquanto {totalPages} conta as do capítulo (veja Contagem de páginas do livro).

const docs = buildBundle(bundle, { metadata: { author: 'A. Author' } });
docs[3].metadata.title;   // o `title:` do primeiro capítulo

#Exemplo ao vivo: abrir um pacote

O pen carrega do repositório um livro de exemplo de dois capítulos (lantern.postext, com tipo próprio, uma figura SVG e uma tabela). Ele registra as fontes e as imagens do pacote, diagrama o livro com buildBundle e pinta todas as páginas. Make the PDF renderiza os mesmos documentos com o postext-pdf, incorporando as fontes do pacote. Escolha um arquivo .postext seu, exportado do Sandbox, por exemplo, para vê-lo do mesmo jeito.

Postext · abrir um pacote .postext
import {
  openBundle,
  loadBundleFonts,
  registerBundleImages,
  buildBundle,
  bundleResourceBytes,
  bundleFontProvider,
  renderPage,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';
 
// A two-chapter book with its own typeface, an SVG figure and a table.
const SAMPLE = 'https://cdn.jsdelivr.net/gh/drnachio/postext@main/docs/examples/open-bundle/lantern.postext';
 
const status = document.getElementById('status');
const pdfButton = document.getElementById('pdf');
let current = null;
 
async function show(data) {
  // Chapters, config (fonts wired to the bundle's own files), resources and
  // every file, keyed by its path inside the bundle.
  const bundle = await openBundle(data);
 
  // Layout measures text with the fonts the browser has: register the
  // bundle's faces, and load the Google Fonts it names but does not carry
  // (the default running heads use Open Sans; see the pen's CSS).
  await loadBundleFonts(bundle);
  await document.fonts.load('600 16px "Open Sans"');
  await registerBundleImages(bundle);
 
  // One VDTDocument per chapter, each continuing the one before it.
  const docs = buildBundle(bundle);
  const pages = docs.flatMap((doc) => doc.pages.map((page) => renderPage(page, doc)));
  document.getElementById('pages').replaceChildren(...pages);
  status.textContent = `${bundle.name} · ${bundle.chapters.length} chapter(s) · ${pages.length} page(s)`
    + (bundle.warnings.length ? ` · ${bundle.warnings.length} warning(s)` : '');
  current = { bundle, docs };
  pdfButton.disabled = false;
  document.getElementById('links').hidden = true;
}
 
// Fonts the bundle does not carry come from Fontsource.
async function fontsource(family, weight, style) {
  const id = family.toLowerCase().replace(/\s+/g, '-');
  const res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@latest/files/${id}-latin-${weight}-${style}.woff2`);
  if (!res.ok) throw new Error(`font fetch failed: ${res.status} ${family}`);
  return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
}
 
pdfButton.addEventListener('click', async () => {
  pdfButton.disabled = true;
  status.textContent = 'Rendering the PDF…';
  const { bundle, docs } = current;
  const bytes = await renderToPdf(docs, {
    fontProvider: bundleFontProvider(bundle, { decodeWoff2: decompressWoff2, fallback: fontsource }),
    resourceBytes: bundleResourceBytes(bundle),
  });
  const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' }));
  document.getElementById('open').href = url;
  document.getElementById('download').href = url;
  document.getElementById('links').hidden = false;
  status.textContent = `${bundle.name} · ${(bytes.length / 1024).toFixed(0)} KB PDF`;
  pdfButton.disabled = false;
});
 
document.getElementById('file').addEventListener('change', async (event) => {
  const file = event.target.files[0];
  if (!file) return;
  status.textContent = `Opening ${file.name}…`;
  await show(file).catch((err) => { status.textContent = `Could not open ${file.name}: ${err.message}`; });
});
 
const res = await fetch(SAMPLE);
await show(await res.arrayBuffer());
index.html
<p>
  <label>Open a .postext file: <input id="file" type="file" accept=".postext,application/zip"></label>
  <button id="pdf" disabled>Make the PDF</button>
  <span id="links" hidden>
    <a id="open" target="_blank" rel="noopener">open it</a> ·
    <a id="download" download="book.pdf">download it</a>
  </span>
</p>
<p id="status">Loading the sample book…</p>
<div id="pages"></div>
style.css
body {
  margin: 16px;
  font-family: system-ui, sans-serif;
  background: #e8e8e8;
}
#pages {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: flex-start;
}
#pages canvas {
  display: block;
  width: 240px;
  height: auto;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

Carrega um editor interativo de codepen.io. O exemplo importa a versão mais recente do postext de uma CDN.

#Criar um pacote

createBundle grava um arquivo .postext a partir de um documento: os seus capítulos, a configuração, os recursos e os conteúdos a que eles fazem referência.

import { createBundle } from 'postext';
 
const { bytes, manifest, warnings } = await createBundle({
  name: 'The Lantern',
  locale: 'en',
  chapters: [
    { markdown: '# Dusk\n\nIt is drawn in :ref{id="lantern"}.' },
    { title: 'Night', markdown: '# Night\n\n…' },
  ],
  config,
  resources: [{
    id: 'lantern', typeId: 'figure', kind: 'svg', caption: 'The lantern.',
    svg: { fileId: 'lantern.svg', width: 240, height: 150 },
    createdAt: 0, updatedAt: 0,
  }],
  files: { 'lantern.svg': svgMarkup, 'garamond-regular': fontBytes },
});
EntradaSignificado
name, id, description, localeOs metadados do manifesto. id assume por padrão um slug de name.
chapters ou markdownO livro, um { title?, markdown } por capítulo, ou um documento único.
configO PostextConfig. Os valores iguais aos padrões ficam fora do manifesto.
resourcesOs recursos. Uma imagem nomeia o seu conteúdo por bitmap.fileId / svg.fileId (e svg.pdfFileId para uma matriz de impressão).
filesOs conteúdos por fileId (um objeto ou um Map): as imagens a que os recursos fazem referência e os arquivos de fonte a que as variantes de config.customFonts fazem referência. Os valores podem ser um Uint8Array, um ArrayBuffer, um Blob ou uma string (marcação SVG).
thumbnail{ data, mime }: uma imagem de capa (PNG, JPEG, WebP, GIF ou SVG).
canvasScope'book' pede aos visualizadores que diagramem o livro inteiro como um único canvas.
mtimeA data de modificação gravada em todos os arquivos do zip (um Date, um timestamp ou uma string de data). Se omitida, é o momento da chamada, então duas chamadas com a mesma entrada geram bytes diferentes. Passe uma data fixa e a mesma entrada gera os mesmos bytes, que você pode comparar ou calcular o hash. Um zip guarda data e hora sem fuso horário, em passos de dois segundos, de 1980 a 2099, e a data é gravada no horário local da máquina. Para bytes que coincidam em qualquer máquina, construa a data a partir de campos locais, como new Date(1980, 0, 1): um timestamp ou uma string terminada em Z designa um instante, que cai em um horário local diferente em cada fuso ('1980-01-01T00:00:00Z' ainda é 1979 a oeste de UTC). Uma data fora desses anos, em horário local, lança um erro.
localizedMais idiomas do mesmo livro, por tag de idioma: { es: { chapters?, config?, resources? } }. As entradas acima passam então a ser o conteúdo de locale, que se torna obrigatório. Veja Pacotes bilíngues.

Ela devolve os bytes do zip, o manifest gravado como preset.json, todos os arquivos como files (caminho → bytes) e uma lista de warnings. Os arquivos recebem o nome do id do seu recurso (resources/lantern.svg) ou do nome do arquivo de fonte (fonts/…), e os capítulos, o da sua ordem e do seu título (chapters/01-dusk.md). As fontes são declaradas no fonts do manifesto, nunca dentro de config.customFonts. Algumas coisas ficam de fora, cada uma com um aviso:

  • um recurso ou uma variante de fonte cujo conteúdo não está em files
  • uma variante .woff (o renderizador de PDF não consegue incorporá-la)
  • uma família marcada com redistributable: false

No navegador, entregue bytes a um link de download: URL.createObjectURL(new Blob([bytes], { type: 'application/zip' })). No Node, grave-os com fs.writeFile. createBundle e openBundle não precisam de DOM. Os dists usam caminhos de módulo sem extensão, então no Node puro, sem bundler, eles precisam de um hook de resolução. O arquivo docs/examples/open-bundle/build-sample.mjs do repositório mostra um em poucas linhas.

#Pacotes bilíngues

Um arquivo .postext pode levar um livro em vários idiomas, e openBundle(bytes, { locale }) o lê em qualquer um deles. createBundle grava um a partir de localized: uma entrada por idioma extra, cada uma com o que difere do conteúdo principal (a entrada locale):

const { bytes, manifest } = await createBundle({
  name: 'The Lantern',
  locale: 'en',
  chapters: [{ markdown: '# Dusk\n\n…' }, { markdown: '# Night\n\n…' }],
  config,
  resources: [lanternFigure, hoursTable],
  files: { 'lantern.svg': svgEn, 'lantern-es.svg': svgEs },
  localized: {
    es: {
      chapters: [{ markdown: '# Anochecer\n\n…' }, { markdown: '# Noche\n\n…' }],
      config: { headings: { levels: [{ level: 1, numberingTemplate: 'Capítulo {1}' }] } },
      resources: [
        { id: 'lantern', caption: 'El farol.', svg: { fileId: 'lantern-es.svg', width: 240, height: 150 } },
        { id: 'hours', caption: 'Horas de luz.' },
      ],
    },
  },
});
 
const es = await openBundle(bytes, { locale: 'es' });   // capítulos, configuração e legendas em espanhol
  • chapters: o livro naquele idioma. Os arquivos de capítulo vão para uma pasta por idioma (chapters/en/01-dusk.md, chapters/es/01-anochecer.md) e o chapters do manifesto vira um mapa idioma → capítulos. Um idioma sem chapters lê os principais; quando nenhum idioma tem capítulos próprios, eles continuam sendo uma única lista.
  • config: a configuração para aquele idioma. Cada chave de primeiro nível substitui por inteiro a chave compartilhada quando o pacote é lido naquele idioma, então o headings acima substitui o objeto headings inteiro. As chaves omitidas, ou iguais às compartilhadas, são compartilhadas e não são gravadas, então passar a configuração completa do idioma funciona tão bem quanto passar só as poucas chaves que mudam. Uma chave definida com os seus padrões enquanto a compartilhada não está (layout: {}) é gravada como foi dada, de modo que redefine o valor compartilhado. As fontes são compartilhadas: as famílias do customFonts de um idioma se juntam ao fonts do pacote.
  • resources: o texto dos recursos compartilhados, associados pelo id: caption, note, altText e a table de uma tabela. Uma imagem com palavras pode ter a sua própria arte: bitmap.fileId ou svg.fileId (e svg.pdfFileId) nomeiam outro conteúdo em files, gravado como resources/es/lantern.svg. Os outros campos, como o tipo ou o posicionamento, são compartilhados. Um id que não está entre os resources fica de fora com um aviso, e uma imagem de idioma que falta deixa o idioma com a compartilhada, também com um aviso.

O manifesto lista todos os idiomas em locales (['en', 'es']), mantém o principal como locale e guarda o resto em localized. openBundle sem idioma lê o idioma principal.

Que idioma o leitor recebe. openBundle(bytes, { locale }) serve o idioma exato; na falta dele, o idioma base (es-MX lê es); e, na falta deste, o principal. bundle.locale diz qual foi servido. Os capítulos e o texto vêm sempre do mesmo idioma. O idioma principal mantém o texto compartilhado mesmo quando localized traz uma variante regional dele: um pacote pt-PT com uma entrada pt-BR lê as legendas brasileiras só para pt-BR, e as compartilhadas para pt-PT e pt.

#Exemplo ao vivo: criar um pacote

O pen constrói um livro de dois capítulos com uma figura SVG e lista os arquivos que createBundle gravou, junto com o manifesto. Ele oferece o zip para download e depois o abre de novo com openBundle e pinta a primeira página: o percurso completo em poucas linhas. Importe o arquivo baixado no Sandbox para continuar trabalhando nele lá.

Postext · criar um pacote .postext
import { createBundle, openBundle, registerBundleImages, buildBundle, renderPage } from 'https://esm.sh/postext';
 
// A picture resource names its payload by fileId; the bytes (here, SVG
// markup) go in `files` under that same id.
const lanternSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 150">
  <rect width="240" height="150" fill="#f3efe6"/>
  <path d="M100 36 h40 l8 14 h-56 z" fill="#2f3e46"/>
  <rect x="98" y="50" width="44" height="58" rx="4" fill="#f6c453" stroke="#2f3e46" stroke-width="4"/>
  <circle cx="120" cy="79" r="11" fill="#fff4c2"/>
  <path d="M94 108 h52 l-6 12 h-40 z" fill="#2f3e46"/>
</svg>`;
 
const resources = [{
  id: 'lantern',
  typeId: 'figure',
  kind: 'svg',
  caption: 'The lantern by the door.',
  svg: { fileId: 'lantern.svg', width: 240, height: 150 },
  createdAt: 0,
  updatedAt: 0,
}];
 
const text = 'The lantern hung from a nail by the door, and every evening someone lit it. Nobody remembered who had put the nail there, or why the lantern was never moved.';
 
// One entry per chapter; a chapter without a title takes its first # heading.
const chapters = [
  { markdown: `# Dusk\n\n${text} It is drawn in :ref{id="lantern"}.\n\n${text}\n\n${text}` },
  { markdown: `# Night\n\n${text}\n\n${text}` },
];
 
const config = {
  layout: { layoutType: 'double' },
  // Two short chapters that run on, with no blank verso between them (an
  // H1 otherwise opens on a fresh recto), as in the open-bundle sample.
  headings: { levels: [{ level: 1, numberingTemplate: 'Chapter {1}', breakBefore: { enabled: false } }] },
};
 
// Everything a .postext file holds: manifest, chapters, resources, fonts.
const { bytes, manifest, files, warnings } = await createBundle({
  name: 'The Lantern',
  locale: 'en',
  chapters,
  config,
  resources,
  files: { 'lantern.svg': lanternSvg },
});
if (warnings.length) console.warn(warnings);
 
const url = URL.createObjectURL(new Blob([bytes], { type: 'application/zip' }));
document.getElementById('download').href = url;
document.getElementById('actions').hidden = false;
document.getElementById('files').replaceChildren(...Object.entries(files).map(([path, data]) => {
  const li = document.createElement('li');
  li.textContent = `${path} (${data.length} B)`;
  return li;
}));
document.getElementById('manifest').textContent = JSON.stringify(manifest, null, 2);
 
// Round trip: open the file just written, the way any program would.
await Promise.all([
  document.fonts.load('16px "EB Garamond"'),
  document.fonts.load('bold 16px "EB Garamond"'),
  document.fonts.load('bold 16px "Open Sans"'), // the default heading face
]);
const bundle = await openBundle(bytes);
await registerBundleImages(bundle);
const [firstChapter] = buildBundle(bundle);
document.getElementById('page').replaceChildren(renderPage(firstChapter.pages[0], firstChapter));
document.getElementById('status').textContent =
  `${bundle.name}: ${bundle.chapters.length} chapters, ${(bytes.length / 1024).toFixed(1)} KB`;
index.html
<p id="status">Building the bundle…</p>
<p id="actions" hidden>
  <a id="download" download="lantern.postext">Download lantern.postext</a> ·
  <a href="https://postext.dev/en/sandbox" target="_blank" rel="noopener">open the Sandbox</a> and import it (Projects → New → Import .postext…)
</p>
<div id="output">
  <section>
    <h3>Files in the bundle</h3>
    <ul id="files"></ul>
    <h3>preset.json</h3>
    <pre id="manifest"></pre>
  </section>
  <section>
    <h3>Opened again: page 1</h3>
    <div id="page"></div>
  </section>
</div>
style.css
body {
  margin: 16px;
  font-family: system-ui, sans-serif;
  background: #e8e8e8;
}
#output {
  display: flex;
  flex-wrap: wrap;
  gap: 24px;
  align-items: flex-start;
}
#output section {
  flex: 1 1 280px;
  min-width: 0;
}
h3 {
  margin: 8px 0;
  font-size: 14px;
}
ul {
  margin: 0;
  padding-left: 20px;
  font-family: ui-monospace, monospace;
  font-size: 13px;
}
pre {
  max-height: 320px;
  overflow: auto;
  padding: 8px;
  background: #fff;
  font-size: 12px;
}
#page canvas {
  display: block;
  max-width: 100%;
  height: auto;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

Carrega um editor interativo de codepen.io. O exemplo importa a versão mais recente do postext de uma CDN.

#Trabalhar com pacotes

Como o Sandbox, a skill para agentes e o pacote postext leem e gravam o mesmo arquivo, um arquivo .postext é uma forma prática de passar um livro de uma ferramenta para outra:

  • Comece por um pacote. Porte uma publicação existente com a skill para agentes, ou faça o design de um livro no Sandbox e exporte-o (Baixar (.postext) no menu ⋯ da linha do livro no painel Livros). Carregue o arquivo no seu programa com openBundle para renderizá-lo em canvas, HTML ou PDF. Mantenha o arquivo como fonte do livro: edite os capítulos, a configuração ou os recursos no código e grave-o de volta com createBundle, ou simplesmente recarregue-o sempre que ele mudar.
  • Depure e faça o ajuste fino no Sandbox. Quando algo na saída do seu programa precisa de ajuste (uma figura que cai na página errada, um estilo de título, o equilíbrio das colunas), exporte com createBundle o que o seu programa diagrama. Importe esse arquivo no Sandbox (Livros → Novo → Abrir um arquivo .postext…), corrija o texto, o design ou as figuras com a visualização ao vivo, o painel Verificações e a vista PDF, e exporte-o de novo. O seu programa então carrega o arquivo corrigido com openBundle. Ou copie o que mudou de volta para o seu código: o config do manifesto contém só os valores que diferem dos padrões, então ele se lê como um diff curto.

#API de baixo nível

postext/bundle também exporta as peças sobre as quais openBundle e createBundle são construídas, para aplicações que guardam ou servem pacotes do seu próprio jeito (um diretório descompactado servido por HTTP, registros em um banco de dados):

  • openBundleZip(bytes) / zipBundle(files, { mtime }): a camada do arquivo compactado. Na abertura, tolera uma pasta raiz e ignora as entradas __MACOSX e os arquivos ocultos. Caminhos que saem do pacote são recusados. mtime data os arquivos como o campo de mesmo nome na entrada de createBundle.
  • readBundle(manifest, readFile, options) lê um manifesto e uma função readFile(path) e devolve capítulos, configuração, recursos, imagens e fontes. options define o idioma, como os identificadores de arquivo são nomeados (ids), a configuração base (baseConfig, por baixo da do manifesto; por padrão, a paleta e os tipos de recurso de bundleBaseConfig no idioma do pacote, que resolveBundleConfigLocale(manifest, locale) devolve, e uma aplicação que passe a sua própria baseConfig deve localizá-la para esse idioma; com um manifesto anterior a configVersion: 4, o seu math é fixado com o do pacote; com um anterior a 5, o espaço em linha do seu layout; com um anterior a 6, o espaço nos boxes do seu layout, a folga depois de dois-pontos do seu bodyText, as marcas em linha dos seus headings e o tamanho das suas capitulares; com um anterior a 7, as quebras depois de travessão e a composição em bandeira do seu bodyText; e com um anterior a 8, a divisão logo abaixo de um título dos seus headings e as quebras depois do hífen de palavras compostas e o espaço sob os contêineres :::paragraphs do seu bodyText; veja Pacotes gravados pelo postext 1.4 ou anterior) e como os tamanhos intrínsecos são medidos.
  • planBundle(meta, content) / resolveBundleFiles(plan, sources): o lado da gravação, dividido em um plano puro (nomes de arquivo e manifesto) e na resolução dos bytes por meio das funções readBlob / readFont.
  • isBundleManifest(value), as funções que escolhem o idioma (pickChapterSpecs, pickLocaleOverrides, pickBundleView, resolveBundleLocale, resolveBundleConfigLocale), svgSize / bitmapSize e os tipos do formato (BundleManifest, BundleResourceSpec, BundleFontFamilySpec, …).
  • CONFIG_VERSION, migrateConfig(config, configVersion, { content }), pinLegacyHeadingBreaks(config), pinLegacyMathSize(config), pinLegacyInlineGap(config), pinLegacyBoxResourceGap(config), pinLegacyHeadingMarks(config), pinLegacyDropCapSize(config), pinLegacyColonListRoom(config), pinLegacyBoxChildCut(config), pinLegacyDashBreaks(config), pinLegacyRaggedBreaking(config), pinLegacyHeadingSplit(config), pinLegacyParagraphContainerSpacing(config), pinLegacyHyphenBreaks(config) e LEGACY_MATH_SIZE (0,5 ÷ 0,442): uma configuração guardada, expressa nos termos atuais (veja Pacotes gravados pelo postext 1.4 ou anterior). readBundle aplica isso; uma aplicação que guarda configurações do seu próprio jeito também pode aplicar, uma vez por cópia guardada.

O Sandbox é construído sobre essas peças. Ele acrescenta os seus próprios identificadores de armazenamento e os registros de páginas de layouts.json.