Capitolo 3.19 · Parte II · Il mestiere
Uso programmatico: pacchetti .postext
Aprire, impaginare, disegnare e creare file .postext dal codice, i pacchetti bilingui, i file delle versioni precedenti e l’API di basso livello
In breve
Questa pagina è per chi scrive codice. Un file .postext contiene un libro intero in un solo file: il testo, le impostazioni, i font e le immagini. La pagina mostra come aprire un file di questo tipo, impaginarlo e disegnarne le pagine. Mostra come creare un file nuovo dal tuo testo e come tenere due lingue di un libro in un solo file. Spiega anche come vengono letti i file creati da versioni precedenti di Postext.
#Pacchetti (file .postext)
Un file .postext è un libro intero in un solo file: un archivio zip che contiene un manifest preset.json, un file markdown per capitolo, i contenuti delle risorse (bitmap, SVG, master di stampa PDF) e i file dei font che la configurazione nomina. La Sandbox lo esporta e lo importa e la skill per agenti lo consegna. Anche il pacchetto postext può crearlo e aprirlo, così un libro può passare tra questi strumenti e il tuo programma senza perdere nulla.
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
Il manifest è descritto campo per campo nell’appendice Formato dei pacchetti di preset della Sandbox. Un file può contenere anche layouts.json, il numero di pagine calcolato dalla Sandbox, così lì il libro si apre già impaginato, oppure uno per ogni edizione di un libro multilingue (layouts.zh-Hant.json, letto per primo). openBundle li ignora.
L’API è esportata da postext stesso e dal sottopercorso postext/bundle, che aggiunge le funzioni di basso livello. Importa da postext quando devi anche disegnare. In questo modo gli adattatori dei pacchetti e i renderer condividono un’unica istanza del modulo, il che conta su una CDN come esm.sh, dove ogni punto di ingresso è una build separata.
#Aprire un pacchetto
openBundle riceve i byte del file (un Uint8Array, un ArrayBuffer, oppure un Blob / File da un <input type="file">) e restituisce tutto ciò di cui hanno bisogno il motore e i suoi backend:
import { openBundle } from 'postext';
const bundle = await openBundle(await file.arrayBuffer(), { locale: 'es' });
bundle.chapters; // [{ title, file, markdown }, …] in book order
bundle.config; // PostextConfig, ready for buildDocument
bundle.resources; // Resource[]
bundle.files; // Map<path, Uint8Array>: every file of the bundle| Campo | Che cosa contiene |
|---|---|
manifest | Il preset.json convalidato. |
id, name, description | Presi dal manifest. |
locale, locales | La lingua in cui è stato letto il contenuto, e ogni lingua che un pacchetto bilingue contiene. options.locale ne sceglie una: prima il tag esatto, poi la lingua di base, poi la lingua propria del pacchetto. |
chapters | { title, file, markdown } per capitolo. Un capitolo senza titolo nel manifest prende il testo del suo primo titolo #. |
config | La tavolozza di colori predefinita e i tipi di risorsa nella lingua del pacchetto, poi il config del manifest, poi le sostituzioni della lingua. La lingua del pacchetto è il locale qui sopra, quindi un pacchetto in una sola lingua riceve le proprie etichette qualunque cosa chieda options.locale. Un manifest che non indica nessuna lingua prende quella impostata dal suo config (locale, poi la lingua di sillabazione), altrimenti options.locale. customFonts elenca le famiglie di font del pacchetto. È la stessa configurazione con cui la Sandbox apre il pacchetto. |
resources | Le risorse, con le didascalie della lingua scelta. Una dimensione assente dal manifest viene letta dal file. |
fonts | Una voce per font: { family, weight, style, format, file, bytes }. |
files | Ogni file dell’archivio, indicizzato per percorso. |
thumbnail, canvasScope | Il percorso dell’immagine di copertina, e come il pacchetto chiede di essere visualizzato: il view del manifest, con sopra il localized[…].view della lingua servita. |
start | Il punto in cui inizia il libro, per un pacchetto che contiene una parte di un libro più lungo: lo start del manifest, oppure il localized[…].start della lingua servita. Assente per un libro che inizia dal suo principio. |
warnings | Problemi non bloccanti: un file di font non supportato, un master di stampa mancante. |
Il fileId di un contenuto è il suo percorso dentro il pacchetto. resource.svg.fileId, resource.bitmap.fileId e il fileId di ogni variante di customFonts si possono cercare direttamente in bundle.files. openBundle genera un errore quando i byte non sono uno zip, quando non c’è un preset.json valido (alla radice o sotto un’unica cartella di primo livello), o quando manca un file che il manifest nomina.
Pacchetti scritti da postext 1.4 o precedenti
Ogni manifest scritto da createBundle e dalla Sandbox porta configVersion: 14: le regole di configurazione per cui è stato scritto il suo config. Un manifest senza questo campo è stato scritto da postext 1.4 o da una versione precedente, che impostava diciotto cose in modo diverso:
- Interruzioni prima dei titoli (regole 3): fino alla 1.4, un oggetto
headingssenza interruzione per l’H1 non ne aveva nessuna (vedi Sostituzioni per livello). - La dimensione delle formule (regole 4): fino alla 1.4, le formule risultavano 1,131 volte più grandi di quanto indica
fontSizeScale(vedi Dimensione delle formule). - Lo spazio sotto una risorsa in linea (regole 5): fino alla 1.4, il testo dopo una figura o una tabella
placement.position: 'here'riprendeva alla linea successiva della griglia, senza la distanza degli elementi flottanti sotto di essa (vedilayout.inlineResourceGapin Impaginazione). - Lo spazio intorno a una risorsa in linea dentro un riquadro (regole 6): fino alla 1.4, una risorsa del genere stava a contatto con il testo del riquadro che la conteneva (vedi
layout.inlineResourceGapInBoxesin Impaginazione). - Stili in linea nei titoli (regole 6): fino alla 1.4, un titolo stampava le parole dei suoi segni
*italic*,**bold**e altri nel proprio stile semplice (vediheadings.inlineMarksin Titoli). - La dimensione di un capolettera (regole 6): fino alla 1.4, il
dropCapdi un testo di design senzafontSizeera alto quanto tutti i riquadri di riga che occupa, con la sommità sopra la prima riga (vedidropCapin Elementi di testo). - Lo spazio sotto una riga che termina con i due punti (regole 6): fino alla 1.4,
keepColonWithListconsiderava sufficiente per l’elenco una riga di spazio sotto la riga che termina con i due punti, e una prima voce di due righe che le regole di orfane e vedove tengono unita passava alla colonna successiva senza di essa (vedibodyText.colonListRoom). - Le righe che lascia il taglio di un riquadro (regole 6): fino alla 1.4, un riquadro diviso dentro un paragrafo o una voce di elenco poteva lasciarne una sola riga su un lato, purché ciascun lato del riquadro contenesse in tutto le sue
splitMinLinesrighe (vedilayout.boxChildSplitMinLinesin Impaginazione). - A capo dopo una lineetta (regole 7): fino alla 1.4, Knuth–Plass non terminava mai una riga dopo una lineetta lunga o media composta senza spazi tra due parole (
say—that’s), e il divisore riga per riga del testo formattato lo faceva solo tra due lettere (vedibodyText.breakAfterDashesin Testo corrente). - Testo in bandiera (regole 7): fino alla 1.4, il testo corrente in bandiera veniva composto riga per riga, riempiendo ogni riga prima della successiva, qualunque cosa dicesse
optimalLineBreaking(vedibodyText.optimalRaggedin Testo corrente). - La divisione sotto un titolo (regole 8): fino alla 1.4, il paragrafo sotto un titolo al piede di una colonna teneva lì tutte le righe che ci stavano, per quanto poche ne passassero alla colonna successiva (vedi
headings.keepWithNextSplitin Titoli). - Lo spazio sotto un contenitore
:::paragraphs(regole 8): fino alla 1.4, lo spazio dello stile veniva messo sotto l’ultimo paragrafo prima dell’aggancio alla griglia, lo spazio superiore del blocco successivo (ilmarginTopdi un titolo) veniva aggiunto sotto di esso, e la spaziatura tra paragrafi del testo veniva tralasciata (vedibodyText.paragraphContainerSpacingin Testo corrente). - A capo dopo il trattino di una parola composta (regole 8): fino alla 1.4, Knuth–Plass non terminava mai una riga giustificata dopo un trattino tra due lettere (
well-known) in un paragrafo senza formattazione in linea, mentre lo faceva in uno con formattazione (vedibodyText.breakAfterHyphensin Testo corrente). - Poesie senza separatore (regole 9): fino alla 1.22, una poesia
:::versele cui righe non contenevano||veniva composta come emistichi singoli, ogni riga centrata (vedibodyText.verse.layoutin Versi). - Un rientro di prima riga accanto a un rientro sporgente (regole 9): fino alla 1.22, l’
hangingIndentdi uno stile di paragrafo sostituiva il suofirstLineIndent, e la prima riga iniziava aindent(vedi Stili di paragrafo). - Una barra rovesciata a fine riga (regole 9): fino alla 1.22, una barra rovesciata che terminava una riga di un paragrafo, di una citazione o di una voce di elenco, e
\\prima di uno spazio, venivano stampati, e le righe si univano con uno spazio (vedibodyText.hardLineBreaksin Testo corrente). - Recinzioni di codice (regole 9): fino alla 1.22, una recinzione
```o~~~e le righe al suo interno venivano lette come Markdown: righe unite in paragrafi, una riga#diventava un titolo, le recinzioni venivano stampate (vedicodeStyle.blocksin Listati di codice). - Righe di versi che vanno a capo (regole 10): nella 1.23, una riga di una poesia composta riga per riga più larga della giustezza andava a capo con la sua spaziatura naturale tra le parole, per quanto poco sbordasse (vedi
bodyText.verse.tightenin Versi).
openBundle e readBundle leggono il config di un manifest del genere, e la configurazione localized di ogni lingua, tramite migrateConfig, che scrive esplicitamente le interruzioni impaginate dalla 1.4 e moltiplica la scala delle formule per 1,131 (dividendo per lo stesso valore i suoi margini delle formule in evidenza in em). Un manifest marcato da 3 a 7, scritto da una prerelease della 1.5, riceve solo i vincoli delle regole successive al suo marchio. Con 3 sono la dimensione delle formule, la distanza in linea, i cinque vincoli delle regole 6, i due delle regole 7 e i tre delle regole 8; con 4, la distanza in linea e i vincoli delle regole 6, 7 e 8; con 5, i vincoli delle regole 6, 7 e 8; con 6, i vincoli delle regole 7 e 8; con 7, solo i vincoli delle regole 8. La divisione sotto un titolo (pinLegacyHeadingSplit) viene scritta come headings.keepWithNextSplit: 'fill' sugli headings che i livelli lasciano in vigore, quando un capitolo letto ha un titolo e la configurazione non indica un valore proprio, mantiene attivo headings.keepWithNext e non disattiva bodyText.avoidOrphans. Gli a capo nelle parole composte (pinLegacyHyphenBreaks) vengono scritti come bodyText.breakAfterHyphens: false sul bodyText in vigore, quando un capitolo letto compone un trattino tra due lettere e la configurazione né lo imposta già né disattiva optimalLineBreaking. Lo spazio sotto i contenitori (pinLegacyParagraphContainerSpacing) viene scritto come bodyText.paragraphContainerSpacing: 'add' sul bodyText in vigore, quando la configurazione dichiara uno stile di paragrafo (in paragraphStyles o nelle sostituzioni del visualizzatore HTML), un capitolo letto apre un contenitore :::paragraphs su una riga a sé e la configurazione non lo imposta già. Gli a capo dopo le lineette (pinLegacyDashBreaks) vengono scritti come bodyText.breakAfterDashes: false sul bodyText in vigore, quando un capitolo letto compone una lineetta lunga o media senza spazi tra due parole (prima una lettera, una cifra o una punteggiatura di chiusura, dopo una lettera, una cifra o una parentesi o virgoletta di apertura; una virgoletta prima della lineetta conta quando prima della virgoletta c’è una lettera, una cifra, una punteggiatura di chiusura o uno spazio indivisibile, come in "no"—and, non in said "—Hola; un segno in linea che tocca la lineetta o la virgoletta, come il ** di **riddles.**—I, conta su entrambi i lati) e la configurazione non lo imposta già. La divisione in righe in bandiera (pinLegacyRaggedBreaking) viene scritta come bodyText.optimalRagged: false sul bodyText in vigore, quando la configurazione compone in bandiera una parte del testo corrente (un textAlign diverso da 'justify' sul testo corrente, su uno stile di paragrafo, sul corpo di un riquadro, sul corpo di una parte o sul corpo di uno stile di sezione (headingStyles[].bodyStyle), oppure nelle sostituzioni del visualizzatore HTML), non lo imposta già e non disattiva optimalLineBreaking. La distanza nei riquadri (pinLegacyBoxResourceGap) viene scritta come layout.inlineResourceGapInBoxes: false sul layout in vigore, quando una risorsa è inserita su una riga a sé dentro un :::callout dei capitoli letti e la configurazione non lo imposta già. Il taglio dei riquadri (pinLegacyBoxChildCut) viene scritto come layout.boxChildSplitMinLines: 1 sul layout in vigore, quando un capitolo letto apre un :::callout su una riga a sé e la configurazione non lo imposta già. I segni nei titoli (pinLegacyHeadingMarks) vengono scritti come headings.inlineMarks: false sugli headings che i livelli lasciano in vigore, quando un titolo dei capitoli letti contiene un segno (*, _, ^, ~, :smallcaps[ o un link nel suo testo) e la configurazione non indica un valore proprio. I capilettera (pinLegacyDropCapSize) ricevono la loro dimensione della 1.4 scritta esplicitamente come dropCap.fontSize, ovunque si trovino: nell’unità dell’interlinea dell’elemento quando questa è una lunghezza, altrimenti nell’unità del suo corpo. Lo spazio sotto la riga con i due punti (pinLegacyColonListRoom) viene scritto come bodyText.colonListRoom: 'line' sul bodyText in vigore, quando un elenco dei capitoli letti segue una riga che termina con i due punti (righe vuote in mezzo ammesse) e la configurazione né indica uno spazio né disattiva keepColonWithList. La distanza in linea (pinLegacyInlineGap) viene scritta come layout.inlineResourceGap: 'above' sul layout che i livelli lasciano in vigore, quando una riga dei capitoli letti inserisce una risorsa (::resource{id="…"} da solo sulla sua riga, come lo legge il parser: una menzione nel testo corrente o in un segmento di codice non conta) e la configurazione non indica una distanza propria. La dimensione viene vincolata sul math che i livelli lasciano in vigore (il math proprio di una lingua sostituisce quello condiviso), e solo quando i capitoli letti contengono un $: un pacchetto senza formule conserva il suo config come è scritto. Quando né il manifest né la lingua indicano un math, quello in vigore è il baseConfig di readBundle (quello del lettore), e anch’esso viene vincolato, perché la 1.4 componeva le formule del pacchetto a quella dimensione: un fontSizeScale: 1.5 di base si legge come 1,5 × 1,1312. Le interruzioni prima dei titoli della base vengono prese così come sono. Un pacchetto vecchio conserva quindi ciò che queste regole impaginavano, e bundle.config mostra le interruzioni, la dimensione delle formule, le distanze, i segni nei titoli, le dimensioni dei capilettera, lo spazio sotto la riga con i due punti, il taglio dei riquadri, gli a capo dopo le lineette, gli a capo nelle parole composte, la divisione in bandiera, la divisione sotto un titolo e lo spazio sotto i contenitori con cui viene impaginato. Le correzioni di impaginazione della 1.5 non hanno vincoli e si applicano a esso come a qualsiasi libro, quindi una pagina che toccano può ancora spostarsi (vedi Dimensione delle formule per l’elenco). Un preset.json scritto a mano per le regole attuali imposta "configVersion": 14; marcare con questo valore il manifest di un pacchetto vecchio è anche il modo, in una riga, per leggerlo con le regole attuali (un pacchetto senza versione perde allora anche il vincolo delle interruzioni prima dei titoli). Un manifest marcato 8, scritto da postext dalla 1.5 alla 1.22, riceve solo i quattro vincoli delle regole 9, come del resto ogni manifest più vecchio: la disposizione dei versi (pinLegacyVerseLayout) viene scritta come bodyText.verse.layout: 'bayt' sul bodyText in vigore, quando un capitolo letto compone una poesia :::verse la cui recinzione non indica una disposizione e le cui righe non contengono un separatore di emistichi, e la configurazione non lo imposta già; i rientri abbinati (pinLegacyPairedIndents) eliminano il firstLineIndent di ogni stile di paragrafo (in paragraphStyles o nelle sostituzioni del visualizzatore HTML) che imposta anche un hangingIndent diverso da zero; gli a capo forzati (pinLegacyHardBreaks) vengono scritti come bodyText.hardLineBreaks: false sul bodyText in vigore, quando un capitolo letto termina con una barra rovesciata una riga di un paragrafo, di una citazione o di una voce di elenco e il blocco prosegue sotto, oppure compone \\ prima di uno spazio e di altro testo (esclusi codice e formule in linea, formule in evidenza, titoli e poesie :::verse), e la configurazione non lo imposta già; e le recinzioni di codice (pinLegacyCodeBlocks) vengono scritte come codeStyle.blocks: false, quando un capitolo letto apre una recinzione ``` o ~~~ (tre o più caratteri, con al massimo tre spazi di rientro) e la configurazione non lo imposta già. Un manifest marcato 9, scritto da postext 1.23, riceve solo il vincolo delle regole 10, come del resto ogni manifest più vecchio: le righe di versi che vanno a capo (pinLegacyVerseTightening) vengono scritte come bodyText.verse.tighten: false sul bodyText in vigore, quando un capitolo letto compone una poesia riga per riga (una recinzione :::verse che indica layout=lines, oppure che non indica una disposizione su righe senza separatore di emistichi mentre la configurazione non compone queste poesie come bayt) e la configurazione non lo imposta già. Un manifest marcato 10, scritto da postext 1.24, riceve solo il vincolo delle regole 11, come del resto ogni manifest più vecchio: il bilanciamento di una griglia di caratteri (pinLegacyGridBalancing) viene scritto come headings.balancing.enabled: true sugli headings in vigore, quando la configurazione unita imposta cjk.grid.enabled nel testo orizzontale e non imposta essa stessa enabled, perché la 1.24 bilanciava una pagina del genere per impostazione predefinita. Le regole 11 impediscono anche gli a capo di un solo carattere nei titoli di opere, compongono i numeri cerchiati come caratteri cinesi e compongono il testo di design CJK con le regole del testo corrente (#637): un manifest marcato 10 o più vecchio riceve cjk.titleMinChars: 1 (pinLegacyTitleBreaks) quando un capitolo letto compone un titolo di opera (《 〈 o :book[), cjk.circledNumbers: 'western' (pinLegacyCircledNumbers) quando uno compone un numero cerchiato (U+2460–U+24FF, U+2776–U+2793), e cjk.composeDesignText: false (pinLegacyDesignText) quando un capitolo letto o la configurazione stessa contiene testo CJK, ciascuno sul cjk in vigore e solo quando la configurazione non lo imposta già. Tagliano anche le tabelle in linea e compongono :::columns nel testo corrente (#634): un manifest marcato 10 o più vecchio riceve tableStyle.splitInline: false (pinLegacyInlineTableSplit) sul tableStyle in vigore quando un capitolo letto inserisce una risorsa, e layout.flowColumns: false (pinLegacyFlowColumns) sul layout in vigore quando un capitolo letto apre una recinzione :::columns su una riga a sé, ciascuno solo quando la configurazione non lo imposta già. Offrono inoltre a un elemento flottante la cima della colonna di un’apertura a tutta larghezza (#639): un manifest marcato 10 o più vecchio riceve layout.floatsUnderOpener: false (pinLegacyOpenerHeadFloats) sul layout in vigore quando la configurazione unita imposta un livello o uno stile di titolo con span: 'page' e un capitolo letto ha un titolo, solo quando la configurazione non lo imposta già. Le regole 12 permettono a una pagina su una griglia di caratteri di trattenere le sue righe (#717): un manifest marcato 11 o più vecchio la cui configurazione unita attiva cjk.grid.enabled riceve bodyText.avoidWidows: true, e nella regione japan bodyText.avoidOrphans: true (pinLegacyGridBreakRules), sul bodyText in vigore, ciascuno solo quando la configurazione non lo imposta già. Mantengono anche due caratteri di una nota su due righe da ciascun lato di un a capo (#720): un manifest del genere riceve cjk.warichu.minChars: 1 (pinLegacyNoteBreaks) sul cjk in vigore quando un capitolo letto compone una nota :warichu[ o la configurazione unita imposta citations.notes: 'warichu', solo quando la configurazione non lo imposta già. E nella regione Giappone una parola occidentale prende lo spazio Han–latino accanto alle sue parentesi e ai suoi segni (#721): un manifest marcato 11 o più vecchio la cui configurazione unita si risolve nella regione Giappone riceve cjk.latinSpacingEdge: 'letters' (pinLegacyLatinSpacingEdge) sul cjk in vigore quando un capitolo letto contiene kana o kanji, solo quando la configurazione non lo imposta già. E una risorsa composta nel testo prende placement.captionSide come fa un elemento flottante di colonna (#729): un manifest marcato 11 o più vecchio la cui configurazione unita imposta layout.layoutType: 'oneAndHalf' riceve layout.inlineCaptionSide: false (pinLegacyInlineCaptionSide) quando un capitolo letto inserisce una risorsa, solo quando la configurazione non lo imposta già.
Le regole 12 cambiano anche il modo in cui si bilancia una colonna corta, e un manifest marcato 11 o più vecchio mantiene ciò con cui è stato impaginato. L’ordine delle leve (#707): riceve headings.balancing.maxLinesBeforeLoose impostato al suo maxLinesPerHeading, o a 4 quando non ne imposta nessuno (pinLegacyLooseOrder), sugli headings in vigore, quando un capitolo letto ha un titolo e la configurazione unita bilancia le sue colonne, consente a un titolo di occupare più di due righe e non imposta già la chiave. La riga sotto una figura su più colonne (#708): riceve headings.balancing.stretchAfterWideFloats: 'always' (pinLegacyWideFloatStretch) sugli headings in vigore, quando un capitolo letto inserisce o cita una risorsa o fa flottare un riquadro, e la configurazione unita bilancia le sue colonne, non disattiva stretchAfterFloats e non imposta già la chiave. Le interruzioni dentro i gruppi di un indice analitico (#712): riceve index.groups.splitMinEntries: 1 (pinLegacyIndexGroupSplit) sull’index in vigore, quando un capitolo letto compone una recinzione :::index su una riga a sé e la configurazione non imposta già la chiave. L’unità del taglio a livello (#711): riceve headings.balancing.trailingStep: 'grid' (pinLegacyTrailingStep) sugli headings in vigore, quando un capitolo letto può comporre testo fuori dall’interlinea del testo corrente (una recinzione :::, una recinzione di codice, una citazione, un elenco, una formula in evidenza o una citazione bibliografica), e la configurazione unita bilancia le sue colonne, mantiene attivo trailing o beforeSpan e non imposta già la chiave.
Le regole 12 vanno a capo agli spazi nell’etichetta di un rimando quando l’etichetta è fatta di parole (#702). Un manifest marcato 11 o più vecchio riceve crossRefs.breakLabels: false (pinLegacyRefLabelBreaks) sul crossRefs in vigore, quando un capitolo letto contiene un rimando (:ref{…}, \ref{…}, \eqref{…} o un @id di pandoc-crossref) e la configurazione non indica l’impostazione: ogni etichetta resta l’unica parola con cui era stata impaginata.
Le regole 12 compongono una parola senza lettere (46., 1367)) nella scrittura del testo che la circonda (#725). Un manifest marcato 11 o più vecchio riceve bodyText.scriptContext: false (pinLegacyScriptContext) sul bodyText in vigore, qualunque cosa contengano i suoi capitoli, quando la configurazione non indica l’impostazione: una parola del genere viene composta da sola, come era stata impaginata. La regola agisce in un font che disegna la punteggiatura in base alla scrittura, e quali font carica un libro non si sa quando se ne legge il manifest.
Le regole 13 trasformano in un’impostazione la lingua in cui viene composto il testo (#764), e le regole 14 rendono predefinita la lingua propria del documento (#770). Un manifest marcato 13 o più vecchio riceve bodyText.shapingLanguage: 'host' (pinLegacyShapingLanguage) sul bodyText in vigore, qualunque cosa contengano i suoi capitoli, quando la configurazione non indica l’impostazione; uno che indica un valore lo mantiene. Il suo testo viene misurato nella lingua della pagina che ospita l’impaginazione, e in nessuna lingua in un worker, come era stato impaginato. Un manifest marcato 13 è stato scritto da postext 1.29: la 1.29.1 e la 1.29.2 impaginavano un libro che non indica un valore nella lingua dell’applicazione ospite, come fa il vincolo; la 1.29.0, il cui valore predefinito era 'document', lo impaginava in quella del documento con un canvas e un PDF che non seguivano la misurazione, e anch’esso si legge con 'host'. Un manifest marcato 14 che non indica un valore viene impaginato nella lingua del documento.
Le regole 13 disegnano anche la casella di una voce di attività (- [ ], - [x]): un quadrato disegnato dal motore, con un segno di spunta quando la voce è completata, che occupa uno spazio proprio prima del testo della voce (#765). Un manifest marcato 12 o più vecchio riceve unorderedLists.taskBox: 'character' (pinLegacyTaskBox) sull’unorderedLists in vigore, quando un capitolo contiene una voce di attività e la configurazione non indica l’impostazione. La sua casella resta il carattere ☐ o ☑ composto nel carattere dell’elenco, e il testo della voce inizia dopo la larghezza di quel carattere, come era stato impaginato. Quella larghezza dipendeva dal font che conteneva il carattere, nella maggior parte dei libri un font di sistema, e non si conosce quando si legge il manifest.
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, verse: { layout: 'bayt', tighten: false }, hardLineBreaks: false }, codeStyle: { blocks: 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); // today's rules: `config` itselfcontent è il markdown che la configurazione impagina (una stringa o un elenco di capitoli). Senza di esso la dimensione delle formule viene vincolata ogni volta che la matematica è attiva, lo spazio sotto i contenitori ogni volta che la configurazione dichiara uno stile di paragrafo, e le due distanze, i segni nei titoli, lo spazio sotto la riga con i due punti, il taglio dei riquadri, gli a capo dopo le lineette, la divisione sotto un titolo e gli a capo nelle parole composte sempre, perché il motore non può sapere se il libro contiene una formula, un contenitore :::paragraphs, una figura in linea, un titolo con segni, un elenco introdotto dai due punti, un riquadro, una lineetta senza spazi, un titolo o una parola composta. La divisione in bandiera viene vincolata dalla sola configurazione, con o senza contenuto. Migra una configurazione salvata una sola volta e salvala di nuovo con CONFIG_VERSION: il vincolo delle formule moltiplica la scala, quindi una configurazione migrata due volte crescerebbe due volte. Anche la disposizione dei versi e le recinzioni di codice vengono vincolate senza contenuto, perché il libro potrebbe contenere una poesia :::verse senza separatore o una recinzione, e i rientri abbinati vengono vincolati dalla sola configurazione. Lo stesso vale per le righe di versi che vanno a capo della 1.23, perché il libro potrebbe comporre una poesia riga per riga.
#Impaginare e disegnare un pacchetto
Quattro funzioni collegano un pacchetto aperto al motore e ai backend:
loadBundleFonts(bundle)registra i font del pacchetto indocument.fonts, e i loro byte nel registro dei font del motore per le immagini SVG (registerFontBytes). Attendine il completamento prima di impaginare, perché l’impaginazione misura il testo con i font di cui dispone il browser. Le famiglie che il pacchetto nomina ma non contiene (Google Fonts) devi comunque caricarle tu, come per qualsiasi altro documento.registerBundleImages(bundle)decodifica le immagini per il backend canvas (renderPage,renderToCanvas), poster dei video compresi.bundleImageUrl(bundle)è la funzione di risoluzioneresourceImageUrlperrenderToHtml, ebundleVideoUrl(bundle)la sua funzione di risoluzioneresourceVideoUrlper i file video contenuti in un pacchetto. Entrambe ricolorano le figure SVG quandodiagramStyle.singleInkè attivo, una volta sola: ricolorano il markup e contrassegnano le immagini perché nessun backend le tinga di nuovo (vedi Un solo inchiostro su canvas e in HTML). Entrambe incorporano inoltre in ogni SVG i font che il suo testo nomina, prima dai font propri del pacchetto, poi dai font registrati nel motore (vedi Font nel testo SVG);registerBundleImages(bundle, { onWarning })ebundleImageUrl(bundle, { onWarning })segnalano una famiglia senza font.buildBundle(bundle)impagina i capitoli in ordine e restituisce unVDTDocumentper capitolo. Ogni capitolo prosegue quello precedente: contatori dei titoli e delle risorse, la parte aperta, la parità delle pagine e la numerazione delle pagine. Un capitolo che stampa l’indice (:::toc) o l’indice analitico (:::index) riceve la struttura dell’intero libro. Accetta le stesse opzioni dibuildDocument, piùconfigper sostituire la configurazione del pacchetto,cacheper condividere una cache delle misure emetadata(vedi sotto).bundleResourceBytes(bundle)ebundleFontProvider(bundle, { decodeWoff2, fallback })sono le opzioniresourceBytesefontProviderdelrenderToPdfdipostext-pdf. Il fornitore di font sceglie dal pacchetto il peso più vicino dello stile richiesto. Per un font.woff2ha bisogno didecompressWoff2, e per una famiglia che il pacchetto non contiene chiamafallbackcon gli argomenti del renderer,requestcompreso, e passa avanti qualunque cosa restituisca. Un fallback che scarica solo il filelatindi una famiglia stampa come riquadri vuoti una famiglia cinese che il pacchetto non incorpora; uno che risponde con porzioni, comesliceFontProviderin Font cinesi, giapponesi e coreani, la stampa per intero.
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); // one VDTDocument per chapter
const firstPage = renderPage(docs[0].pages[0], docs[0]); // a <canvas>
const pdf = await renderToPdf(docs, { // the whole book
fontProvider: bundleFontProvider(bundle, { decodeWoff2: decompressWoff2, fallback: fontsource }),
resourceBytes: bundleResourceBytes(bundle),
});Per impaginare da te un singolo capitolo, passa bundle.chapters[i].markdown, bundle.resources e bundle.config a buildDocument, come faresti per qualsiasi documento.
I metadati del libro. Come nella Sandbox, il front matter del primo capitolo è quello del libro: buildBundle passa il suo title, il suo author e il resto a ogni capitolo, così le testatine {title} e {author} valgono su ogni pagina e il doc.metadata di ogni capitolo li contiene. Un blocco di front matter in cima a un capitolo successivo viene ignorato: viene individuato dalle sue righe --- e svuotato senza essere analizzato, quindi un YAML che il parser rifiuterebbe non fa danni. options.metadata fornisce i valori che il front matter non imposta (prevale il front matter). Anche il numero di pagine del libro arriva a ogni capitolo: {bookTotalPages} lo stampa, mentre {totalPages} conta le pagine del capitolo (vedi Numero di pagine del libro).
const docs = buildBundle(bundle, { metadata: { author: 'A. Author' } });
docs[3].metadata.title; // the first chapter's `title:`Dove inizia il libro. Un pacchetto può contenere una parte di una pubblicazione più lunga: le pagine da 58 a 61 di un fascicolo, il capitolo 4 di un manuale. Il suo start dice che cosa viene prima, nei termini della continuation di buildDocument: le pagine prima della prima (pageIndexOffset, che decide su quale lato cade la pagina 1, e con esso i margini speculari e le testatine pari e dispari), la numerazione delle pagine in vigore (pageNumbering), i contatori dei titoli (headings), la parte aperta (part) e i contatori di risorse, enunciati, note a piè di pagina e righe. createBundle lo scrive come start di preset.json, openBundle lo restituisce come bundle.start, e buildBundle impagina con esso il primo capitolo, come farebbe buildDocument({ markdown, continuation: start }, config), e da lì concatena i capitoli successivi; anche {bookTotalPages} conta le pagine che precedono il libro. Un manifest senza start si legge come prima, e un lettore che non conosce il campo lo ignora. bookPageCount non ne fa parte: le pagine le conta il lettore. In un pacchetto con più lingue, localized[…].start dà a un’edizione un inizio proprio.
const { bytes } = await createBundle({
name: 'Field notes, chapter 4',
markdown,
config,
// Page 58, an even page, in chapter 4.
start: { pageIndexOffset: 57, pageNumbering: { startAt: 58 }, headings: { h1: 3 } },
});
const bundle = await openBundle(bytes);
bundle.start; // { pageIndexOffset: 57, pageNumbering: { startAt: 58 }, headings: { h1: 3, h2: 0, … } }
const [doc] = buildBundle(bundle);
doc.pages[0].pageLabel; // '58'#Esempio dal vivo: aprire un pacchetto
Il pen carica dal repository un libro di esempio di due capitoli (lantern.postext, con un carattere proprio, una figura SVG e una tabella). Registra i font e le immagini del pacchetto, impagina il libro con buildBundle e disegna ogni pagina. Make the PDF disegna gli stessi documenti con postext-pdf, incorporando i font del pacchetto. Scegli un tuo file .postext, per esempio esportato dalla Sandbox, per vederlo allo stesso modo.
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);
}Carica un editor interattivo da codepen.io. L’esempio importa da una CDN l’ultima versione di postext.
#Creare un pacchetto
createBundle scrive un file .postext a partire da un documento: i suoi capitoli, la configurazione, le risorse e i contenuti a cui fanno riferimento.
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 },
});| Input | Significato |
|---|---|
name, id, description, locale | I metadati del manifest. id vale per impostazione predefinita uno slug di name. |
chapters o markdown | Il libro, un { title?, markdown } per capitolo, oppure un singolo documento. |
config | Il PostextConfig. I valori uguali a quelli predefiniti vengono lasciati fuori dal manifest. |
resources | Le risorse. Un’immagine indica il suo contenuto con bitmap.fileId / svg.fileId (e svg.pdfFileId per un master di stampa). |
files | I contenuti per fileId (un oggetto o una Map): le immagini a cui fanno riferimento le risorse e i file dei font a cui fanno riferimento le varianti di config.customFonts. I valori possono essere un Uint8Array, un ArrayBuffer, un Blob o una stringa (markup SVG). |
thumbnail | { data, mime }: un’immagine di copertina (PNG, JPEG, WebP, GIF o SVG). |
canvasScope | 'book' chiede ai visualizzatori di impaginare l’intero libro come un unico canvas. |
start | Il punto in cui inizia il libro, per un pacchetto che contiene una parte di un libro più lungo: la continuation con cui verrebbe costruito un singolo documento. Viene scritto come start del manifest. |
mtime | La data di modifica scritta su ogni file dell’archivio (una Date, un timestamp o una stringa di data). Se la ometti, è l’ora della chiamata, quindi due chiamate con lo stesso input producono byte diversi. Passa una data fissa e lo stesso input produce gli stessi byte, che puoi sottoporre a hash o confrontare. Uno zip conserva data e ora senza fuso orario, a passi di due secondi, dal 1980 al 2099, e la data viene scritta nell’ora locale della macchina. Per avere byte identici su ogni macchina, costruisci la data a partire da campi locali, come new Date(1980, 0, 1): un timestamp o una stringa che termina con Z indica un istante, che cade in un’ora locale diversa in ogni fuso orario ('1980-01-01T00:00:00Z' a ovest di UTC è ancora 1979). Una data fuori da quegli anni, in ora locale, genera un errore. |
localized | Altre lingue dello stesso libro, per tag di lingua: { es: { chapters?, config?, resources? } }. Gli input qui sopra diventano allora il contenuto di locale, che è obbligatorio. Vedi Pacchetti bilingui. |
Restituisce i bytes dell’archivio, il manifest scritto come preset.json, ogni file come files (percorso → byte) e un elenco di warnings. I file prendono il nome dall’id della loro risorsa (resources/lantern.svg) o dal nome del file del font (fonts/…), e i capitoli dal loro ordine e dal titolo (chapters/01-dusk.md). I font sono dichiarati nei fonts del manifest, mai dentro config.customFonts. Alcune cose vengono lasciate fuori, ciascuna con un avviso:
- una risorsa o un font il cui contenuto non è in
files - un font
.woff(il backend PDF non può incorporarlo) - una famiglia marcata
redistributable: false
Nel browser, passa bytes a un link di download: URL.createObjectURL(new Blob([bytes], { type: 'application/zip' })). In Node, scrivili con fs.writeFile. createBundle e openBundle non hanno bisogno di un DOM. Le dist usano percorsi di modulo senza estensione, quindi in Node puro, senza bundler, serve un hook di risoluzione. Il file docs/examples/open-bundle/build-sample.mjs del repository ne mostra uno in poche righe.
#Pacchetti bilingui
Un file .postext può contenere un libro in più lingue, e openBundle(bytes, { locale }) lo legge in una qualsiasi di esse. createBundle ne scrive uno a partire da localized: una voce per ogni lingua aggiuntiva, ciascuna con ciò che differisce dal contenuto principale (l’input 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' }); // Spanish chapters, config and captionschapters: il libro in quella lingua. I file dei capitoli vanno in una cartella per lingua (chapters/en/01-dusk.md,chapters/es/01-anochecer.md) e ichaptersdel manifest diventano una mappa lingua → capitoli. Una lingua senzachapterslegge quelli principali; quando nessuna lingua ha capitoli propri, restano un unico elenco.config: la configurazione per quella lingua. Ogni chiave di primo livello sostituisce per intero la chiave condivisa quando il pacchetto viene letto in quella lingua, quindiheadingsqui sopra sostituisce l’intero oggettoheadings. Le chiavi omesse, o uguali a quelle condivise, sono condivise e non vengono scritte, quindi passare la configurazione completa della lingua funziona come passare le poche chiavi che cambiano. Una chiave impostata ai valori predefiniti mentre quella condivisa non lo è (layout: {}) viene scritta così com’è, quindi azzera il valore condiviso. I font sono condivisi: le famiglie deicustomFontsdi una lingua si aggiungono aifontsdel pacchetto.resources: i testi delle risorse condivise, abbinati perid:caption,note,altTexte iltabledi una tabella. Un’immagine che contiene parole può avere una grafica propria:bitmap.fileIdosvg.fileId(esvg.pdfFileId) indicano un altro contenuto infiles, scritto comeresources/es/lantern.svg. Gli altri campi, come il tipo o la collocazione, sono condivisi. Un id che non è tra leresourcesviene lasciato fuori con un avviso, e un’immagine mancante per una lingua lascia quella lingua sull’immagine condivisa, anche in questo caso con un avviso.
Il manifest elenca ogni lingua in locales (['en', 'es']), mantiene quella principale come locale e conserva le altre sotto localized. openBundle senza lingua legge la lingua principale.
Quale lingua riceve chi legge. openBundle(bytes, { locale }) serve la lingua esatta, altrimenti la sua lingua di base (es-MX legge es), altrimenti quella principale, e bundle.locale indica quale ha servito. Capitoli e testi provengono sempre dalla stessa lingua. La lingua principale mantiene i testi condivisi anche quando localized contiene una sua variante regionale: un pacchetto pt-PT con una voce pt-BR legge le didascalie brasiliane solo per pt-BR, e quelle condivise per pt-PT e pt.
#Esempio dal vivo: creare un pacchetto
Il pen costruisce un libro di due capitoli con una figura SVG ed elenca i file che createBundle ha scritto insieme al manifest. Offre l’archivio da scaricare, poi lo riapre con openBundle e ne disegna la prima pagina: il giro completo in poche righe. Importa il file scaricato nella Sandbox per continuare a lavorarci lì.
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);
}Carica un editor interattivo da codepen.io. L’esempio importa da una CDN l’ultima versione di postext.
#Lavorare con i pacchetti
Dato che la Sandbox, la skill per agenti e il pacchetto postext leggono e scrivono tutti lo stesso file, un file .postext è un modo comodo per passare un libro da uno strumento all’altro:
- Parti da un pacchetto. Porta una pubblicazione esistente con la skill per agenti, oppure progetta un libro nella Sandbox ed esportalo (Scarica (.postext) nel menu ⋯ della sua riga nel pannello Libri). Carica il file dal tuo programma con
openBundleper disegnarlo su canvas, in HTML o in PDF. Tieni il file come sorgente del libro: modifica i capitoli, la configurazione o le risorse nel codice e riscrivilo concreateBundle, oppure ricaricalo semplicemente ogni volta che cambia. - Correggi e rifinisci nella Sandbox. Quando qualcosa nell’output del tuo programma va sistemato (una figura che finisce sulla pagina sbagliata, uno stile di titolo, il bilanciamento delle colonne), esporta con
createBundleciò che il tuo programma impagina. Importa quel file nella Sandbox (Libri → Nuovo → Apri un file .postext…), correggi il testo, il progetto grafico o le figure con l’anteprima dal vivo, il pannello Verifiche e la vista PDF, poi esportalo di nuovo. Il tuo programma carica allora il file corretto conopenBundle. Oppure riporta le modifiche nel tuo codice: ilconfigdel manifest contiene solo i valori diversi da quelli predefiniti, quindi si legge come un breve diff.
#API di basso livello
postext/bundle esporta anche i mattoni su cui si basano openBundle e createBundle, per le applicazioni ospiti che conservano o servono i pacchetti a modo loro (una cartella decompressa servita via HTTP, record in un database):
openBundleZip(bytes)/zipBundle(files, { mtime }): il livello dell’archivio. L’apertura tollera una cartella di primo livello e ignora le voci__MACOSXe i file nascosti. I percorsi che escono dal pacchetto vengono rifiutati.mtimedata i file come fa l’input dicreateBundle.readBundle(manifest, readFile, options)trasforma un manifest più una callbackreadFile(path)in capitoli, configurazione, risorse, immagini e font.optionsimposta la lingua, come vengono chiamati gli id dei file (ids), la configurazione di base (baseConfig, sotto quella del manifest; per impostazione predefinita la tavolozza e i tipi di risorsa dibundleBaseConfignella lingua del pacchetto, che restituisceresolveBundleConfigLocale(manifest, locale), e un’applicazione ospite che passa il propriobaseConfigdovrebbe localizzarlo in quella lingua; sotto un manifest precedente aconfigVersion: 4il suomathviene vincolato insieme a quello del pacchetto, sotto uno precedente alla 5 la distanza in linea del suolayout, sotto uno precedente alla 6 la distanza nei riquadri del suolayout, lo spazio sotto la riga con i due punti del suobodyText, i segni in linea dei suoiheadingse le dimensioni dei suoi capilettera, sotto uno precedente alla 7 gli a capo dopo le lineette e la divisione in bandiera del suobodyText, e sotto uno precedente alla 8 la divisione sotto un titolo dei suoiheadingse gli a capo nelle parole composte e lo spazio sotto i contenitori:::paragraphsdel suobodyText, vedi Pacchetti scritti da postext 1.4 o precedenti) e come vengono misurate le dimensioni intrinseche.readResolutionlegge dal file la risoluzione di ogni bitmap e la mette inbitmap.fileResolution; vale true per impostazione predefinita quando il pacchetto impostalayout.bitmapResolution: 'file'.planBundle(meta, content)/resolveBundleFiles(plan, sources): il lato della scrittura, diviso in un piano puro (nomi dei file e manifest) e nella risoluzione dei byte tramite le callbackreadBlob/readFont.isBundleManifest(value), i selettori di lingua (pickChapterSpecs,pickLocaleOverrides,pickBundleView,resolveBundleLocale,resolveBundleConfigLocale),svgSize/bitmapSize/bitmapInfo(i pixel di una bitmap e la risoluzione che il suo file dichiara) e i tipi del 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)eLEGACY_MATH_SIZE(0,5 ÷ 0,442): una configurazione salvata nei termini attuali (vedi Pacchetti scritti da postext 1.4 o precedenti).readBundlela applica; anche un’applicazione ospite che conserva le configurazioni a modo suo può farlo, una volta per ogni copia salvata.
La Sandbox è costruita su queste funzioni. Vi aggiunge i propri id di archiviazione e i record di pagine layouts.json.