postext-epub 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/dist/__tests__/sampleBook.d.ts +17 -0
- package/dist/__tests__/sampleBook.d.ts.map +1 -0
- package/dist/__tests__/sampleBook.js +103 -0
- package/dist/__tests__/sampleBook.js.map +1 -0
- package/dist/epubcheck.test.d.ts +2 -0
- package/dist/epubcheck.test.d.ts.map +1 -0
- package/dist/epubcheck.test.js +69 -0
- package/dist/epubcheck.test.js.map +1 -0
- package/dist/fixed/fixed.test.d.ts +2 -0
- package/dist/fixed/fixed.test.d.ts.map +1 -0
- package/dist/fixed/fixed.test.js +225 -0
- package/dist/fixed/fixed.test.js.map +1 -0
- package/dist/fixed/fontUse.d.ts +21 -0
- package/dist/fixed/fontUse.d.ts.map +1 -0
- package/dist/fixed/fontUse.js +82 -0
- package/dist/fixed/fontUse.js.map +1 -0
- package/dist/fixed/index.d.ts +3 -0
- package/dist/fixed/index.d.ts.map +1 -0
- package/dist/fixed/index.js +254 -0
- package/dist/fixed/index.js.map +1 -0
- package/dist/fixed/outline.d.ts +40 -0
- package/dist/fixed/outline.d.ts.map +1 -0
- package/dist/fixed/outline.js +88 -0
- package/dist/fixed/outline.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/package/pack.d.ts +25 -0
- package/dist/package/pack.d.ts.map +1 -0
- package/dist/package/pack.js +253 -0
- package/dist/package/pack.js.map +1 -0
- package/dist/package/pack.test.d.ts +2 -0
- package/dist/package/pack.test.d.ts.map +1 -0
- package/dist/package/pack.test.js +164 -0
- package/dist/package/pack.test.js.map +1 -0
- package/dist/package/read.d.ts +15 -0
- package/dist/package/read.d.ts.map +1 -0
- package/dist/package/read.js +198 -0
- package/dist/package/read.js.map +1 -0
- package/dist/package/strings.d.ts +17 -0
- package/dist/package/strings.d.ts.map +1 -0
- package/dist/package/strings.js +24 -0
- package/dist/package/strings.js.map +1 -0
- package/dist/reflowable/__tests__/vdt.d.ts +13 -0
- package/dist/reflowable/__tests__/vdt.d.ts.map +1 -0
- package/dist/reflowable/__tests__/vdt.js +55 -0
- package/dist/reflowable/__tests__/vdt.js.map +1 -0
- package/dist/reflowable/__tests__/xml.d.ts +8 -0
- package/dist/reflowable/__tests__/xml.d.ts.map +1 -0
- package/dist/reflowable/__tests__/xml.js +107 -0
- package/dist/reflowable/__tests__/xml.js.map +1 -0
- package/dist/reflowable/css.d.ts +45 -0
- package/dist/reflowable/css.d.ts.map +1 -0
- package/dist/reflowable/css.js +491 -0
- package/dist/reflowable/css.js.map +1 -0
- package/dist/reflowable/index.d.ts +4 -0
- package/dist/reflowable/index.d.ts.map +1 -0
- package/dist/reflowable/index.js +257 -0
- package/dist/reflowable/index.js.map +1 -0
- package/dist/reflowable/inline.d.ts +56 -0
- package/dist/reflowable/inline.d.ts.map +1 -0
- package/dist/reflowable/inline.js +334 -0
- package/dist/reflowable/inline.js.map +1 -0
- package/dist/reflowable/model.d.ts +261 -0
- package/dist/reflowable/model.d.ts.map +1 -0
- package/dist/reflowable/model.js +6 -0
- package/dist/reflowable/model.js.map +1 -0
- package/dist/reflowable/reflowable.test.d.ts +2 -0
- package/dist/reflowable/reflowable.test.d.ts.map +1 -0
- package/dist/reflowable/reflowable.test.js +469 -0
- package/dist/reflowable/reflowable.test.js.map +1 -0
- package/dist/reflowable/rtl.test.d.ts +2 -0
- package/dist/reflowable/rtl.test.d.ts.map +1 -0
- package/dist/reflowable/rtl.test.js +217 -0
- package/dist/reflowable/rtl.test.js.map +1 -0
- package/dist/reflowable/walk.d.ts +42 -0
- package/dist/reflowable/walk.d.ts.map +1 -0
- package/dist/reflowable/walk.js +1096 -0
- package/dist/reflowable/walk.js.map +1 -0
- package/dist/reflowable/xhtml.d.ts +33 -0
- package/dist/reflowable/xhtml.d.ts.map +1 -0
- package/dist/reflowable/xhtml.js +472 -0
- package/dist/reflowable/xhtml.js.map +1 -0
- package/dist/shared/accessibility.d.ts +20 -0
- package/dist/shared/accessibility.d.ts.map +1 -0
- package/dist/shared/accessibility.js +30 -0
- package/dist/shared/accessibility.js.map +1 -0
- package/dist/shared/assets.d.ts +46 -0
- package/dist/shared/assets.d.ts.map +1 -0
- package/dist/shared/assets.js +143 -0
- package/dist/shared/assets.js.map +1 -0
- package/dist/shared/assets.test.d.ts +2 -0
- package/dist/shared/assets.test.d.ts.map +1 -0
- package/dist/shared/assets.test.js +106 -0
- package/dist/shared/assets.test.js.map +1 -0
- package/dist/shared/media.d.ts +11 -0
- package/dist/shared/media.d.ts.map +1 -0
- package/dist/shared/media.js +52 -0
- package/dist/shared/media.js.map +1 -0
- package/dist/shared/uuid.d.ts +7 -0
- package/dist/shared/uuid.d.ts.map +1 -0
- package/dist/shared/uuid.js +63 -0
- package/dist/shared/uuid.js.map +1 -0
- package/dist/shared/xml.d.ts +54 -0
- package/dist/shared/xml.d.ts.map +1 -0
- package/dist/shared/xml.js +214 -0
- package/dist/shared/xml.js.map +1 -0
- package/dist/shared/xml.test.d.ts +2 -0
- package/dist/shared/xml.test.d.ts.map +1 -0
- package/dist/shared/xml.test.js +71 -0
- package/dist/shared/xml.test.js.map +1 -0
- package/dist/shared/xmlRead.d.ts +19 -0
- package/dist/shared/xmlRead.d.ts.map +1 -0
- package/dist/shared/xmlRead.js +85 -0
- package/dist/shared/xmlRead.js.map +1 -0
- package/dist/types.d.ts +205 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/worker/client.d.ts +39 -0
- package/dist/worker/client.d.ts.map +1 -0
- package/dist/worker/client.js +184 -0
- package/dist/worker/client.js.map +1 -0
- package/dist/worker/epub.worker.d.ts +2 -0
- package/dist/worker/epub.worker.d.ts.map +1 -0
- package/dist/worker/epub.worker.js +7 -0
- package/dist/worker/epub.worker.js.map +1 -0
- package/dist/worker/protocol.d.ts +44 -0
- package/dist/worker/protocol.d.ts.map +1 -0
- package/dist/worker/protocol.js +6 -0
- package/dist/worker/protocol.js.map +1 -0
- package/dist/worker/serve.d.ts +11 -0
- package/dist/worker/serve.d.ts.map +1 -0
- package/dist/worker/serve.js +53 -0
- package/dist/worker/serve.js.map +1 -0
- package/dist/worker/worker.test.d.ts +2 -0
- package/dist/worker/worker.test.d.ts.map +1 -0
- package/dist/worker/worker.test.js +168 -0
- package/dist/worker/worker.test.js.map +1 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Postext contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# postext-epub
|
|
2
|
+
|
|
3
|
+
**EPUB 3 writer for [postext](https://www.npmjs.com/package/postext).**
|
|
4
|
+
|
|
5
|
+
`postext-epub` writes a book that `postext` has laid out as an EPUB 3.3 file, in the browser or in Node, with no server. It reads the same per-chapter `VDTDocument`s the PDF backend renders, so page numbers, footnotes, citations, cross-references, the contents and the index arrive already resolved. The whole file is built in memory and returned as bytes.
|
|
6
|
+
|
|
7
|
+
A book comes out in one of the two renditions EPUB 3 defines (the `rendition:layout` property):
|
|
8
|
+
|
|
9
|
+
- **Fixed layout** (`layout: 'fixed'`, EPUB `pre-paginated`): every printed page as it is, one XHTML document per page, with real, selectable text placed where the PDF places it.
|
|
10
|
+
- **Reflowable** (`layout: 'reflowable'`, the EPUB default): the text, figures and tables in reading order, as semantic XHTML that the reading system sets again for its screen, typeface and type size.
|
|
11
|
+
|
|
12
|
+
Right-to-left books (Arabic, Persian, Hebrew…) declare their language and direction on every document and on the navigation, and a right-bound book turns its pages to the left (`page-progression-direction="rtl"`). In the reflowable rendition a run set against its paragraph (a Latin name in Arabic, an Arabic quotation in English) is a `dir` isolate, a language named on `:ltr[…]{lang=en}` is declared, a `:::verse` poem sets each bayt as two hemistichs side by side, and the tatweels kashida justification inserted are left out (the reading system justifies; tatweels the author typed stay). The fixed layout paints them as printed, out of reach of a text selection.
|
|
13
|
+
|
|
14
|
+
**Website:** [postext.dev](https://postext.dev/) · **Docs:** [EPUB books](https://postext.dev/en/docs/configuration#epub-books-postext-epub)
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install postext postext-epub
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`postext` is a peer dependency. Each `postext-epub` release needs the `postext` it was released with, or a later one of the same major, so upgrade the two together. Or import both straight from a CDN:
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { buildDocument } from 'https://esm.sh/postext';
|
|
26
|
+
import { renderToEpub } from 'https://esm.sh/postext-epub';
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick example
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { buildDocument } from 'postext';
|
|
33
|
+
import { renderToEpub } from 'postext-epub';
|
|
34
|
+
|
|
35
|
+
// Postext measures text with the fonts the browser has loaded.
|
|
36
|
+
await document.fonts.load('16px "EB Garamond"');
|
|
37
|
+
|
|
38
|
+
const doc = buildDocument({ markdown: '# Lantern\n\nTypeset me.' }, {
|
|
39
|
+
page: { sizePreset: '17x24' },
|
|
40
|
+
bodyText: { fontFamily: 'EB Garamond' },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const bytes = await renderToEpub([doc], {
|
|
44
|
+
layout: 'reflowable', // or 'fixed'
|
|
45
|
+
metadata: { title: 'Lantern', creators: ['Ada Lovelace'], language: 'en' },
|
|
46
|
+
fonts: [
|
|
47
|
+
{ family: 'EB Garamond', weight: 400, style: 'normal', bytes: garamondRegular, format: 'woff2' },
|
|
48
|
+
{ family: 'EB Garamond', weight: 400, style: 'italic', bytes: garamondItalic, format: 'woff2' },
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const url = URL.createObjectURL(new Blob([bytes], { type: 'application/epub+zip' }));
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## A whole book
|
|
56
|
+
|
|
57
|
+
`renderToEpub` takes the book's chapters in order, each laid out as a continuation of the one before (counters, page parity, page numbers), which is what `buildBundle` returns for a `.postext` file:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { openBundle, buildBundle } from 'postext';
|
|
61
|
+
import { renderToEpub } from 'postext-epub';
|
|
62
|
+
|
|
63
|
+
const bundle = await openBundle(fileBytes, { locale: 'es' });
|
|
64
|
+
const docs = buildBundle(bundle); // one VDTDocument per chapter
|
|
65
|
+
|
|
66
|
+
const bytes = await renderToEpub(docs, {
|
|
67
|
+
layout: 'fixed',
|
|
68
|
+
metadata: { title: 'Don Quijote', creators: ['Miguel de Cervantes'], language: 'es' },
|
|
69
|
+
fonts: bundle.fonts.map((f) => ({ family: f.family, weight: f.weight, style: f.style, bytes: new Uint8Array(f.bytes), format: f.format })),
|
|
70
|
+
resourceBytes: (fileId) => {
|
|
71
|
+
const data = bundle.files.get(fileId);
|
|
72
|
+
return data ? { bytes: data, mediaType: '' } : undefined; // '' = read the type from the bytes
|
|
73
|
+
},
|
|
74
|
+
onProgress: ({ phase, done, total }) => console.log(phase, done, total),
|
|
75
|
+
onWarning: (w) => console.warn(w),
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The same array is what `renderToPdf` from `postext-pdf` takes for a book, so a PDF and an EPUB of one book come from one layout.
|
|
80
|
+
|
|
81
|
+
## API
|
|
82
|
+
|
|
83
|
+
### `renderToEpub(docs, options): Promise<Uint8Array>`
|
|
84
|
+
|
|
85
|
+
`docs` is the book's print documents, one per chapter in book order (at least one). The result is the `.epub` file.
|
|
86
|
+
|
|
87
|
+
<table>
|
|
88
|
+
<thead>
|
|
89
|
+
<tr><th>Option</th><th>What it does</th></tr>
|
|
90
|
+
</thead>
|
|
91
|
+
<tbody>
|
|
92
|
+
<tr><td><code>layout</code></td><td>Required. <code>'fixed'</code> or <code>'reflowable'</code>.</td></tr>
|
|
93
|
+
<tr><td><code>metadata</code></td><td>Required. <code>title</code> and <code>language</code> (a BCP 47 tag), and optionally <code>subtitle</code>, <code>creators</code> (authors in order), <code>identifier</code>, <code>date</code> (ISO 8601), <code>publisher</code>, <code>rights</code>, <code>description</code> and <code>modified</code>.</td></tr>
|
|
94
|
+
<tr><td><code>fonts</code></td><td>The faces to embed: <code>{ family, weight, style, bytes, format, unicodeRange? }</code>, with <code>format</code> one of <code>woff2</code>, <code>woff</code>, <code>ttf</code>, <code>otf</code>. Each face becomes a file and an <code>@font-face</code> rule. Several files of one face, each with its <code>unicodeRange</code> (Google Fonts slices), are allowed.</td></tr>
|
|
95
|
+
<tr><td><code>resourceBytes</code></td><td><code>(fileId) => { bytes, mediaType } | undefined</code>, sync or async: the pictures the pages place, by file id. Bitmaps as they are, SVGs as their source (not a PDF print master). An empty <code>mediaType</code> is read from the bytes. A single-ink book (<code>diagramStyle.singleInk</code>) has its SVGs recoloured in the file.</td></tr>
|
|
96
|
+
<tr><td><code>cover</code></td><td><code>{ bytes, mediaType, alt? }</code>: a JPEG, PNG, WebP or SVG cover picture. It opens the book on a cover document of its own and is marked as the <code>cover-image</code>. Without it, the fixed layout names its first page as the cover, and the reflowable book has no cover picture.</td></tr>
|
|
97
|
+
<tr><td><code>onProgress</code></td><td><code>{ phase, done, total }</code>, with <code>phase</code> one of <code>resources</code> (fonts and pictures), <code>documents</code> (pages for a fixed layout, chapters for a reflowable one) and <code>package</code> (the package document, the navigation and the zip).</td></tr>
|
|
98
|
+
<tr><td><code>onWarning</code></td><td>Problems that do not stop the file: <code>missingImage</code> (a picture with no bytes, left as an empty frame), <code>missingFont</code> (a family, weight or style the pages use with no embedded face, which reading systems replace) and <code>unsupported</code>.</td></tr>
|
|
99
|
+
<tr><td><code>signal</code></td><td>An <code>AbortSignal</code>; aborting rejects the promise between steps.</td></tr>
|
|
100
|
+
</tbody>
|
|
101
|
+
</table>
|
|
102
|
+
|
|
103
|
+
Without an `identifier`, the book gets a `urn:uuid:` derived from its title, creators and language, so writing the same book again keeps its identity in a reader's library. A bare ISBN becomes `urn:isbn:…`. Pass `modified` as well for byte-identical output from the same input.
|
|
104
|
+
|
|
105
|
+
### Writing on a worker: `createEpubWorker()` (`postext-epub/worker`)
|
|
106
|
+
|
|
107
|
+
`renderToEpub` keeps the thread it runs on busy while it writes; a fixed layout of a long book takes a second or more. In a page, write the book on a worker instead:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { createEpubWorker } from 'postext-epub/worker';
|
|
111
|
+
|
|
112
|
+
const epubWorker = createEpubWorker();
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
const bytes = await epubWorker.render(docs, {
|
|
115
|
+
layout: 'fixed',
|
|
116
|
+
metadata: { title: 'My book', language: 'en' },
|
|
117
|
+
fonts,
|
|
118
|
+
resourceBytes, // a function, as for renderToEpub, or a Map by fileId
|
|
119
|
+
onProgress: (p) => console.log(p.phase, p.done, p.total),
|
|
120
|
+
onWarning: (w) => console.warn(w),
|
|
121
|
+
signal: controller.signal,
|
|
122
|
+
});
|
|
123
|
+
epubWorker.dispose(); // when the page no longer needs it
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`render(docs, options)` takes the options of `renderToEpub` and returns the same bytes. What travels to the worker:
|
|
127
|
+
|
|
128
|
+
- The documents are copied (structured clone).
|
|
129
|
+
- A `resourceBytes` function is asked on the calling thread for every picture the pages place before the worker starts, since the worker cannot reach the host's stores. A `Map` of `{ bytes, mediaType }` by file id is sent as it is.
|
|
130
|
+
- The buffers of the fonts, the pictures and the cover are transferred, not copied, so the caller's views of them are empty afterwards. Pass copies of bytes you keep.
|
|
131
|
+
- The finished file comes back transferred.
|
|
132
|
+
|
|
133
|
+
Progress and warnings are forwarded as they happen. Aborting the signal rejects at once with an `AbortError` and stops the worker, and the next render starts a fresh one.
|
|
134
|
+
|
|
135
|
+
The worker script is `new URL('./epub.worker.js', import.meta.url)`, which webpack, Vite and esbuild bundle. A host that builds its own worker passes it as `createEpubWorker({ worker })`, a `Worker` or a function that makes one. Its script imports `postext-epub/worker/entry`. A worker that fails to load rejects with an `EpubWorkerError`, and the host can then call `renderToEpub` on its own thread.
|
|
136
|
+
|
|
137
|
+
### `readEpub(bytes): ReadEpubResult`
|
|
138
|
+
|
|
139
|
+
Reads an EPUB back for a viewer, without `DOMParser`: the layout, the metadata, the reading direction, every file by its zip path, the manifest, the spine in reading order, the table of contents (from the navigation document, or the NCX of an older file), the page list, the fixed-layout viewport and the cover picture's path. `resolveHref(base, href)` and `dirOf(path)` resolve the links between those files.
|
|
140
|
+
|
|
141
|
+
### Lower-level exports
|
|
142
|
+
|
|
143
|
+
`buildFixedPublication` and `buildReflowablePublication` return the publication model (`EpubPublication`: items, spine, navigation, accessibility metadata) without packing it, and `packEpub(publication)` writes such a model as a file. `bookIdentifier` and `uuidV5` compute the identifier `renderToEpub` uses.
|
|
144
|
+
|
|
145
|
+
## What each layout produces
|
|
146
|
+
|
|
147
|
+
### Fixed layout
|
|
148
|
+
|
|
149
|
+
- One XHTML document per printed page, from the HTML backend's markup for that page. The viewport is the trimmed page in CSS px; the cut-line margin is cropped.
|
|
150
|
+
- Text is real text in the embedded fonts, so it can be selected, searched and read aloud. Formulas are inline SVG; pictures are files, each stored once.
|
|
151
|
+
- `page-spread-left` and `page-spread-right` follow the page parity and the binding. A right-bound book (Arabic, vertical Chinese) has `rtl` page progression, and its pages run right to left.
|
|
152
|
+
- Links work between pages: cross-references, footnotes, the printed contents and the index. Links to an element the book does not hold are dropped.
|
|
153
|
+
- The table of contents comes from the headings and the part pages, the page list from the printed page labels (`iv`, `12`), and the landmarks point at the cover, the printed contents and the start of the body.
|
|
154
|
+
|
|
155
|
+
### Reflowable
|
|
156
|
+
|
|
157
|
+
- One XHTML document per chapter (`text/chapter-NNN.xhtml`). A part opens a document of its own.
|
|
158
|
+
- Paragraphs are rebuilt from the laid-out lines: the hyphens the line breaks added are removed, hyphens that belong to the word are kept, and a paragraph split across pages or columns is one paragraph again.
|
|
159
|
+
- Figures and tables follow the text that cites them, with their numbered captions and alt text. Tables are real tables with header rows and merged cells. Lists nest, boxes become `<aside>` with their titles, heading levels never skip, and formulas are SVG labelled with their TeX.
|
|
160
|
+
- Footnotes link to the notes at the end of the chapter and back. Cross-references, citations and web links work. The printed contents link to the headings, and the index's page numbers link to page markers.
|
|
161
|
+
- Each printed page leaves a page-break marker where its text starts, so the page list gives the print page numbers.
|
|
162
|
+
- A stylesheet is derived from the configuration: sizes relative to the body text, the book's colours, justification with automatic hyphenation, indents, and styles for headings, boxes, tables, captions, lists and notes. Vertical Chinese keeps vertical writing; Arabic documents are `dir="rtl"`.
|
|
163
|
+
- Paragraphs set in a paragraph style (`:::paragraphs{style=…}`, a `:::verse` fence's style) carry its class, inside boxes too, and index entries are classed by their level, both as the layout names them (`VDTBlock.paragraphStyleId`, `VDTBlock.indexLevel`). Documents laid out by an older engine fall back to telling them from the face and the indent.
|
|
164
|
+
- The stylesheet follows the first chapter's configuration. A chapter configured otherwise (its own heading styles, say), and every document of a part whose `palette` recolours the book, links a second stylesheet after it (`styles/book-2.css`…) with only the rules that change; documents that change the same share it.
|
|
165
|
+
- A pull quote repeats words of the text, so it would be read twice. A box with no title (or a title of marks alone, such as a hanging quotation mark) whose words the chapter's text also has is kept on the page as `<aside epub:type="pullquote" role="doc-pullquote" aria-hidden="true">`: readers see it, assistive technology and read-aloud skip it, and the words are read once, in the text. A box whose style id names it a pull quote but whose words are its own is marked `epub:type="pullquote"` and read.
|
|
166
|
+
|
|
167
|
+
Both renditions carry the EPUB Accessibility 1.1 metadata (access modes, features such as the table of contents and print page numbers, hazards and a summary), and neither claims WCAG conformance by default.
|
|
168
|
+
|
|
169
|
+
## Limitations
|
|
170
|
+
|
|
171
|
+
- A part's palette recolours the colours equal to the palette entries it overrides, as the printed text does; two entries that share a colour take the last override.
|
|
172
|
+
- A pull quote edited beyond leaving words out (an ellipsis) does not match the text, and is read as well.
|
|
173
|
+
- Fonts are embedded as given. Respect the font licences: leave out families that may not be redistributed.
|
|
174
|
+
- `renderToEpub` writes on the calling thread; use `createEpubWorker` in a page. The writer checks the signal between steps, so a call on the page's own thread stops at the next step, not at once.
|
|
175
|
+
|
|
176
|
+
## Checking output with EPUBCheck
|
|
177
|
+
|
|
178
|
+
Output is checked with [W3C EPUBCheck](https://www.w3.org/publishing/epubcheck/) 5.4. In this repository, with `epubcheck` on the `PATH`:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
pnpm --filter postext-epub epubcheck # sample books built in the tests
|
|
182
|
+
pnpm --filter postext-epub validate # the guide, the showcase presets and Cookbook books, both layouts
|
|
183
|
+
node packages/postext-epub/scripts/epubcheck.mjs path/to/book.postext --lang es --layout both --out /tmp/epub
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- `epubcheck` (`src/epubcheck.test.ts`) writes the test suite's sample books (fixed, fixed with a cover, reflowable, reflowable with a cover) and fails on any EPUBCheck error or warning. Without the command it is skipped; `EPUBCHECK_OUT=dir` keeps the files.
|
|
187
|
+
- `scripts/epubcheck.mjs` lays out one `.postext` file or preset folder with its own fonts, writes the EPUB in one or both layouts and runs EPUBCheck on it. Options: `--lang`, `--layout fixed|reflowable|both`, `--out`, `--chapters 0,2`, `--report result.json`, `--no-cover`. For the reflowable book it also compares the letters of the laid-out text with those of the EPUB, so a lost or repeated paragraph shows.
|
|
188
|
+
- `scripts/validate.mjs` runs `epubcheck.mjs` on the whole matrix of books and prints a table; `--only name,name`, `--jobs N`, `--full` (all of Hong Lou Meng). It exits with 1 when any book has an EPUBCheck error or warning.
|
|
189
|
+
|
|
190
|
+
The scripts read this checkout's built dists: run `npx tsc` in `packages/postext`, `postext-pdf`, `postext-citeproc` and `postext-epub` first (`validate` builds `postext-epub` itself). The usage notes EPUBCheck leaves (`CSS-028` for `@font-face`, `OBS-001` for the NCX kept for older readers, `HTM_062` for `xlink:href` in formulas) are informational.
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PostextConfig, VDTDocument } from 'postext';
|
|
2
|
+
export declare const LORA: Uint8Array<ArrayBuffer>;
|
|
3
|
+
export declare const PNG: Uint8Array<ArrayBuffer>;
|
|
4
|
+
export declare const SAMPLE_CHAPTERS: string[];
|
|
5
|
+
export declare function sampleBook(): VDTDocument[];
|
|
6
|
+
/** Lay out chapters as a book with the sample page and a config of the
|
|
7
|
+
* caller's on top. */
|
|
8
|
+
export declare function layOutBook(chapters: string[], overrides?: PostextConfig): VDTDocument[];
|
|
9
|
+
/** Amiri (OFL), the subset the PDF backend's Arabic tests embed. */
|
|
10
|
+
export declare const AMIRI: Uint8Array<ArrayBuffer>;
|
|
11
|
+
/** A right-to-left book (#402): Arabic chapters, right-bound, with Latin
|
|
12
|
+
* and digit runs, an English isolate naming its language, «(١)» notes,
|
|
13
|
+
* justified text stretched with kashidas, a `:::verse` poem and an
|
|
14
|
+
* English block. */
|
|
15
|
+
export declare const ARABIC_CHAPTERS: string[];
|
|
16
|
+
export declare function arabicSampleBook(): VDTDocument[];
|
|
17
|
+
//# sourceMappingURL=sampleBook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sampleBook.d.ts","sourceRoot":"","sources":["../../src/__tests__/sampleBook.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAY,WAAW,EAAE,MAAM,SAAS,CAAC;AAwBpE,eAAO,MAAM,IAAI,yBAAkH,CAAC;AAEpI,eAAO,MAAM,GAAG,yBAA6I,CAAC;AA2B9J,eAAO,MAAM,eAAe,UAG3B,CAAC;AAEF,wBAAgB,UAAU,IAAI,WAAW,EAAE,CAE1C;AAED;uBACuB;AACvB,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,SAAS,GAAE,aAAkB,GAAG,WAAW,EAAE,CAE3F;AAED,oEAAoE;AACpE,eAAO,MAAM,KAAK,yBAAmI,CAAC;AAEtJ;;;qBAGqB;AACrB,eAAO,MAAM,eAAe,UAoB3B,CAAC;AAEF,wBAAgB,gBAAgB,IAAI,WAAW,EAAE,CAWhD"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// A real two-chapter book laid out in node for the integration tests and
|
|
2
|
+
// the EPUBCheck samples: a figure with alternative text, headings at two
|
|
3
|
+
// levels, a cross-reference into the other chapter and a page footer.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { buildBundle } from 'postext';
|
|
6
|
+
// Text measured with a fixed advance (half an em per character): stable
|
|
7
|
+
// line breaks without a font file or a DOM.
|
|
8
|
+
class StubCtx {
|
|
9
|
+
font = '';
|
|
10
|
+
letterSpacing = '0px';
|
|
11
|
+
measureText(s) {
|
|
12
|
+
const size = Number(/(\d+(?:\.\d+)?)px/.exec(this.font)?.[1] ?? 16);
|
|
13
|
+
return {
|
|
14
|
+
width: [...s].length * size * 0.5,
|
|
15
|
+
actualBoundingBoxAscent: size * 0.7,
|
|
16
|
+
actualBoundingBoxDescent: size * 0.2,
|
|
17
|
+
fontBoundingBoxAscent: size * 0.8,
|
|
18
|
+
fontBoundingBoxDescent: size * 0.2,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
globalThis.OffscreenCanvas = class {
|
|
23
|
+
getContext() {
|
|
24
|
+
return new StubCtx();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
export const LORA = new Uint8Array(fs.readFileSync(new URL('../../../../apps/web/public/fonts/Lora-Regular.ttf', import.meta.url)));
|
|
28
|
+
// 1×1 PNG.
|
|
29
|
+
export const PNG = Uint8Array.from(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==', 'base64'));
|
|
30
|
+
const pt = (value) => ({ value, unit: 'pt' });
|
|
31
|
+
const config = {
|
|
32
|
+
page: { width: pt(360), height: pt(480), margins: { top: pt(24), bottom: pt(24), left: pt(24), right: pt(24) } },
|
|
33
|
+
locale: 'en-us',
|
|
34
|
+
footer: {
|
|
35
|
+
elements: [{
|
|
36
|
+
kind: 'text', id: 'pn', content: '{pageNumber}', fontSize: pt(8), overflow: 'ellipsis-end',
|
|
37
|
+
placement: { anchor: { to: 'container', edge: 'bottom' }, size: { width: 'auto', height: 'auto' } },
|
|
38
|
+
}],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
const resources = [{
|
|
42
|
+
id: 'f1',
|
|
43
|
+
typeId: 'figure',
|
|
44
|
+
kind: 'bitmap',
|
|
45
|
+
caption: 'A square.',
|
|
46
|
+
altText: 'A red square on white',
|
|
47
|
+
createdAt: 0,
|
|
48
|
+
updatedAt: 0,
|
|
49
|
+
bitmap: { fileId: 'f1.png', format: 'png', width: 400, height: 300 },
|
|
50
|
+
}];
|
|
51
|
+
const para = 'Body text that runs on for a while so the page fills and the paragraph wraps over several lines. ';
|
|
52
|
+
export const SAMPLE_CHAPTERS = [
|
|
53
|
+
['---', 'title: Sample book', 'author: Ada Lovelace', '---', '', '# Opening chapter {#opening}', '', `See :ref{id=f1} and the [closing words](#closing). ${para.repeat(3)}`, '', '::resource{id=f1}', '', '## A section', '', para.repeat(12)].join('\n'),
|
|
54
|
+
['# Second chapter', '', `Back to the [opening](#opening). ${para.repeat(6)}`, '', '## Closing words {#closing}', '', para.repeat(4)].join('\n'),
|
|
55
|
+
];
|
|
56
|
+
export function sampleBook() {
|
|
57
|
+
return buildBundle({ chapters: SAMPLE_CHAPTERS.map((markdown) => ({ markdown })), config, resources });
|
|
58
|
+
}
|
|
59
|
+
/** Lay out chapters as a book with the sample page and a config of the
|
|
60
|
+
* caller's on top. */
|
|
61
|
+
export function layOutBook(chapters, overrides = {}) {
|
|
62
|
+
return buildBundle({ chapters: chapters.map((markdown) => ({ markdown })), config: { ...config, ...overrides } });
|
|
63
|
+
}
|
|
64
|
+
/** Amiri (OFL), the subset the PDF backend's Arabic tests embed. */
|
|
65
|
+
export const AMIRI = new Uint8Array(fs.readFileSync(new URL('../../../postext-pdf/src/__tests__/fixtures/arabic/amiri-subset.ttf', import.meta.url)));
|
|
66
|
+
/** A right-to-left book (#402): Arabic chapters, right-bound, with Latin
|
|
67
|
+
* and digit runs, an English isolate naming its language, «(١)» notes,
|
|
68
|
+
* justified text stretched with kashidas, a `:::verse` poem and an
|
|
69
|
+
* English block. */
|
|
70
|
+
export const ARABIC_CHAPTERS = [
|
|
71
|
+
[
|
|
72
|
+
'# الفصل الأول',
|
|
73
|
+
'',
|
|
74
|
+
`قال الراوي إن الملك AAA كان يحكم سنة ١٤٤٥ هـ (2024 م)[^a]. ${'قال الراوي إن الملك شهريار كان يحكم بلاد الهند والصين، وكان له أخ اسمه شاه زمان. '.repeat(4)}`,
|
|
75
|
+
'',
|
|
76
|
+
'وقال الشيخ :ltr[the old man]{lang=en} ثم سكت وكتب جمـيل.',
|
|
77
|
+
'',
|
|
78
|
+
':::verse{ornament="٭"}',
|
|
79
|
+
'يا حرقة الدهر كفي || إن لم تكفي فعفي',
|
|
80
|
+
'فلا بحظي أعطي || ولا بصنعة كفي',
|
|
81
|
+
':::',
|
|
82
|
+
'',
|
|
83
|
+
':::paragraphs{dir=ltr}',
|
|
84
|
+
'An English paragraph with the word كتاب in it.',
|
|
85
|
+
':::',
|
|
86
|
+
'',
|
|
87
|
+
'[^a]: حاشية NOTE-A.',
|
|
88
|
+
].join('\n'),
|
|
89
|
+
['# الفصل الثاني', '', 'قال الراوي إن الملك شهريار كان يحكم بلاد الهند والصين. '.repeat(10)].join('\n'),
|
|
90
|
+
];
|
|
91
|
+
export function arabicSampleBook() {
|
|
92
|
+
return buildBundle({
|
|
93
|
+
chapters: ARABIC_CHAPTERS.map((markdown) => ({ markdown })),
|
|
94
|
+
config: {
|
|
95
|
+
...config,
|
|
96
|
+
locale: 'ar',
|
|
97
|
+
bodyText: { fontFamily: 'Amiri', textAlign: 'justify' },
|
|
98
|
+
headings: { fontFamily: 'Amiri' },
|
|
99
|
+
footnotes: { markerTemplate: '({n})', numbering: 'page' },
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=sampleBook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sampleBook.js","sourceRoot":"","sources":["../../src/__tests__/sampleBook.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"epubcheck.test.d.ts","sourceRoot":"","sources":["../src/epubcheck.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// W3C EPUBCheck on generated sample books. Runs only when asked
|
|
2
|
+
// (`pnpm epubcheck`, or EPUBCHECK=1) and the `epubcheck` command is on the
|
|
3
|
+
// PATH: it takes a few seconds of Java per book. Set EPUBCHECK_OUT to keep
|
|
4
|
+
// the files.
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { renderToEpub } from './index';
|
|
11
|
+
import { AMIRI, LORA, PNG, arabicSampleBook, sampleBook } from './__tests__/sampleBook';
|
|
12
|
+
const available = process.env.EPUBCHECK === '1' && spawnSync('epubcheck', ['--version'], { encoding: 'utf8' }).status === 0;
|
|
13
|
+
function epubcheck(file) {
|
|
14
|
+
const out = spawnSync('epubcheck', ['--json', '-', file], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
15
|
+
const report = JSON.parse(out.stdout);
|
|
16
|
+
return report.messages.filter((m) => m.severity === 'ERROR' || m.severity === 'FATAL' || m.severity === 'WARNING');
|
|
17
|
+
}
|
|
18
|
+
const base = (layout) => ({
|
|
19
|
+
layout,
|
|
20
|
+
metadata: {
|
|
21
|
+
title: 'Sample book',
|
|
22
|
+
subtitle: 'A test of both renditions',
|
|
23
|
+
creators: ['Ada Lovelace'],
|
|
24
|
+
language: 'en-US',
|
|
25
|
+
date: '2026-10-04',
|
|
26
|
+
publisher: 'Postext',
|
|
27
|
+
modified: new Date(Date.UTC(2026, 9, 4)),
|
|
28
|
+
},
|
|
29
|
+
fonts: [{ family: 'Lora', weight: 400, style: 'normal', bytes: LORA, format: 'ttf' }],
|
|
30
|
+
resourceBytes: (fileId) => fileId === 'f1.png' ? { bytes: PNG, mediaType: 'image/png' } : undefined,
|
|
31
|
+
});
|
|
32
|
+
/** The right-to-left sample (#402), in Arabic. */
|
|
33
|
+
const arabic = (layout) => ({
|
|
34
|
+
layout,
|
|
35
|
+
metadata: { title: 'كتاب تجريبي', creators: ['المؤلف'], language: 'ar', modified: new Date(Date.UTC(2026, 9, 4)) },
|
|
36
|
+
fonts: [{ family: 'Amiri', weight: 400, style: 'normal', bytes: AMIRI, format: 'ttf' }],
|
|
37
|
+
cover: { bytes: PNG, mediaType: 'image/png', alt: 'غلاف الكتاب' },
|
|
38
|
+
});
|
|
39
|
+
const samples = [
|
|
40
|
+
{ name: 'fixed', options: base('fixed') },
|
|
41
|
+
{ name: 'fixed-cover', options: { ...base('fixed'), cover: { bytes: PNG, mediaType: 'image/png', alt: 'A red square' } } },
|
|
42
|
+
{ name: 'reflowable', options: base('reflowable') },
|
|
43
|
+
{ name: 'reflowable-cover', options: { ...base('reflowable'), cover: { bytes: PNG, mediaType: 'image/png', alt: 'A red square' } } },
|
|
44
|
+
{ name: 'arabic-fixed', options: arabic('fixed'), book: arabicSampleBook },
|
|
45
|
+
{ name: 'arabic-reflowable', options: arabic('reflowable'), book: arabicSampleBook },
|
|
46
|
+
];
|
|
47
|
+
describe.skipIf(!available)('EPUBCheck', () => {
|
|
48
|
+
const dir = process.env.EPUBCHECK_OUT ?? fs.mkdtempSync(path.join(os.tmpdir(), 'postext-epub-'));
|
|
49
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
50
|
+
for (const sample of samples) {
|
|
51
|
+
it(`${sample.name}: 0 errors, 0 warnings`, async () => {
|
|
52
|
+
let bytes;
|
|
53
|
+
try {
|
|
54
|
+
bytes = await renderToEpub((sample.book ?? sampleBook)(), sample.options);
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
// A rendition still being written is not checked yet.
|
|
58
|
+
if (/not implemented/.test(String(e)))
|
|
59
|
+
return;
|
|
60
|
+
throw e;
|
|
61
|
+
}
|
|
62
|
+
const file = path.join(dir, `${sample.name}.epub`);
|
|
63
|
+
fs.writeFileSync(file, bytes);
|
|
64
|
+
const messages = epubcheck(file);
|
|
65
|
+
expect(messages.map((m) => `${m.severity} ${m.ID} ${m.locations[0]?.path ?? ''}:${m.locations[0]?.line ?? ''} ${m.message}`)).toEqual([]);
|
|
66
|
+
}, 120_000);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
//# sourceMappingURL=epubcheck.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"epubcheck.test.js","sourceRoot":"","sources":["../src/epubcheck.test.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,2EAA2E;AAC3E,2EAA2E;AAC3E,aAAa;AAEb,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAExF,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,KAAK,GAAG,IAAI,SAAS,CAAC,WAAW,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC;AAS5H,SAAS,SAAS,CAAC,IAAY;IAC7B,MAAM,GAAG,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC,CAAC;IAC7G,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAqC,CAAC;IAC1E,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC;AACrH,CAAC;AAED,MAAM,IAAI,GAAG,CAAC,MAAqC,EAAuB,EAAE,CAAC,CAAC;IAC5E,MAAM;IACN,QAAQ,EAAE;QACR,KAAK,EAAE,aAAa;QACpB,QAAQ,EAAE,2BAA2B;QACrC,QAAQ,EAAE,CAAC,cAAc,CAAC;QAC1B,QAAQ,EAAE,OAAO;QACjB,IAAI,EAAE,YAAY;QAClB,SAAS,EAAE,SAAS;QACpB,QAAQ,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;KACzC;IACD,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IACrF,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS;CACpG,CAAC,CAAC;AAEH,kDAAkD;AAClD,MAAM,MAAM,GAAG,CAAC,MAAqC,EAAuB,EAAE,CAAC,CAAC;IAC9E,MAAM;IACN,QAAQ,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;IAClH,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IACvF,KAAK,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,EAAE,aAAa,EAAE;CAClE,CAAC,CAAC;AAEH,MAAM,OAAO,GAAiG;IAC5G,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE;IACzC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,EAAE,cAAc,EAAE,EAAE,EAAE;IAC1H,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC,YAAY,CAAC,EAAE;IACnD,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,EAAE,GAAG,IAAI,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,EAAE,cAAc,EAAE,EAAE,EAAE;IACpI,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE;IAC1E,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE;CACrF,CAAC;AAEF,QAAQ,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,GAAG,EAAE;IAC5C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,eAAe,CAAC,CAAC,CAAC;IACjG,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEvC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,EAAE,CAAC,GAAG,MAAM,CAAC,IAAI,wBAAwB,EAAE,KAAK,IAAI,EAAE;YACpD,IAAI,KAAiB,CAAC;YACtB,IAAI,CAAC;gBACH,KAAK,GAAG,MAAM,YAAY,CAAC,CAAC,MAAM,CAAC,IAAI,IAAI,UAAU,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;YAC5E,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,sDAAsD;gBACtD,IAAI,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;oBAAE,OAAO;gBAC9C,MAAM,CAAC,CAAC;YACV,CAAC;YACD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,IAAI,OAAO,CAAC,CAAC;YACnD,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAC9B,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;YACjC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,EAAE,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,EAAE,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC5I,CAAC,EAAE,OAAO,CAAC,CAAC;IACd,CAAC;AACH,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixed.test.d.ts","sourceRoot":"","sources":["../../src/fixed/fixed.test.ts"],"names":[],"mappings":""}
|