Zum Hauptinhalt springen

Kapitel 3.19 · Teil II · Das Handwerk

Programmatische Nutzung: .postext-Pakete

.postext-Dateien aus Code öffnen, setzen, rendern und erstellen, zweisprachige Pakete, Dateien älterer Versionen und die API der unteren Ebene

Aktualisiert 2026-10-1627 minIm Kapitel 3 Konfigurationenescadefritptzhjaar

Kurz gesagt

Diese Seite richtet sich an Menschen, die Code schreiben. Eine .postext-Datei enthält ein ganzes Buch in einer einzigen Datei: Text, Einstellungen, Schriften und Bilder. Die Seite zeigt, wie Sie eine solche Datei öffnen, ihre Seiten setzen und zeichnen. Sie zeigt, wie Sie aus Ihrem eigenen Text eine neue Datei erstellen und wie Sie zwei Sprachfassungen eines Buchs in einer Datei halten. Außerdem erklärt sie, wie Dateien gelesen werden, die ältere Versionen von Postext erzeugt haben.

#Pakete (Dateien im Format .postext)

Eine .postext-Datei ist ein ganzes Buch in einer Datei: ein Zip-Archiv mit einem Manifest preset.json, einer Markdown-Datei pro Kapitel, den Nutzdaten der Ressourcen (Bitmaps, SVGs, PDF-Druckoriginale) und den Schriftdateien, die die Konfiguration nennt. Die Sandbox exportiert und importiert sie, und der Agenten-Skill liefert sie. Auch das Paket postext kann sie erstellen und öffnen, sodass ein Buch zwischen diesen Werkzeugen und Ihrem eigenen Programm wechseln kann, ohne dass etwas verloren geht.

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

Das Manifest ist Feld für Feld im Anhang Format der Vorlagenpakete der Sandbox beschrieben. Eine Datei kann auch layouts.json enthalten, die Seitenzahlen der Sandbox, sodass sich das Buch dort bereits paginiert öffnet, oder eine pro Ausgabe eines mehrsprachigen Buches (layouts.zh-Hant.json, wird zuerst gelesen). openBundle ignoriert sie.

Die API wird aus postext selbst und aus dem Unterpfad postext/bundle exportiert, der die Hilfsfunktionen der unteren Ebene hinzufügt. Importieren Sie aus postext, wenn Sie auch rendern. So teilen sich die Paket-Adapter und die Renderer eine Modulinstanz, was auf einem CDN wie esm.sh wichtig ist, wo jeder Einstiegspunkt ein eigener Build ist.

#Ein Paket öffnen

openBundle nimmt die Bytes der Datei (ein Uint8Array, einen ArrayBuffer oder einen Blob / eine File aus einem <input type="file">) und gibt alles zurück, was die Engine und ihre Backends brauchen:

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
FeldWas es enthält
manifestDie validierte preset.json.
id, name, descriptionAus dem Manifest.
locale, localesDie Sprache, in der der Inhalt gelesen wurde, und jede Sprache, die ein zweisprachiges Paket enthält. options.locale wählt eine aus: zuerst das genaue Tag, dann die Grundsprache, dann die eigene Sprache des Pakets.
chapters{ title, file, markdown } pro Kapitel. Ein Kapitel ohne Titel im Manifest übernimmt den Text seiner ersten Überschrift #.
configDie Standard-Farbpalette und die Ressourcentypen in der Sprache des Pakets, dann die config des Manifests, dann die Abweichungen der Sprache. Die Sprache des Pakets ist das obige locale, sodass ein Paket in einer Sprache seine eigenen Bezeichnungen erhält, gleich was options.locale verlangt. Ein Manifest, das keine Sprache nennt, übernimmt die, die seine config festlegt (locale, dann die Sprache der Silbentrennung), sonst options.locale. customFonts listet die Schriftfamilien des Pakets auf. Es ist dieselbe Konfiguration, mit der die Sandbox das Paket öffnet.
resourcesDie Ressourcen, mit den Legenden der gewählten Sprache. Eine Größe, die im Manifest fehlt, wird aus der Datei gelesen.
fontsEin Eintrag pro Schnitt: { family, weight, style, format, file, bytes }.
filesJede Datei im Archiv, mit ihrem Pfad als Schlüssel.
thumbnail, canvasScopeDer Pfad des Umschlagbildes und wie das Paket angezeigt werden möchte: der view des Manifests, darüber der localized[…].view der ausgelieferten Sprache.
startWo das Buch beginnt, für ein Paket, das einen Teil eines längeren enthält: das start des Manifests oder das localized[…].start der ausgelieferten Sprache. Fehlt bei einem Buch, das an seinem Anfang beginnt.
warningsNicht fatale Probleme: eine nicht unterstützte Schriftdatei, ein fehlendes Druckoriginal.

Die fileId von Nutzdaten ist ihr Pfad innerhalb des Pakets. resource.svg.fileId, resource.bitmap.fileId und die fileId jeder Variante von customFonts lassen sich direkt in bundle.files nachschlagen. openBundle wirft einen Fehler, wenn die Bytes kein Zip sind, wenn es keine gültige preset.json gibt (in der Wurzel oder unter einem Ordner der obersten Ebene) oder wenn eine Datei fehlt, die das Manifest nennt.

Pakete, die postext 1.4 oder früher geschrieben hat

Jedes Manifest, das createBundle und die Sandbox schreiben, trägt configVersion: 14: die Konfigurationsregeln, für die seine config geschrieben wurde. Ein Manifest ohne diese Angabe stammt von postext 1.4 oder früher, das achtzehn Dinge anders handhabte:

  • Umbrüche vor Überschriften (Regeln 3): Bis 1.4 hatte ein Objekt headings ohne Umbruch vor H1 keinen (siehe Überschreibungen pro Ebene).
  • Die Größe von Formeln (Regeln 4): Bis 1.4 fielen Formeln 1,131-mal größer aus, als fontSizeScale angibt (siehe Formelgröße).
  • Der Raum unter einer Inline-Ressource (Regeln 5): Bis 1.4 setzte der Text nach einer Abbildung oder Tabelle mit placement.position: 'here' an der nächsten Rasterlinie fort, ohne Abstand für Gleitobjekte darunter (siehe layout.inlineResourceGap unter Layout).
  • Der Raum um eine Inline-Ressource in einem Kasten (Regeln 6): Bis 1.4 saß eine solche Ressource direkt am Text des Kastens um sie herum (siehe layout.inlineResourceGapInBoxes unter Layout).
  • Inline-Auszeichnungen in Überschriften (Regeln 6): Bis 1.4 druckte eine Überschrift die Wörter ihrer Auszeichnungen *italic*, **bold** und anderer in ihrem eigenen einfachen Stil (siehe headings.inlineMarks unter Überschriften).
  • Die Größe einer Initiale (Regeln 6): Bis 1.4 war das dropCap eines Gestaltungstextes ohne fontSize so hoch wie alle Zeilenkästen, die es überspannt, mit seiner Oberkante über der ersten Zeile (siehe dropCap unter Textelemente).
  • Der Raum unter einer Doppelpunktzeile (Regeln 6): Bis 1.4 hielt keepColonWithList eine Zeile Raum unter der Zeile, die mit einem Doppelpunkt endet, für die Liste für ausreichend, und ein zweizeiliger erster Listenpunkt, den die Regeln für Waisen und Witwen zusammenhalten, ging ohne sie in die nächste Spalte (siehe bodyText.colonListRoom).
  • Die Zeilen, die ein Kastenschnitt übrig lässt (Regeln 6): Bis 1.4 konnte ein Kasten, der innerhalb eines Absatzes oder Listenpunkts geteilt wurde, eine Zeile davon auf einer Seite lassen, solange jede Seite des Kastens insgesamt ihre splitMinLines Zeilen enthielt (siehe layout.boxChildSplitMinLines unter Layout).
  • Zeilenumbrüche an einem Strich (Regeln 7): Bis 1.4 beendete Knuth-Plass nie eine Zeile nach einem Geviert- oder Halbgeviertstrich, der ohne Abstand zwischen Wörtern stand (say—that’s), und der zeilenweise Umbrecher für formatierten Text nur zwischen zwei Buchstaben (siehe bodyText.breakAfterDashes unter Fließtext).
  • Flattersatz (Regeln 7): Bis 1.4 wurde fließender Text im Flattersatz Zeile für Zeile gesetzt, jede Zeile vor der nächsten gefüllt, gleich was optimalLineBreaking besagte (siehe bodyText.optimalRagged unter Fließtext).
  • Die Teilung unter einer Überschrift (Regeln 8): Bis 1.4 behielt der Absatz unter einer Überschrift am Fuß einer Spalte so viele Zeilen, wie dort Platz hatten, wie wenige auch in die nächste Spalte gingen (siehe headings.keepWithNextSplit unter Überschriften).
  • Der Raum unter einem Container :::paragraphs (Regeln 8): Bis 1.4 wurde der Abstand des Formats vor dem Einrasten ins Raster unter den letzten Absatz gesetzt, der Abstand über dem nächsten Block (das marginTop einer Überschrift) wurde darunter addiert, und der Absatzabstand des Textes entfiel (siehe bodyText.paragraphContainerSpacing unter Fließtext).
  • Zeilenumbrüche am Bindestrich eines Kompositums (Regeln 8): Bis 1.4 beendete Knuth-Plass in einem Absatz ohne Inline-Formatierung nie eine Zeile im Blocksatz nach einem Bindestrich zwischen zwei Buchstaben (well-known), in einem Absatz mit Formatierung dagegen schon (siehe bodyText.breakAfterHyphens unter Fließtext).
  • Gedichte ohne Trenner (Regeln 9): Bis 1.22 wurde ein Gedicht :::verse, dessen Zeilen kein || trugen, als einzelne Halbverse gesetzt, jede Zeile zentriert (siehe bodyText.verse.layout unter Vers).
  • Ein Erstzeileneinzug neben einem hängenden Einzug (Regeln 9): Bis 1.22 ersetzte das hangingIndent eines Absatzformats dessen firstLineIndent, und die erste Zeile begann bei indent (siehe Absatzformate).
  • Ein Backslash am Zeilenende (Regeln 9): Bis 1.22 wurde ein Backslash, der eine Zeile eines Absatzes, eines Zitats oder eines Listenpunkts beendete, sowie \\ vor einem Leerzeichen gedruckt, und die Zeilen wurden mit einem Leerzeichen verbunden (siehe bodyText.hardLineBreaks unter Fließtext).
  • Codezäune (Regeln 9): Bis 1.22 wurden ein Codeblock ``` oder ~~~ und die Zeilen darin als Markdown gelesen: Zeilen wurden zu Absätzen verbunden, eine Zeile mit # wurde zur Überschrift, die Codezäune wurden gedruckt (siehe codeStyle.blocks unter Code-Listings).
  • Umbrüche in Versen (Regeln 10): In 1.23 wurde eine Zeile eines zeilenweise gesetzten Gedichts, die breiter als die Satzbreite war, mit ihren natürlichen Wortabständen umbrochen, wie wenig sie auch überstand (siehe bodyText.verse.tighten unter Vers).

openBundle und readBundle lesen die config eines solchen Manifests und die Konfiguration localized jeder Sprache über migrateConfig, das die Umbrüche, die 1.4 gesetzt hat, ausschreibt und den Formelmaßstab mit 1,131 multipliziert (seine Anzeigeränder in em durch diesen Wert geteilt). Ein Manifest mit dem Stempel 3 bis 7, geschrieben von einer Vorabversion von 1.5, erhält nur die Fixierungen der Regeln nach seinem Stempel. Mit 3 sind das die Formelgröße, der Inline-Abstand, die fünf Fixierungen der Regeln 6, die zwei der Regeln 7 und die drei der Regeln 8; mit 4 der Inline-Abstand und die Fixierungen der Regeln 6, 7 und 8; mit 5 die Fixierungen der Regeln 6, 7 und 8; mit 6 die Fixierungen der Regeln 7 und 8; mit 7 nur die Fixierungen der Regeln 8. Die Teilung unter einer Überschrift (pinLegacyHeadingSplit) wird als headings.keepWithNextSplit: 'fill' in die headings geschrieben, die die Schichten gelten lassen, wenn ein gelesenes Kapitel eine Überschrift hat und die Konfiguration keinen eigenen Wert nennt, headings.keepWithNext eingeschaltet lässt und bodyText.avoidOrphans nicht ausschaltet. Die Umbrüche an Komposita (pinLegacyHyphenBreaks) werden als bodyText.breakAfterHyphens: false in das geltende bodyText geschrieben, wenn ein gelesenes Kapitel einen Bindestrich zwischen zwei Buchstaben setzt und die Konfiguration den Wert weder bereits festlegt noch optimalLineBreaking ausschaltet. Der Raum unter Containern (pinLegacyParagraphContainerSpacing) wird als bodyText.paragraphContainerSpacing: 'add' in das geltende bodyText geschrieben, wenn die Konfiguration ein Absatzformat deklariert (in paragraphStyles oder in den Abweichungen des HTML-Betrachters), ein gelesenes Kapitel einen Container :::paragraphs auf einer eigenen Zeile öffnet und die Konfiguration den Wert nicht bereits festlegt. Die Umbrüche an Strichen (pinLegacyDashBreaks) werden als bodyText.breakAfterDashes: false in das geltende bodyText geschrieben, wenn ein gelesenes Kapitel einen Geviert- oder Halbgeviertstrich ohne Abstand zwischen Wörtern setzt (davor ein Buchstabe, eine Ziffer oder ein schließendes Satzzeichen, danach ein Buchstabe, eine Ziffer oder eine öffnende Klammer oder ein öffnendes Anführungszeichen; ein Anführungszeichen davor zählt, wenn vor dem Anführungszeichen ein Buchstabe, eine Ziffer, ein schließendes Satzzeichen oder ein geschütztes Leerzeichen steht, wie in "no"—and, nicht in said "—Hola; eine Inline-Auszeichnung, die den Strich oder das Anführungszeichen berührt, etwa das ** von **riddles.**—I, zählt auf beiden Seiten) und die Konfiguration den Wert nicht bereits festlegt. Der Umbruch im Flattersatz (pinLegacyRaggedBreaking) wird als bodyText.optimalRagged: false in das geltende bodyText geschrieben, wenn die Konfiguration fließenden Text im Flattersatz setzt (ein anderes textAlign als 'justify' im Grundtext, einem Absatzformat, dem Inhalt eines Kastens, dem Inhalt eines Teils oder dem Inhalt eines Abschnittsformats (headingStyles[].bodyStyle) oder in den Abweichungen des HTML-Betrachters), den Wert nicht bereits festlegt und optimalLineBreaking nicht ausschaltet. Der Abstand in Kästen (pinLegacyBoxResourceGap) wird als layout.inlineResourceGapInBoxes: false in das geltende layout geschrieben, wenn eine Ressource auf einer eigenen Zeile in einem :::callout der gelesenen Kapitel eingebettet ist und die Konfiguration den Wert nicht bereits festlegt. Der Kastenschnitt (pinLegacyBoxChildCut) wird als layout.boxChildSplitMinLines: 1 in das geltende layout geschrieben, wenn ein gelesenes Kapitel ein :::callout auf einer eigenen Zeile öffnet und die Konfiguration den Wert nicht bereits festlegt. Die Auszeichnungen in Überschriften (pinLegacyHeadingMarks) werden als headings.inlineMarks: false in die headings geschrieben, die die Schichten gelten lassen, wenn eine Überschrift der gelesenen Kapitel eine Auszeichnung trägt (*, _, ^, ~, :smallcaps[ oder einen Link im Titel) und die Konfiguration keinen eigenen Wert nennt. Bei den Initialen (pinLegacyDropCapSize) wird ihre Größe aus 1.4 als dropCap.fontSize ausgeschrieben, wo immer sie stehen: in der Einheit des Zeilenabstands des Elements, wenn dieser eine Länge ist, sonst in der Einheit seiner Schriftgröße. Der Raum unter der Doppelpunktzeile (pinLegacyColonListRoom) wird als bodyText.colonListRoom: 'line' in das geltende bodyText geschrieben, wenn eine Liste der gelesenen Kapitel auf eine Zeile folgt, die mit einem Doppelpunkt endet (Leerzeilen dazwischen erlaubt), und die Konfiguration weder einen Raum nennt noch keepColonWithList ausschaltet. Der Inline-Abstand (pinLegacyInlineGap) wird als layout.inlineResourceGap: 'above' in das layout geschrieben, das die Schichten gelten lassen, wenn eine Zeile der gelesenen Kapitel eine Ressource einbettet (::resource{id="…"} allein auf seiner Zeile, so wie der Parser es liest: Eine Erwähnung im Fließtext oder in einem Code-Span zählt nicht) und die Konfiguration keinen eigenen Abstand nennt. Die Größe wird in der math fixiert, die die Schichten gelten lassen (eine eigene math einer Sprache ersetzt die gemeinsame), und nur, wenn die gelesenen Kapitel ein $ enthalten: Ein Paket ohne Formeln behält seine config so, wie sie geschrieben ist. Nennen weder das Manifest noch die Sprache eine math, ist die geltende die der baseConfig von readBundle (die des Lesers selbst), und auch sie wird fixiert, da 1.4 die Formeln des Pakets in dieser Größe setzte: Ein Basiswert fontSizeScale: 1.5 wird als 1,5 × 1,1312 gelesen. Die Umbrüche vor Überschriften aus der Basis werden so übernommen, wie sie sind. Ein altes Paket behält also, was diese Regeln gesetzt haben, und bundle.config zeigt die Umbrüche, die Formelgröße, die Abstände, die Auszeichnungen in Überschriften, die Größen der Initialen, den Raum unter der Doppelpunktzeile, den Kastenschnitt, die Umbrüche an Strichen, die Umbrüche an Komposita, den Umbruch im Flattersatz, die Teilung unter einer Überschrift und den Raum unter Containern, mit denen es gesetzt wird. Die Layout-Korrekturen von 1.5 haben keine Fixierung und gelten dafür wie für jedes Buch, sodass sich eine Seite, die sie betreffen, dennoch verschieben kann (die Liste finden Sie unter Formelgröße). Eine von Hand für die heutigen Regeln geschriebene preset.json setzt "configVersion": 14; das Manifest eines alten Pakets damit zu stempeln ist auch der einzeilige Weg, es mit den heutigen Regeln zu lesen (ein Paket ohne Version verliert dann auch seine Fixierung der Umbrüche vor Überschriften). Ein Manifest mit dem Stempel 8, geschrieben von postext 1.5 bis 1.22, erhält nur die vier Fixierungen der Regeln 9, wie auch jedes ältere: Die Versanordnung (pinLegacyVerseLayout) wird als bodyText.verse.layout: 'bayt' in das geltende bodyText geschrieben, wenn ein gelesenes Kapitel ein Gedicht :::verse setzt, dessen Zaun keine Anordnung nennt und dessen Zeilen keinen Halbverstrenner tragen, und die Konfiguration den Wert nicht bereits festlegt; die gepaarten Einzüge (pinLegacyPairedIndents) streichen das firstLineIndent jedes Absatzformats (in paragraphStyles oder in den Abweichungen des HTML-Betrachters), das auch ein hangingIndent ungleich null festlegt; die erzwungenen Zeilenumbrüche (pinLegacyHardBreaks) werden als bodyText.hardLineBreaks: false in das geltende bodyText geschrieben, wenn ein gelesenes Kapitel eine Zeile eines Absatzes, eines Zitats oder eines Listenpunkts mit einem Backslash beendet und der Block darunter weitergeht, oder \\ vor einem Leerzeichen und weiterem Text setzt (Inline-Code und Inline-Formeln, abgesetzte Formeln, Überschriften und Gedichte :::verse ausgenommen), und die Konfiguration den Wert nicht bereits festlegt; und die Codezäune (pinLegacyCodeBlocks) werden als codeStyle.blocks: false geschrieben, wenn ein gelesenes Kapitel einen Codeblock ``` oder ~~~ öffnet (drei oder mehr Zeichen, bis zu drei Leerzeichen eingerückt) und die Konfiguration den Wert nicht bereits festlegt. Ein Manifest mit dem Stempel 9, geschrieben von postext 1.23, erhält nur die Fixierung der Regeln 10, wie auch jedes ältere: Die Umbrüche in Versen (pinLegacyVerseTightening) werden als bodyText.verse.tighten: false in das geltende bodyText geschrieben, wenn ein gelesenes Kapitel ein Gedicht zeilenweise setzt (ein Zaun :::verse, der layout=lines nennt oder keine Anordnung über Zeilen ohne Halbverstrenner nennt, während die Konfiguration solche Gedichte nicht als Bayts setzt) und die Konfiguration den Wert nicht bereits festlegt. Ein Manifest mit dem Stempel 10, geschrieben von postext 1.24, erhält nur die Fixierung der Regeln 11, wie auch jedes ältere: Der Ausgleich eines Zeichenrasters (pinLegacyGridBalancing) wird als headings.balancing.enabled: true in die geltenden headings geschrieben, wenn die zusammengeführte Konfiguration cjk.grid.enabled bei horizontalem Text festlegt und enabled nicht selbst festlegt, da 1.24 eine solche Seite standardmäßig ausglich. Die Regeln 11 bewahren außerdem Buchtitel vor Umbrüchen nach einem Zeichen, setzen eingekreiste Zahlen als chinesische Zeichen und setzen CJK-Gestaltungstext nach den Regeln des Grundtextes (#637): Ein Manifest mit dem Stempel 10 oder älter erhält cjk.titleMinChars: 1 (pinLegacyTitleBreaks), wenn ein gelesenes Kapitel einen Titel setzt (《 〈 oder :book[), cjk.circledNumbers: 'western' (pinLegacyCircledNumbers), wenn eines eine eingekreiste Zahl setzt (U+2460–U+24FF, U+2776–U+2793), und cjk.composeDesignText: false (pinLegacyDesignText), wenn ein gelesenes Kapitel oder die Konfiguration selbst CJK-Text enthält, jeweils im geltenden cjk und nur, wenn die Konfiguration den Wert nicht bereits festlegt. Sie teilen außerdem Inline-Tabellen und setzen :::columns im Fließtext (#634): Ein Manifest mit dem Stempel 10 oder älter erhält tableStyle.splitInline: false (pinLegacyInlineTableSplit) im geltenden tableStyle, wenn ein gelesenes Kapitel eine Ressource einbettet, und layout.flowColumns: false (pinLegacyFlowColumns) im geltenden layout, wenn ein gelesenes Kapitel einen Zaun :::columns auf einer eigenen Zeile öffnet, jeweils nur, wenn die Konfiguration den Wert nicht bereits festlegt. Sie bieten einem Gleitobjekt auch den Kopf der eigenen Spalte eines Auftakts über die ganze Seitenbreite an (#639): Ein Manifest mit dem Stempel 10 oder älter erhält layout.floatsUnderOpener: false (pinLegacyOpenerHeadFloats) im geltenden layout, wenn die zusammengeführte Konfiguration eine Überschriftenebene oder ein Überschriftenformat mit span: 'page' festlegt und ein gelesenes Kapitel eine Überschrift hat, nur wenn die Konfiguration den Wert nicht bereits festlegt. Die Regeln 12 lassen eine Seite auf einem Zeichenraster ihre Zeilen halten (#717): Ein Manifest mit dem Stempel 11 oder älter, dessen zusammengeführte Konfiguration cjk.grid.enabled einschaltet, erhält bodyText.avoidWidows: true und in der Region japan bodyText.avoidOrphans: true (pinLegacyGridBreakRules) im geltenden bodyText, jeweils nur, wenn die Konfiguration den Wert nicht bereits festlegt. Sie halten außerdem zwei Zeichen einer zweireihigen Anmerkung auf jeder Seite eines Zeilenumbruchs zusammen (#720): Ein solches Manifest erhält cjk.warichu.minChars: 1 (pinLegacyNoteBreaks) im geltenden cjk, wenn ein gelesenes Kapitel eine Anmerkung :warichu[ setzt oder die zusammengeführte Konfiguration citations.notes: 'warichu' festlegt, nur wenn die Konfiguration den Wert nicht bereits festlegt. Und in der Region Japan erhält ein westliches Wort den Abstand zwischen Han und Latein auch neben seinen Klammern und Zeichen (#721): Ein Manifest mit dem Stempel 11 oder älter, dessen zusammengeführte Konfiguration zur Region Japan aufgelöst wird, erhält cjk.latinSpacingEdge: 'letters' (pinLegacyLatinSpacingEdge) im geltenden cjk, wenn ein gelesenes Kapitel Kana oder Kanji enthält, nur wenn die Konfiguration den Wert nicht bereits festlegt. Und eine im Text gesetzte Ressource übernimmt placement.captionSide so, wie es ein Spaltengleitobjekt tut (#729): Ein Manifest mit dem Stempel 11 oder älter, dessen zusammengeführte Konfiguration layout.layoutType: 'oneAndHalf' festlegt, erhält layout.inlineCaptionSide: false (pinLegacyInlineCaptionSide), wenn ein gelesenes Kapitel eine Ressource einbettet, nur wenn die Konfiguration den Wert nicht bereits festlegt.

Die Regeln 12 ändern außerdem, wie eine kurze Spalte ausgeglichen wird, und ein Manifest mit dem Stempel 11 oder älter behält, womit es gesetzt wurde. Die Reihenfolge der Hebel (#707): Es erhält headings.balancing.maxLinesBeforeLoose, gesetzt auf sein eigenes maxLinesPerHeading oder auf 4, wenn es keines festlegt (pinLegacyLooseOrder), in den geltenden headings, wenn ein gelesenes Kapitel eine Überschrift hat und die zusammengeführte Konfiguration ihre Spalten ausgleicht, einer Überschrift mehr als zwei Zeilen erlaubt und den Schlüssel nicht bereits festlegt. Die Zeile unter einer Abbildung über mehrere Spalten (#708): Es erhält headings.balancing.stretchAfterWideFloats: 'always' (pinLegacyWideFloatStretch) in den geltenden headings, wenn ein gelesenes Kapitel eine Ressource einbettet oder zitiert oder einen Kasten gleiten lässt und die zusammengeführte Konfiguration ihre Spalten ausgleicht, stretchAfterFloats nicht ausschaltet und den Schlüssel nicht bereits festlegt. Die Umbrüche innerhalb der Gruppen eines Registers (#712): Es erhält index.groups.splitMinEntries: 1 (pinLegacyIndexGroupSplit) im geltenden index, wenn ein gelesenes Kapitel einen Zaun :::index auf einer eigenen Zeile setzt und die Konfiguration den Schlüssel nicht bereits festlegt. Die Einheit des Ebenenschnitts (#711): Es erhält headings.balancing.trailingStep: 'grid' (pinLegacyTrailingStep) in den geltenden headings, wenn ein gelesenes Kapitel Text abseits des Zeilenabstands des Grundtextes setzen kann (ein Zaun :::, ein Codeblock, ein Zitat, eine Liste, eine abgesetzte Formel oder ein Literaturzitat) und die zusammengeführte Konfiguration ihre Spalten ausgleicht, trailing oder beforeSpan eingeschaltet lässt und den Schlüssel nicht bereits festlegt.

Die Regeln 12 brechen die Bezeichnung eines Verweises an ihren Leerzeichen um, wenn die Bezeichnung aus Wörtern besteht (#702). Ein Manifest mit dem Stempel 11 oder älter erhält crossRefs.breakLabels: false (pinLegacyRefLabelBreaks) in den geltenden crossRefs, wenn ein gelesenes Kapitel einen Verweis enthält (:ref{…}, \ref{…}, \eqref{…} oder ein @id von pandoc-crossref) und die Konfiguration die Einstellung nicht nennt: Jede Bezeichnung bleibt das eine Wort, als das sie gesetzt wurde.

Die Regeln 12 formen ein Wort ohne Buchstaben (46., 1367)) in der Schrift des umgebenden Textes (#725). Ein Manifest mit dem Stempel 11 oder älter erhält bodyText.scriptContext: false (pinLegacyScriptContext) im geltenden bodyText, gleich was seine Kapitel enthalten, wenn die Konfiguration die Einstellung nicht nennt: Ein solches Wort wird für sich allein geformt, so wie es gesetzt wurde. Die Regel wirkt in einem Schnitt, der seine Satzzeichen je nach Schriftsystem zeichnet, und welche Schnitte ein Buch lädt, ist beim Lesen seines Manifests nicht bekannt.

Die Regeln 13 machen die Sprache, in der der Text geformt wird, zu einer Einstellung (#764), und die Regeln 14 machen die eigene Sprache des Dokuments zu ihrem Standard (#770). Ein Manifest mit dem Stempel 13 oder älter erhält bodyText.shapingLanguage: 'host' (pinLegacyShapingLanguage) im geltenden bodyText, gleich was seine Kapitel enthalten, wenn die Konfiguration die Einstellung nicht nennt; eines, das einen Wert nennt, behält ihn. Sein Text wird in der Sprache der Seite gemessen, die das Layout ausführt, und in einem Worker in keiner, so wie er gesetzt wurde. Ein Manifest mit dem Stempel 13 wurde von postext 1.29 geschrieben: 1.29.1 und 1.29.2 setzten ein Buch, das keinen Wert nennt, in der Sprache des Hosts, wie es die Fixierung tut; 1.29.0, dessen Standard 'document' war, setzte es in der des Dokuments, mit einem Canvas und einem PDF, die der Messung nicht folgten, und es wird ebenfalls mit 'host' gelesen. Ein Manifest mit dem Stempel 14, das keinen Wert nennt, wird in der Sprache des Dokuments gesetzt.

Die Regeln 13 zeichnen außerdem den Kasten eines Aufgabenpunkts (- [ ], - [x]): ein Quadrat, das die Engine zeichnet, mit einem Häkchen, wenn der Punkt erledigt ist, und das vor dem Text des Punkts einen eigenen Raum einnimmt (#765). Ein Manifest mit dem Stempel 12 oder älter erhält unorderedLists.taskBox: 'character' (pinLegacyTaskBox) in den geltenden unorderedLists, wenn ein Kapitel einen Aufgabenpunkt enthält und die Konfiguration die Einstellung nicht nennt. Sein Kasten bleibt das Zeichen ☐ oder ☑ in der Schrift der Liste, und der Text des Punkts beginnt nach der Breite dieses Zeichens, so wie er gesetzt wurde. Diese Breite stammte von der Schrift, die das Zeichen gerade enthielt, in den meisten Büchern einer Systemschrift, und ist beim Lesen des Manifests nicht bekannt.

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` itself

content ist das Markdown, das die Konfiguration setzt (ein String oder eine Liste von Kapiteln). Ohne diese Angabe wird die Formelgröße immer fixiert, wenn Formeln eingeschaltet sind, der Raum unter Containern immer, wenn die Konfiguration ein Absatzformat deklariert, und die beiden Abstände, die Auszeichnungen in Überschriften, der Raum unter der Doppelpunktzeile, der Kastenschnitt, die Umbrüche an Strichen, die Teilung unter einer Überschrift und die Umbrüche an Komposita immer, da die Engine nicht erkennen kann, ob das Buch eine Formel, einen Container :::paragraphs, eine Inline-Abbildung, eine ausgezeichnete Überschrift, eine durch einen Doppelpunkt eingeleitete Liste, einen Kasten, einen Strich ohne Abstand, eine Überschrift oder ein Kompositum enthält. Der Umbruch im Flattersatz wird allein nach der Konfiguration fixiert, mit oder ohne Inhalt. Migrieren Sie eine gespeicherte Konfiguration einmal und speichern Sie sie erneut unter CONFIG_VERSION: Die Fixierung der Formeln multipliziert den Maßstab, sodass eine zweimal migrierte Konfiguration zweimal wachsen würde. Die Versanordnung und die Codezäune werden ohne die Angabe ebenfalls fixiert, da das Buch ein Gedicht :::verse ohne Trenner oder einen Codeblock enthalten kann, und die gepaarten Einzüge werden allein nach der Konfiguration fixiert. Ebenso die Umbrüche in Versen aus 1.23, da das Buch ein Gedicht zeilenweise setzen kann.

#Ein Paket setzen und rendern

Vier Hilfsfunktionen verbinden ein geöffnetes Paket mit der Engine und den Backends:

  • loadBundleFonts(bundle) registriert die Schnitte des Pakets in document.fonts und ihre Bytes in der Schriftregistrierung der Engine für SVG-Bilder (registerFontBytes). Warten Sie vor dem Setzen darauf, denn das Layout misst Text mit den Schriften, die der Browser hat. Familien, die das Paket nennt, aber nicht mitbringt (Google Fonts), müssen Sie weiterhin selbst laden, wie bei jedem anderen Dokument.
  • registerBundleImages(bundle) dekodiert die Bilder für das Canvas-Backend (renderPage, renderToCanvas), Vorschaubilder von Videos eingeschlossen. bundleImageUrl(bundle) ist der Resolver resourceImageUrl für renderToHtml und bundleVideoUrl(bundle) sein Resolver resourceVideoUrl für die Videodateien, die ein Paket enthält. Beide färben SVG-Abbildungen um, wenn diagramStyle.singleInk eingeschaltet ist, und zwar einmal: Sie färben das Markup um und markieren die Bilder, damit kein Backend sie erneut tönt (siehe Einfarbmodus auf dem Canvas und in HTML). Beide betten außerdem in jedes SVG die Schnitte ein, die sein Text nennt, zuerst aus den eigenen Schriften des Pakets, dann aus den in der Engine registrierten Schnitten (siehe Schriften in SVG-Text); registerBundleImages(bundle, { onWarning }) und bundleImageUrl(bundle, { onWarning }) melden eine Familie ohne Schnitt.
  • buildBundle(bundle) setzt die Kapitel der Reihe nach und gibt ein VDTDocument pro Kapitel zurück. Jedes Kapitel setzt das vorherige fort: Zähler für Überschriften und Ressourcen, den offenen Teil, die Seitenparität und die Seitennummerierung. Ein Kapitel, das das Inhaltsverzeichnis (:::toc) oder das Register (:::index) druckt, erhält die Gliederung des ganzen Buches. Die Funktion nimmt dieselben Optionen wie buildDocument, dazu config, um die Konfiguration des Pakets zu überschreiben, cache, um einen Mess-Cache zu teilen, und metadata (siehe unten).
  • bundleResourceBytes(bundle) und bundleFontProvider(bundle, { decodeWoff2, fallback }) sind die Optionen resourceBytes und fontProvider von renderToPdf aus postext-pdf. Der Font-Provider wählt aus dem Paket die nächstgelegene Stärke des angefragten Stils. Für einen Schnitt .woff2 braucht er decompressWoff2, und für eine Familie, die das Paket nicht mitbringt, ruft er fallback mit den Argumenten des Renderers auf, request eingeschlossen, und reicht weiter, was diese Funktion zurückgibt. Ein Fallback, der nur die Datei latin einer Familie abruft, druckt eine chinesische Familie, die das Paket nicht einbettet, als leere Kästen; einer, der mit Teilen antwortet, etwa sliceFontProvider unter Chinesische, japanische und koreanische Schriften, druckt sie vollständig.
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),
});

Um ein einzelnes Kapitel selbst zu setzen, übergeben Sie bundle.chapters[i].markdown, bundle.resources und bundle.config an buildDocument, wie bei jedem anderen Dokument.

Die Metadaten des Buches. Wie in der Sandbox ist die Frontmatter des ersten Kapitels die des Buches: buildBundle gibt ihr title, ihr author und den Rest an jedes Kapitel weiter, sodass Kolumnentitel mit {title} und {author} auf jeder Seite gelten und das doc.metadata jedes Kapitels sie trägt. Ein Frontmatter-Block am Anfang eines späteren Kapitels wird ignoriert: Er wird an seinen Zeilen --- erkannt und ohne Parsen geleert, sodass YAML, das der Parser ablehnen würde, keinen Schaden anrichtet. options.metadata liefert Werte, die die Frontmatter nicht festlegt (die Frontmatter hat Vorrang). Auch die Seitenzahl des Buches erreicht jedes Kapitel: {bookTotalPages} druckt sie, während {totalPages} die Seiten des Kapitels zählt (siehe Seitenzahl des ganzen Buchs).

const docs = buildBundle(bundle, { metadata: { author: 'A. Author' } });
docs[3].metadata.title;   // the first chapter's `title:`

Wo das Buch beginnt. Ein Paket kann einen Teil einer längeren Veröffentlichung enthalten: die Seiten 58 bis 61 einer Ausgabe, Kapitel 4 eines Lehrbuchs. Sein start gibt an, was davor kommt, in den Begriffen der continuation von buildDocument: die Seiten vor der ersten (pageIndexOffset, das bestimmt, auf welche Seite Seite 1 fällt, und damit die gespiegelten Ränder und die Kolumnentitel ungerader und gerader Seiten), die geltende Seitennummerierung (pageNumbering), die Zähler der Überschriften (headings), den offenen Teil (part) und die Zähler für Ressourcen, Lehrsätze, Fußnoten und Zeilen. createBundle schreibt es als start der preset.json, openBundle gibt es als bundle.start zurück, und buildBundle setzt das erste Kapitel damit, wie es buildDocument({ markdown, continuation: start }, config) täte, und verkettet die folgenden Kapitel von dort aus; {bookTotalPages} zählt auch die Seiten vor dem Buch mit. Ein Manifest ohne start wird wie bisher gelesen, und ein Leser, der das Feld nicht kennt, ignoriert es. bookPageCount gehört nicht dazu: Der Leser zählt die Seiten. In einem Paket mit mehreren Sprachen gibt localized[…].start einer Ausgabe einen eigenen Beginn.

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'

#Live-Beispiel: ein Paket öffnen

Der Pen lädt ein zweikapiteliges Beispielbuch (lantern.postext, mit eigener Schrift, einer SVG-Abbildung und einer Tabelle) aus dem Repository. Er registriert die Schriften und Bilder des Pakets, setzt das Buch mit buildBundle und zeichnet jede Seite. Make the PDF rendert dieselben Dokumente mit postext-pdf und bettet dabei die Schriften des Pakets ein. Wählen Sie eine eigene .postext-Datei, etwa eine aus der Sandbox exportierte, um sie auf dieselbe Weise zu sehen.

Postext · ein .postext-Paket öffnen
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);
}

Lädt einen interaktiven Editor von codepen.io. Das Beispiel importiert die neueste Postext-Version von einem CDN.

#Ein Paket erstellen

createBundle schreibt aus einem Dokument eine .postext-Datei: seine Kapitel, seine Konfiguration, seine Ressourcen und die Nutzdaten, auf die sie verweisen.

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 },
});
EingabeBedeutung
name, id, description, localeDie Metadaten des Manifests. id ist standardmäßig ein Slug von name.
chapters oder markdownDas Buch, ein { title?, markdown } pro Kapitel, oder ein einzelnes Dokument.
configDie PostextConfig. Werte, die den Standardwerten entsprechen, bleiben aus dem Manifest heraus.
resourcesDie Ressourcen. Ein Bild nennt seine Nutzdaten über bitmap.fileId / svg.fileId (und svg.pdfFileId für ein Druckoriginal).
filesDie Nutzdaten nach fileId (ein Objekt oder eine Map): die Bilder, auf die die Ressourcen verweisen, und die Schriftdateien, auf die die Varianten von config.customFonts verweisen. Werte können ein Uint8Array, ein ArrayBuffer, ein Blob oder ein String (SVG-Markup) sein.
thumbnail{ data, mime }: ein Umschlagbild (PNG, JPEG, WebP, GIF oder SVG).
canvasScope'book' bittet Betrachter, das ganze Buch als einen Canvas zu setzen.
startWo das Buch beginnt, für ein Paket, das einen Teil eines längeren enthält: die continuation, mit der ein einzelnes Dokument gesetzt würde. Wird als start des Manifests geschrieben.
mtimeDas Änderungsdatum, das in jede Datei des Archivs geschrieben wird (ein Date, ein Zeitstempel oder ein Datumsstring). Fehlt es, ist es die Zeit des Aufrufs, sodass zwei Aufrufe mit derselben Eingabe unterschiedliche Bytes ergeben. Übergeben Sie ein festes Datum, ergibt dieselbe Eingabe dieselben Bytes, die Sie hashen oder vergleichen können. Ein Zip speichert Datum und Uhrzeit ohne Zeitzone, in Schritten von zwei Sekunden, von 1980 bis 2099, und das Datum wird in der Ortszeit des Rechners geschrieben. Für Bytes, die auf jedem Rechner übereinstimmen, bilden Sie das Datum aus lokalen Feldern, etwa new Date(1980, 0, 1): Ein Zeitstempel oder ein String, der auf Z endet, bezeichnet einen Zeitpunkt, der in jeder Zeitzone auf eine andere Ortszeit fällt ('1980-01-01T00:00:00Z' ist westlich von UTC noch 1979). Ein Datum außerhalb dieser Jahre, in Ortszeit, wirft einen Fehler.
localizedWeitere Sprachen desselben Buches, nach Sprach-Tag: { es: { chapters?, config?, resources? } }. Die obigen Eingaben sind dann der Inhalt von locale, das in diesem Fall erforderlich ist. Siehe Zweisprachige Pakete.

Die Funktion gibt die bytes des Archivs zurück, das als preset.json geschriebene manifest, jede Datei als files (Pfad → Bytes) und eine Liste von warnings. Dateien werden nach der ID ihrer Ressource (resources/lantern.svg) oder dem Namen ihrer Schriftdatei (fonts/…) benannt, Kapitel nach ihrer Reihenfolge und ihrem Titel (chapters/01-dusk.md). Schriften werden in den fonts des Manifests deklariert, nie in config.customFonts. Manches bleibt draußen, jeweils mit einer Warnung:

  • eine Ressource oder ein Schnitt, deren Nutzdaten nicht in files stehen
  • ein Schnitt .woff (das PDF-Backend kann ihn nicht einbetten)
  • eine Familie mit der Markierung redistributable: false

Übergeben Sie bytes im Browser an einen Download-Link: URL.createObjectURL(new Blob([bytes], { type: 'application/zip' })). In Node schreiben Sie sie mit fs.writeFile. createBundle und openBundle brauchen kein DOM. Die Distributionen verwenden Modulpfade ohne Dateiendung, deshalb brauchen sie unter reinem Node, ohne Bundler, einen Resolve-Hook. docs/examples/open-bundle/build-sample.mjs im Repository zeigt einen in wenigen Zeilen.

#Zweisprachige Pakete

Eine .postext-Datei kann ein Buch in mehreren Sprachen enthalten, und openBundle(bytes, { locale }) liest es in jeder davon. createBundle schreibt eine solche Datei aus localized: ein Eintrag pro zusätzlicher Sprache, jeweils mit dem, was sich vom primären Inhalt (der Eingabe locale) unterscheidet:

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 captions
  • chapters: das Buch in dieser Sprache. Die Kapiteldateien kommen in einen Ordner pro Sprache (chapters/en/01-dusk.md, chapters/es/01-anochecer.md), und die chapters des Manifests werden zu einer Zuordnung Sprache → Kapitel. Eine Sprache ohne chapters liest die primären; hat keine Sprache eigene Kapitel, bleiben sie eine einzige Liste.
  • config: die Konfiguration für diese Sprache. Jeder Schlüssel der obersten Ebene ersetzt den gemeinsamen Schlüssel vollständig, wenn das Paket in dieser Sprache gelesen wird, sodass headings oben das ganze Objekt headings ersetzt. Weggelassene oder mit den gemeinsamen übereinstimmende Schlüssel werden geteilt und nicht geschrieben; die vollständige Konfiguration der Sprache zu übergeben funktioniert also ebenso gut wie die wenigen Schlüssel, die sich ändern. Ein Schlüssel, der auf seine Standardwerte gesetzt ist, während der gemeinsame es nicht ist (layout: {}), wird so geschrieben, wie er übergeben wurde, und setzt damit den gemeinsamen Wert zurück. Schriften werden geteilt: Die Familien aus den customFonts einer Sprache kommen zu den fonts des Pakets hinzu.
  • resources: der Wortlaut der gemeinsamen Ressourcen, zugeordnet über id: caption, note, altText und die table einer Tabelle. Ein Bild mit Wörtern darin kann eine eigene Grafik haben: bitmap.fileId oder svg.fileId (und svg.pdfFileId) nennen andere Nutzdaten in files, geschrieben als resources/es/lantern.svg. Andere Felder, etwa der Typ oder die Platzierung, werden geteilt. Eine ID, die nicht unter resources vorkommt, bleibt mit einer Warnung draußen, und ein fehlendes sprachspezifisches Bild belässt die Sprache beim gemeinsamen, ebenfalls mit einer Warnung.

Das Manifest listet jede Sprache in locales auf (['en', 'es']), behält die primäre als locale und speichert die übrigen unter localized. openBundle ohne Sprache liest die primäre Sprache.

Welche Sprache ein Leser erhält. openBundle(bytes, { locale }) liefert die genaue Sprache, sonst ihre Grundsprache (es-MX liest es), sonst die primäre, und bundle.locale gibt an, welche geliefert wurde. Kapitel und Wortlaut stammen immer aus derselben Sprache. Die primäre Sprache behält den gemeinsamen Wortlaut, auch wenn localized eine regionale Variante davon enthält: Ein Paket pt-PT mit einem Eintrag pt-BR liest die brasilianischen Legenden nur für pt-BR und die gemeinsamen für pt-PT und pt.

#Live-Beispiel: ein Paket erstellen

Der Pen baut ein zweikapiteliges Buch mit einer SVG-Abbildung und listet die Dateien auf, die createBundle geschrieben hat, zusammen mit dem Manifest. Er bietet das Archiv zum Download an, öffnet es dann mit openBundle wieder und zeichnet seine erste Seite: der vollständige Hin- und Rückweg in wenigen Zeilen. Importieren Sie die heruntergeladene Datei in die Sandbox, um dort daran weiterzuarbeiten.

Postext · ein .postext-Paket erstellen
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);
}

Lädt einen interaktiven Editor von codepen.io. Das Beispiel importiert die neueste Postext-Version von einem CDN.

#Mit Paketen arbeiten

Da die Sandbox, der Agenten-Skill und das Paket postext alle dieselbe Datei lesen und schreiben, ist eine .postext-Datei ein bequemer Weg, ein Buch von einem Werkzeug an ein anderes weiterzugeben:

  • Mit einem Paket beginnen. Portieren Sie eine bestehende Veröffentlichung mit dem Agenten-Skill, oder gestalten Sie ein Buch in der Sandbox und exportieren Sie es (Herunterladen (.postext) im Menü ⋯ seiner Zeile im Bedienfeld Bücher). Laden Sie die Datei in Ihrem Programm mit openBundle, um sie auf Canvas, als HTML oder als PDF zu rendern. Behalten Sie die Datei als Quelle des Buches: Bearbeiten Sie die Kapitel, die Konfiguration oder die Ressourcen im Code und schreiben Sie sie mit createBundle zurück, oder laden Sie sie einfach neu, wann immer sie sich ändert.
  • In der Sandbox Fehler suchen und feinabstimmen. Wenn etwas in der Ausgabe Ihres Programms Nacharbeit braucht (eine Abbildung, die auf der falschen Seite landet, ein Überschriftenformat, der Spaltenausgleich), exportieren Sie mit createBundle, was Ihr Programm setzt. Importieren Sie diese Datei in die Sandbox (Bücher → Neu → Eine .postext-Datei öffnen …), korrigieren Sie Text, Gestaltung oder Abbildungen mit der Live-Vorschau, dem Bedienfeld Prüfungen und der PDF-Ansicht, und exportieren Sie sie dann erneut. Ihr Programm lädt danach die korrigierte Datei mit openBundle. Oder übernehmen Sie die Änderungen zurück in Ihren Code: Die config des Manifests enthält nur die Werte, die von den Standardwerten abweichen, und liest sich deshalb wie ein kurzer Diff.

#API der unteren Ebene

postext/bundle exportiert außerdem die Bausteine hinter openBundle und createBundle, für Hosts, die Pakete auf eigene Weise speichern oder ausliefern (ein entpacktes Verzeichnis über HTTP, Datensätze in einer Datenbank):

  • openBundleZip(bytes) / zipBundle(files, { mtime }): die Archivschicht. Beim Öffnen wird ein Ordner der obersten Ebene toleriert, und Einträge __MACOSX sowie Dotfiles werden ignoriert. Pfade, die aus dem Paket hinausführen, werden abgewiesen. mtime datiert die Dateien wie die gleichnamige Eingabe von createBundle.
  • readBundle(manifest, readFile, options) liest ein Manifest und einen Callback readFile(path) zu Kapiteln, Konfiguration, Ressourcen, Bildern und Schriften ein. options legt die Sprache fest, wie Datei-IDs benannt werden (ids), die Basiskonfiguration (baseConfig, unter der des Manifests; standardmäßig die Palette und die Ressourcentypen von bundleBaseConfig in der Sprache des Pakets, die resolveBundleConfigLocale(manifest, locale) zurückgibt, und ein Host, der eine eigene baseConfig übergibt, sollte sie in diese Sprache lokalisieren; unter einem Manifest älter als configVersion: 4 wird ihre math mit der des Pakets fixiert, unter einem älter als 5 der Inline-Abstand ihres layout, unter einem älter als 6 der Abstand in Kästen ihres layout, der Raum unter der Doppelpunktzeile ihres bodyText, die Auszeichnungen in ihren headings und die Größen ihrer Initialen, unter einem älter als 7 die Umbrüche an Strichen und der Umbruch im Flattersatz ihres bodyText und unter einem älter als 8 die Teilung unter einer Überschrift ihrer headings sowie die Umbrüche an Komposita und der Raum unter Containern :::paragraphs ihres bodyText, siehe Pakete, die postext 1.4 oder früher geschrieben hat) und wie Eigengrößen gemessen werden. readResolution liest die Auflösung jeder Bitmap aus ihrer Datei in bitmap.fileResolution; der Standardwert ist true, wenn das Paket layout.bitmapResolution: 'file' festlegt.
  • planBundle(meta, content) / resolveBundleFiles(plan, sources): die Schreibseite, aufgeteilt in einen reinen Plan (Dateinamen und Manifest) und das Auflösen der Bytes über die Callbacks readBlob / readFont.
  • isBundleManifest(value), die Sprachwähler (pickChapterSpecs, pickLocaleOverrides, pickBundleView, resolveBundleLocale, resolveBundleConfigLocale), svgSize / bitmapSize / bitmapInfo (die Pixel einer Bitmap und die Auflösung, die ihre Datei angibt) und die Formattypen (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) und LEGACY_MATH_SIZE (0,5 ÷ 0,442): eine gespeicherte Konfiguration in den Begriffen von heute (siehe Pakete, die postext 1.4 oder früher geschrieben hat). readBundle wendet das an; ein Host, der Konfigurationen auf eigene Weise speichert, kann das ebenfalls tun, einmal pro gespeicherter Kopie.

Die Sandbox baut darauf auf. Sie ergänzt eigene Speicher-IDs und die Seitendatensätze layouts.json.