@fiduswriter/books-document 0.1.2 → 0.1.3
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/README.md +3 -3
- package/dist/exporter/bits/index.d.ts.map +1 -1
- package/dist/exporter/bits/index.js +3 -7
- package/dist/exporter/bits/index.js.map +1 -1
- package/dist/exporter/bits/templates.d.ts.map +1 -1
- package/dist/exporter/bits/templates.js.map +1 -1
- package/dist/exporter/docx/index.d.ts.map +1 -1
- package/dist/exporter/docx/index.js +2 -4
- package/dist/exporter/docx/index.js.map +1 -1
- package/dist/exporter/docx/render.d.ts.map +1 -1
- package/dist/exporter/docx/render.js.map +1 -1
- package/dist/exporter/epub/index.d.ts.map +1 -1
- package/dist/exporter/epub/index.js +5 -12
- package/dist/exporter/epub/index.js.map +1 -1
- package/dist/exporter/epub/templates.d.ts.map +1 -1
- package/dist/exporter/epub/templates.js.map +1 -1
- package/dist/exporter/epub/tools.d.ts.map +1 -1
- package/dist/exporter/epub/tools.js.map +1 -1
- package/dist/exporter/html/index.d.ts.map +1 -1
- package/dist/exporter/html/index.js +2 -5
- package/dist/exporter/html/index.js.map +1 -1
- package/dist/exporter/html/templates.d.ts.map +1 -1
- package/dist/exporter/html/templates.js +3 -1
- package/dist/exporter/html/templates.js.map +1 -1
- package/dist/exporter/html/tools.d.ts.map +1 -1
- package/dist/exporter/html/tools.js +1 -2
- package/dist/exporter/html/tools.js.map +1 -1
- package/dist/exporter/latex/index.d.ts.map +1 -1
- package/dist/exporter/latex/index.js.map +1 -1
- package/dist/exporter/latex/templates.d.ts.map +1 -1
- package/dist/exporter/latex/templates.js.map +1 -1
- package/dist/exporter/native/index.d.ts.map +1 -1
- package/dist/exporter/native/index.js +1 -1
- package/dist/exporter/native/index.js.map +1 -1
- package/dist/exporter/odt/index.d.ts.map +1 -1
- package/dist/exporter/odt/index.js +2 -4
- package/dist/exporter/odt/index.js.map +1 -1
- package/dist/exporter/odt/render.d.ts.map +1 -1
- package/dist/exporter/odt/render.js.map +1 -1
- package/dist/exporter/print/index.d.ts.map +1 -1
- package/dist/exporter/print/index.js +2 -1
- package/dist/exporter/print/index.js.map +1 -1
- package/dist/exporter/print/templates.js.map +1 -1
- package/dist/exporter/tools.d.ts.map +1 -1
- package/dist/exporter/tools.js +2 -4
- package/dist/exporter/tools.js.map +1 -1
- package/dist/importer/native/index.d.ts +13 -2
- package/dist/importer/native/index.d.ts.map +1 -1
- package/dist/importer/native/index.js +20 -8
- package/dist/importer/native/index.js.map +1 -1
- package/dist/importer/native/reader.d.ts.map +1 -1
- package/dist/importer/native/reader.js +1 -3
- package/dist/importer/native/reader.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/exporter/bits/index.ts +27 -31
- package/src/exporter/bits/templates.ts +4 -4
- package/src/exporter/docx/index.ts +33 -32
- package/src/exporter/docx/render.ts +16 -17
- package/src/exporter/epub/index.ts +23 -43
- package/src/exporter/epub/templates.ts +12 -14
- package/src/exporter/epub/tools.ts +3 -5
- package/src/exporter/html/index.ts +32 -46
- package/src/exporter/html/templates.ts +15 -13
- package/src/exporter/html/tools.ts +4 -9
- package/src/exporter/latex/index.ts +17 -20
- package/src/exporter/latex/templates.ts +2 -6
- package/src/exporter/native/index.ts +14 -14
- package/src/exporter/odt/index.ts +27 -26
- package/src/exporter/odt/render.ts +16 -17
- package/src/exporter/print/index.ts +15 -11
- package/src/exporter/print/templates.ts +1 -1
- package/src/exporter/tools.ts +15 -44
- package/src/global.d.ts +2 -0
- package/src/importer/native/index.ts +56 -32
- package/src/importer/native/reader.ts +9 -18
- package/src/index.ts +1 -1
- package/src/modules.d.ts +1 -1
- package/src/types.ts +2 -2
package/src/exporter/tools.ts
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* decrypt end-to-end encrypted chapters.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
10
|
+
import type {Schema} from "prosemirror-model"
|
|
11
|
+
import {getSettings} from "@fiduswriter/document/schema/convert"
|
|
12
|
+
import {acceptAllNoInsertions} from "@fiduswriter/document/transform"
|
|
13
|
+
import {addAlert} from "fwtoolkit"
|
|
14
14
|
|
|
15
15
|
import type {
|
|
16
16
|
ChapterLoader,
|
|
@@ -60,11 +60,8 @@ export async function getMissingChapterData(
|
|
|
60
60
|
e2ee?: E2EEStrategy
|
|
61
61
|
} = {}
|
|
62
62
|
): Promise<void> {
|
|
63
|
-
const {
|
|
64
|
-
|
|
65
|
-
loader = noopChapterLoader,
|
|
66
|
-
e2ee = noopE2EEStrategy
|
|
67
|
-
} = options
|
|
63
|
+
const {rawContent = false, loader = noopChapterLoader, e2ee = noopE2EEStrategy} =
|
|
64
|
+
options
|
|
68
65
|
|
|
69
66
|
const bookDocuments = book.chapters.map(chapter =>
|
|
70
67
|
documentList.find(doc => doc.id === chapter.text)
|
|
@@ -134,9 +131,7 @@ async function decryptE2EEChapters(
|
|
|
134
131
|
"error",
|
|
135
132
|
`${gettext("No encryption key found for chapter:")} ${chapterLabel}. ${gettext("The key may not have been shared with you.")}`
|
|
136
133
|
)
|
|
137
|
-
throw new Error(
|
|
138
|
-
`No encryption key for document ${String(doc.id)}`
|
|
139
|
-
)
|
|
134
|
+
throw new Error(`No encryption key for document ${String(doc.id)}`)
|
|
140
135
|
}
|
|
141
136
|
|
|
142
137
|
if (!doc.e2ee_salt) {
|
|
@@ -147,11 +142,7 @@ async function decryptE2EEChapters(
|
|
|
147
142
|
const salt = base64ToUint8Array(doc.e2ee_salt)
|
|
148
143
|
const iterations = doc.e2ee_iterations || 600000
|
|
149
144
|
|
|
150
|
-
const key = await e2ee.resolvePasswordToKey(
|
|
151
|
-
password,
|
|
152
|
-
salt,
|
|
153
|
-
iterations
|
|
154
|
-
)
|
|
145
|
+
const key = await e2ee.resolvePasswordToKey(password, salt, iterations)
|
|
155
146
|
|
|
156
147
|
try {
|
|
157
148
|
const decryptedContent = (await e2ee.decryptObject(
|
|
@@ -160,18 +151,11 @@ async function decryptE2EEChapters(
|
|
|
160
151
|
)) as Record<string, unknown>
|
|
161
152
|
|
|
162
153
|
// Update the plaintext title.
|
|
163
|
-
const titleNode = (
|
|
164
|
-
|
|
165
|
-
Record<string, unknown>[] | undefined
|
|
166
|
-
)?.[0]
|
|
167
|
-
const titleContent =
|
|
168
|
-
(titleNode?.content as
|
|
169
|
-
Record<string, unknown>[] | undefined) || []
|
|
154
|
+
const titleNode = (decryptedContent.content as Record<string, unknown>[] | undefined)?.[0]
|
|
155
|
+
const titleContent = (titleNode?.content as Record<string, unknown>[] | undefined) || []
|
|
170
156
|
let title = ""
|
|
171
157
|
titleContent.forEach(child => {
|
|
172
|
-
const marks =
|
|
173
|
-
(child.marks as
|
|
174
|
-
Record<string, unknown>[] | undefined) || []
|
|
158
|
+
const marks = (child.marks as Record<string, unknown>[] | undefined) || []
|
|
175
159
|
if (!marks.some(m => (m.type as string) === "deletion")) {
|
|
176
160
|
title += (child.text as string) || ""
|
|
177
161
|
}
|
|
@@ -179,36 +163,23 @@ async function decryptE2EEChapters(
|
|
|
179
163
|
if (title) {
|
|
180
164
|
doc.title = title.substring(0, 255)
|
|
181
165
|
if (typeof sessionStorage !== "undefined") {
|
|
182
|
-
sessionStorage.setItem(
|
|
183
|
-
`e2ee_title_${String(doc.id)}`,
|
|
184
|
-
doc.title
|
|
185
|
-
)
|
|
166
|
+
sessionStorage.setItem(`e2ee_title_${String(doc.id)}`, doc.title)
|
|
186
167
|
}
|
|
187
168
|
}
|
|
188
169
|
|
|
189
170
|
// Parse ProseMirror content.
|
|
190
171
|
if (rawContent) {
|
|
191
172
|
doc.rawContent = JSON.parse(
|
|
192
|
-
JSON.stringify(
|
|
193
|
-
schema
|
|
194
|
-
.nodeFromJSON(
|
|
195
|
-
decryptedContent as Record<string, unknown>
|
|
196
|
-
)
|
|
197
|
-
.toJSON()
|
|
198
|
-
)
|
|
173
|
+
JSON.stringify(schema.nodeFromJSON(decryptedContent as Record<string, unknown>).toJSON())
|
|
199
174
|
)
|
|
200
175
|
}
|
|
201
176
|
doc.content = acceptAllNoInsertions(
|
|
202
|
-
schema.nodeFromJSON(
|
|
203
|
-
decryptedContent as Record<string, unknown>
|
|
204
|
-
)
|
|
177
|
+
schema.nodeFromJSON(decryptedContent as Record<string, unknown>)
|
|
205
178
|
).toJSON()
|
|
206
179
|
doc.settings = getSettings(doc.content)
|
|
207
180
|
|
|
208
181
|
// Decrypt encrypted images.
|
|
209
|
-
const encryptedImageEntries = Object.entries(
|
|
210
|
-
doc.images || {}
|
|
211
|
-
).filter(
|
|
182
|
+
const encryptedImageEntries = Object.entries(doc.images || {}).filter(
|
|
212
183
|
([, entry]) =>
|
|
213
184
|
entry.file_type === "application/octet-stream" &&
|
|
214
185
|
typeof entry.image === "string"
|
package/src/global.d.ts
ADDED
|
@@ -5,14 +5,28 @@
|
|
|
5
5
|
* chapter and delegates creation of the book record to a `BookImporterBackend`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { NativeImporterBackend
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import type {E2EEOptions, NativeImporterBackend} from "@fiduswriter/document"
|
|
9
|
+
import {NativeImporter} from "@fiduswriter/document/importer/native"
|
|
10
|
+
import {addAlert} from "fwtoolkit"
|
|
11
11
|
|
|
12
|
-
import type {
|
|
12
|
+
import type {BookImporterBackend, Chapter, User} from "../../types.js"
|
|
13
13
|
|
|
14
|
-
export {
|
|
15
|
-
export {
|
|
14
|
+
export {FIDUSBOOK_VERSION} from "./reader.js"
|
|
15
|
+
export {readFidusBookFile, FidusBookReader} from "./reader.js"
|
|
16
|
+
|
|
17
|
+
export interface E2EEOptionsResult {
|
|
18
|
+
options: E2EEOptions
|
|
19
|
+
password: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface NativeBookImporterOptions {
|
|
23
|
+
path?: string
|
|
24
|
+
getE2EEOptions?: () => Promise<E2EEOptionsResult | null>
|
|
25
|
+
onChapterImported?: (
|
|
26
|
+
doc: Record<string, unknown>,
|
|
27
|
+
password: string
|
|
28
|
+
) => Promise<void>
|
|
29
|
+
}
|
|
16
30
|
|
|
17
31
|
export class NativeBookImporter {
|
|
18
32
|
file: Blob
|
|
@@ -20,6 +34,11 @@ export class NativeBookImporter {
|
|
|
20
34
|
path: string
|
|
21
35
|
nativeBackend: NativeImporterBackend
|
|
22
36
|
bookBackend: BookImporterBackend
|
|
37
|
+
getE2EEOptions?: () => Promise<E2EEOptionsResult | null>
|
|
38
|
+
onChapterImported?: (
|
|
39
|
+
doc: Record<string, unknown>,
|
|
40
|
+
password: string
|
|
41
|
+
) => Promise<void>
|
|
23
42
|
|
|
24
43
|
ok: boolean
|
|
25
44
|
statusText: string
|
|
@@ -30,13 +49,16 @@ export class NativeBookImporter {
|
|
|
30
49
|
user: User,
|
|
31
50
|
nativeBackend: NativeImporterBackend,
|
|
32
51
|
bookBackend: BookImporterBackend,
|
|
33
|
-
|
|
52
|
+
options: string | NativeBookImporterOptions = "/"
|
|
34
53
|
) {
|
|
54
|
+
const opts = typeof options === "string" ? {path: options} : options
|
|
35
55
|
this.file = file
|
|
36
56
|
this.user = user
|
|
37
|
-
this.path = path
|
|
57
|
+
this.path = opts.path?.endsWith("/") ? opts.path : `${opts.path || "/"}/`
|
|
38
58
|
this.nativeBackend = nativeBackend
|
|
39
59
|
this.bookBackend = bookBackend
|
|
60
|
+
this.getE2EEOptions = opts.getE2EEOptions
|
|
61
|
+
this.onChapterImported = opts.onChapterImported
|
|
40
62
|
|
|
41
63
|
this.ok = false
|
|
42
64
|
this.statusText = ""
|
|
@@ -68,8 +90,8 @@ export class NativeBookImporter {
|
|
|
68
90
|
}
|
|
69
91
|
|
|
70
92
|
async readZip(): Promise<void> {
|
|
71
|
-
let textFiles: Array<{
|
|
72
|
-
let binaryFiles: Array<{
|
|
93
|
+
let textFiles: Array<{filename: string; content: string}> = []
|
|
94
|
+
let binaryFiles: Array<{filename: string; content: Blob}> = []
|
|
73
95
|
|
|
74
96
|
try {
|
|
75
97
|
const JSZip = (await import("jszip")).default
|
|
@@ -113,15 +135,18 @@ export class NativeBookImporter {
|
|
|
113
135
|
}
|
|
114
136
|
|
|
115
137
|
async processFidusbookFile(
|
|
116
|
-
textFiles: Array<{
|
|
117
|
-
binaryFiles: Array<{
|
|
138
|
+
textFiles: Array<{filename: string; content: string}>,
|
|
139
|
+
binaryFiles: Array<{filename: string; content: Blob}>
|
|
118
140
|
): Promise<void> {
|
|
119
141
|
const versionEntry = textFiles.find(
|
|
120
142
|
f => f.filename === "filetype-version"
|
|
121
143
|
)
|
|
122
144
|
const filetypeVersion = Number.parseFloat(versionEntry?.content || "")
|
|
123
145
|
|
|
124
|
-
if (
|
|
146
|
+
if (
|
|
147
|
+
filetypeVersion < 1.0 ||
|
|
148
|
+
filetypeVersion > 1.0
|
|
149
|
+
) {
|
|
125
150
|
this.statusText =
|
|
126
151
|
gettext(
|
|
127
152
|
"The Fidusbook file version is not supported by this server: "
|
|
@@ -144,13 +169,9 @@ export class NativeBookImporter {
|
|
|
144
169
|
textFiles.find(f => f.filename === "book.json")?.content || "{}"
|
|
145
170
|
) as Record<string, any>
|
|
146
171
|
|
|
147
|
-
const sortedChapters = [
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
number: number
|
|
151
|
-
part?: string
|
|
152
|
-
}>)
|
|
153
|
-
].sort((a, b) => a.chapter_index - b.chapter_index)
|
|
172
|
+
const sortedChapters = [...(bookData.chapters as Array<{chapter_index: number; number: number; part?: string}>)].sort(
|
|
173
|
+
(a, b) => a.chapter_index - b.chapter_index
|
|
174
|
+
)
|
|
154
175
|
|
|
155
176
|
const importedDocIds: Record<number, number> = {}
|
|
156
177
|
|
|
@@ -177,10 +198,7 @@ export class NativeBookImporter {
|
|
|
177
198
|
}
|
|
178
199
|
|
|
179
200
|
const docJson = JSON.parse(docFile.content) as Record<string, any>
|
|
180
|
-
const imagesJson = JSON.parse(imagesFile.content) as Record<
|
|
181
|
-
string,
|
|
182
|
-
any
|
|
183
|
-
>
|
|
201
|
+
const imagesJson = JSON.parse(imagesFile.content) as Record<string, any>
|
|
184
202
|
const bibJson = JSON.parse(bibFile.content) as Record<string, any>
|
|
185
203
|
|
|
186
204
|
const chapterPrefix = `chapters/${ci}/images/`
|
|
@@ -194,21 +212,27 @@ export class NativeBookImporter {
|
|
|
194
212
|
const safeBookTitle = (bookData.title as string) || "Untitled"
|
|
195
213
|
const chapterPath = `${this.path}${safeBookTitle}/${(docJson.title as string) || "Untitled"}`
|
|
196
214
|
|
|
215
|
+
let e2eeResult: E2EEOptionsResult | null = null
|
|
216
|
+
if (this.getE2EEOptions) {
|
|
217
|
+
e2eeResult = await this.getE2EEOptions()
|
|
218
|
+
}
|
|
219
|
+
|
|
197
220
|
const importer = new NativeImporter(
|
|
198
221
|
docJson,
|
|
199
222
|
bibJson,
|
|
200
|
-
{
|
|
223
|
+
{db: imagesJson},
|
|
201
224
|
chapterOtherFiles,
|
|
202
225
|
this.user,
|
|
203
226
|
this.nativeBackend,
|
|
204
227
|
{
|
|
205
|
-
requestedPath: chapterPath
|
|
228
|
+
requestedPath: chapterPath,
|
|
229
|
+
e2eeOptions: e2eeResult?.options ?? null
|
|
206
230
|
}
|
|
207
231
|
)
|
|
208
232
|
|
|
209
233
|
let doc
|
|
210
234
|
try {
|
|
211
|
-
;({
|
|
235
|
+
;({doc} = await importer.init())
|
|
212
236
|
} catch (error) {
|
|
213
237
|
addAlert(
|
|
214
238
|
"error",
|
|
@@ -220,6 +244,10 @@ export class NativeBookImporter {
|
|
|
220
244
|
}
|
|
221
245
|
|
|
222
246
|
importedDocIds[ci] = doc.id as number
|
|
247
|
+
|
|
248
|
+
if (e2eeResult && this.onChapterImported) {
|
|
249
|
+
await this.onChapterImported(doc, e2eeResult.password)
|
|
250
|
+
}
|
|
223
251
|
}
|
|
224
252
|
|
|
225
253
|
const chapters: Chapter[] = sortedChapters.map(chapter => ({
|
|
@@ -228,11 +256,7 @@ export class NativeBookImporter {
|
|
|
228
256
|
part: chapter.part || ""
|
|
229
257
|
}))
|
|
230
258
|
|
|
231
|
-
const book = await this.bookBackend.createBook(
|
|
232
|
-
bookData as any,
|
|
233
|
-
chapters,
|
|
234
|
-
false
|
|
235
|
-
)
|
|
259
|
+
const book = await this.bookBackend.createBook(bookData as any, chapters, false)
|
|
236
260
|
this.bookId = book.id || null
|
|
237
261
|
this.ok = true
|
|
238
262
|
this.statusText = `"${bookData.title}" ${gettext("successfully imported.")}`
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* exporters. No server interaction.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type {DocumentListEntry} from "../../types.js"
|
|
10
10
|
|
|
11
11
|
export const FIDUSBOOK_VERSION = "1.0"
|
|
12
12
|
export const MIN_FIDUSBOOK_VERSION = 1.0
|
|
@@ -58,7 +58,7 @@ export async function readFidusBookFile(
|
|
|
58
58
|
const content = await zipfs.files[filename].async(
|
|
59
59
|
isText ? "string" : "blob"
|
|
60
60
|
)
|
|
61
|
-
const entry = {
|
|
61
|
+
const entry = {filename, content}
|
|
62
62
|
if (isText) {
|
|
63
63
|
textFiles.push(entry as ArchiveFile)
|
|
64
64
|
} else {
|
|
@@ -93,9 +93,9 @@ export async function readFidusBookFile(
|
|
|
93
93
|
textFiles.find(f => f.filename === "book.json")?.content as string
|
|
94
94
|
) as Record<string, unknown>
|
|
95
95
|
|
|
96
|
-
const sortedChapters = [
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
const sortedChapters = [...(bookData.chapters as Array<{chapter_index: number}>)].sort(
|
|
97
|
+
(a, b) => a.chapter_index - b.chapter_index
|
|
98
|
+
)
|
|
99
99
|
|
|
100
100
|
const documentList: DocumentListEntry[] = []
|
|
101
101
|
|
|
@@ -116,18 +116,9 @@ export async function readFidusBookFile(
|
|
|
116
116
|
throw new Error(`Missing chapter data for index ${ci}`)
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
const docJson = JSON.parse(docFile.content as string) as Record<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
>
|
|
123
|
-
const imagesJson = JSON.parse(imagesFile.content as string) as Record<
|
|
124
|
-
string,
|
|
125
|
-
{ image: string; file?: Blob }
|
|
126
|
-
>
|
|
127
|
-
const bibJson = JSON.parse(bibFile.content as string) as Record<
|
|
128
|
-
string,
|
|
129
|
-
unknown
|
|
130
|
-
>
|
|
119
|
+
const docJson = JSON.parse(docFile.content as string) as Record<string, unknown>
|
|
120
|
+
const imagesJson = JSON.parse(imagesFile.content as string) as Record<string, {image: string; file?: Blob}>
|
|
121
|
+
const bibJson = JSON.parse(bibFile.content as string) as Record<string, unknown>
|
|
131
122
|
|
|
132
123
|
const chapterPrefix = `chapters/${ci}/images/`
|
|
133
124
|
const chapterImages: Record<string, Blob> = {}
|
|
@@ -164,7 +155,7 @@ export async function readFidusBookFile(
|
|
|
164
155
|
} as DocumentListEntry)
|
|
165
156
|
}
|
|
166
157
|
|
|
167
|
-
return {
|
|
158
|
+
return {book: bookData, documentList}
|
|
168
159
|
}
|
|
169
160
|
|
|
170
161
|
export class FidusBookReader {
|
package/src/index.ts
CHANGED
package/src/modules.d.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Core types for the @fiduswriter/books-document package.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type {Schema} from "prosemirror-model"
|
|
6
6
|
import type {
|
|
7
7
|
BibDB,
|
|
8
8
|
CSL,
|
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
User
|
|
13
13
|
} from "@fiduswriter/document"
|
|
14
14
|
|
|
15
|
-
export type {
|
|
15
|
+
export type {CSL, User} from "@fiduswriter/document"
|
|
16
16
|
|
|
17
17
|
/** A single chapter entry inside a book. */
|
|
18
18
|
export interface Chapter {
|