@fiduswriter/books-document 0.1.28 → 0.1.29
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/dist/ui/book_dialog/dialog.d.ts +98 -0
- package/dist/ui/book_dialog/dialog.d.ts.map +1 -0
- package/dist/ui/book_dialog/dialog.js +590 -0
- package/dist/ui/book_dialog/dialog.js.map +1 -0
- package/dist/ui/book_dialog/export.d.ts +81 -0
- package/dist/ui/book_dialog/export.d.ts.map +1 -0
- package/dist/ui/book_dialog/export.js +184 -0
- package/dist/ui/book_dialog/export.js.map +1 -0
- package/dist/ui/book_dialog/index.d.ts +38 -0
- package/dist/ui/book_dialog/index.d.ts.map +1 -0
- package/dist/ui/book_dialog/index.js +36 -0
- package/dist/ui/book_dialog/index.js.map +1 -0
- package/dist/ui/book_dialog/sanity_check.d.ts +22 -0
- package/dist/ui/book_dialog/sanity_check.d.ts.map +1 -0
- package/dist/ui/book_dialog/sanity_check.js +109 -0
- package/dist/ui/book_dialog/sanity_check.js.map +1 -0
- package/dist/ui/book_dialog/templates.d.ts +74 -0
- package/dist/ui/book_dialog/templates.d.ts.map +1 -0
- package/dist/ui/book_dialog/templates.js +526 -0
- package/dist/ui/book_dialog/templates.js.map +1 -0
- package/dist/ui/book_dialog/types.d.ts +165 -0
- package/dist/ui/book_dialog/types.d.ts.map +1 -0
- package/dist/ui/book_dialog/types.js +15 -0
- package/dist/ui/book_dialog/types.js.map +1 -0
- package/package.json +5 -1
- package/src/ui/book_dialog/dialog.ts +765 -0
- package/src/ui/book_dialog/export.ts +398 -0
- package/src/ui/book_dialog/index.ts +81 -0
- package/src/ui/book_dialog/sanity_check.ts +208 -0
- package/src/ui/book_dialog/templates.ts +705 -0
- package/src/ui/book_dialog/types.ts +180 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Book export initiation.
|
|
3
|
+
*
|
|
4
|
+
* Wraps every book exporter of this package in the chapter-data preloading
|
|
5
|
+
* and progress reporting the dialog's export menu needs, and delivers the
|
|
6
|
+
* resulting Blob through the host-supplied
|
|
7
|
+
* {@link BookExportContext.deliver}.
|
|
8
|
+
*
|
|
9
|
+
* The exporters themselves are environment-agnostic: they load chapter data
|
|
10
|
+
* through injected strategies (defaulting to no-ops) and simply return the
|
|
11
|
+
* produced Blob. Preloading here with the host's `chapterLoader`/`e2ee` makes
|
|
12
|
+
* the exporters' own internal `getMissingChapterData` call a no-op.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {ProgressCallback} from "@fiduswriter/document/exporter/tools/progress"
|
|
16
|
+
import {BITSBookExporter} from "../../exporter/bits/index.js"
|
|
17
|
+
import {DOCXBookExporter} from "../../exporter/docx/index.js"
|
|
18
|
+
import {EpubBookExporter} from "../../exporter/epub/index.js"
|
|
19
|
+
import {HTMLBookExporter} from "../../exporter/html/index.js"
|
|
20
|
+
import {LatexBookExporter} from "../../exporter/latex/index.js"
|
|
21
|
+
import {NativeBookExporter} from "../../exporter/native/index.js"
|
|
22
|
+
import {ODTBookExporter} from "../../exporter/odt/index.js"
|
|
23
|
+
import {PrintBookExporter} from "../../exporter/print/index.js"
|
|
24
|
+
import {getMissingChapterData} from "../../exporter/tools.js"
|
|
25
|
+
import type {Book, DocumentListEntry} from "../../types.js"
|
|
26
|
+
import {addAlert, addProgress, gettext} from "fwtoolkit"
|
|
27
|
+
import type {ContentMenuInit} from "fwtoolkit/content_menu"
|
|
28
|
+
import type {BookExportContext} from "./types.js"
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The subset of a book exporter this module needs. Every exporter of
|
|
32
|
+
* `@fiduswriter/books-document` exposes these members; the declared return
|
|
33
|
+
* type of `init()` differs slightly between them (some return `false`, some
|
|
34
|
+
* `void`, instead of a Blob), so it is kept loose here. `mimeType` likewise
|
|
35
|
+
* only exists on the DOCX/ODT exporters, which read it off the instance.
|
|
36
|
+
*/
|
|
37
|
+
export interface BookExporterLike {
|
|
38
|
+
book: Book
|
|
39
|
+
defaultFilename: string
|
|
40
|
+
// Declared inconsistently across the exporters: some resolve to `false`
|
|
41
|
+
// or nothing, and BITS/DOCX/ODT can return `false` synchronously.
|
|
42
|
+
init(
|
|
43
|
+
progressCallback?: ProgressCallback
|
|
44
|
+
): Promise<Blob | false | void> | false
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Arguments passed to the callbacks of the single-book export menu. The
|
|
49
|
+
* dialog assembles them once and hands them to every entry.
|
|
50
|
+
*/
|
|
51
|
+
export interface BookExportMenuArgs {
|
|
52
|
+
saveBook: () => Promise<unknown>
|
|
53
|
+
book: Book
|
|
54
|
+
documentList: DocumentListEntry[]
|
|
55
|
+
context: BookExportContext
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Model of the book dialog's export menu (a content menu of actions). */
|
|
59
|
+
export type BookExportMenu = ContentMenuInit
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Load any missing chapter data (fetching content lazily and decrypting E2EE
|
|
63
|
+
* chapters), run the given exporter, then hand the resulting Blob to the
|
|
64
|
+
* host's `deliver`.
|
|
65
|
+
*
|
|
66
|
+
* @param exporter - A constructed book exporter instance.
|
|
67
|
+
* @param documentList - Document entries for the book's chapters.
|
|
68
|
+
* @param context - The host's schema, chapter loader, E2EE strategy and
|
|
69
|
+
* delivery callback.
|
|
70
|
+
* @param mimeType - MIME type of the produced file.
|
|
71
|
+
* @param rawContent - Whether the exporter needs doc.rawContent.
|
|
72
|
+
*/
|
|
73
|
+
export const runBookExport = (
|
|
74
|
+
exporter: BookExporterLike,
|
|
75
|
+
documentList: DocumentListEntry[],
|
|
76
|
+
context: BookExportContext,
|
|
77
|
+
mimeType: string,
|
|
78
|
+
rawContent = false
|
|
79
|
+
): Promise<unknown> => {
|
|
80
|
+
const {schema, chapterLoader, e2ee, deliver} = context
|
|
81
|
+
const formatName =
|
|
82
|
+
exporter.defaultFilename.split(".").pop()?.toUpperCase() ||
|
|
83
|
+
gettext("Book")
|
|
84
|
+
const task = addProgress(
|
|
85
|
+
"info",
|
|
86
|
+
`${exporter.book.title}: ${gettext("Exporting")} ${formatName}...`,
|
|
87
|
+
{autoClose: 6000}
|
|
88
|
+
)
|
|
89
|
+
const progressCallback: ProgressCallback = (message, percentage) =>
|
|
90
|
+
task.update(percentage ?? null, message)
|
|
91
|
+
|
|
92
|
+
return getMissingChapterData(exporter.book, documentList, schema, {
|
|
93
|
+
rawContent,
|
|
94
|
+
loader: chapterLoader,
|
|
95
|
+
e2ee,
|
|
96
|
+
progressCallback
|
|
97
|
+
})
|
|
98
|
+
.then(() => exporter.init(progressCallback))
|
|
99
|
+
.then(blob => {
|
|
100
|
+
task.update(100, gettext("Export complete."))
|
|
101
|
+
if (blob) {
|
|
102
|
+
const extension = `.${exporter.defaultFilename.split(".").pop()}`
|
|
103
|
+
deliver(blob, exporter.defaultFilename, {
|
|
104
|
+
description: exporter.book.title,
|
|
105
|
+
mimeType,
|
|
106
|
+
extensions: [extension]
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
return blob
|
|
110
|
+
})
|
|
111
|
+
.catch((error: Error) => {
|
|
112
|
+
task.close()
|
|
113
|
+
addAlert("error", error.message || gettext("Book export failed."))
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Export a book as an Epub file. */
|
|
118
|
+
export const exportEpub = (
|
|
119
|
+
book: Book,
|
|
120
|
+
documentList: DocumentListEntry[],
|
|
121
|
+
context: BookExportContext
|
|
122
|
+
) =>
|
|
123
|
+
runBookExport(
|
|
124
|
+
new EpubBookExporter(
|
|
125
|
+
context.schema,
|
|
126
|
+
context.csl,
|
|
127
|
+
context.bookStyles,
|
|
128
|
+
book,
|
|
129
|
+
context.user,
|
|
130
|
+
documentList,
|
|
131
|
+
book.updated ?? 0
|
|
132
|
+
),
|
|
133
|
+
documentList,
|
|
134
|
+
context,
|
|
135
|
+
"application/epub+zip"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
/** Export a book as a BITS (Book Interchange Tag Set) file. */
|
|
139
|
+
export const exportBITS = (
|
|
140
|
+
book: Book,
|
|
141
|
+
documentList: DocumentListEntry[],
|
|
142
|
+
context: BookExportContext
|
|
143
|
+
) =>
|
|
144
|
+
runBookExport(
|
|
145
|
+
new BITSBookExporter(
|
|
146
|
+
context.schema,
|
|
147
|
+
context.csl,
|
|
148
|
+
book,
|
|
149
|
+
context.user,
|
|
150
|
+
documentList,
|
|
151
|
+
book.updated ?? 0
|
|
152
|
+
),
|
|
153
|
+
documentList,
|
|
154
|
+
context,
|
|
155
|
+
"application/zip"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
/** Export a book as a multi-file HTML archive. */
|
|
159
|
+
export const exportHTML = (
|
|
160
|
+
book: Book,
|
|
161
|
+
documentList: DocumentListEntry[],
|
|
162
|
+
context: BookExportContext,
|
|
163
|
+
multiDoc = true
|
|
164
|
+
) =>
|
|
165
|
+
runBookExport(
|
|
166
|
+
new HTMLBookExporter(
|
|
167
|
+
context.schema,
|
|
168
|
+
context.csl,
|
|
169
|
+
context.bookStyles,
|
|
170
|
+
book,
|
|
171
|
+
context.user,
|
|
172
|
+
documentList,
|
|
173
|
+
book.updated ?? 0,
|
|
174
|
+
multiDoc
|
|
175
|
+
),
|
|
176
|
+
documentList,
|
|
177
|
+
context,
|
|
178
|
+
"application/zip"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
/** Export a book as a single-file HTML archive. */
|
|
182
|
+
export const exportSingleHTML = (
|
|
183
|
+
book: Book,
|
|
184
|
+
documentList: DocumentListEntry[],
|
|
185
|
+
context: BookExportContext
|
|
186
|
+
) => exportHTML(book, documentList, context, false)
|
|
187
|
+
|
|
188
|
+
/** Export a book as LaTeX. */
|
|
189
|
+
export const exportLatex = (
|
|
190
|
+
book: Book,
|
|
191
|
+
documentList: DocumentListEntry[],
|
|
192
|
+
context: BookExportContext
|
|
193
|
+
) =>
|
|
194
|
+
runBookExport(
|
|
195
|
+
new LatexBookExporter(
|
|
196
|
+
context.schema,
|
|
197
|
+
book,
|
|
198
|
+
context.user,
|
|
199
|
+
documentList,
|
|
200
|
+
book.updated ?? 0
|
|
201
|
+
),
|
|
202
|
+
documentList,
|
|
203
|
+
context,
|
|
204
|
+
"application/zip"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
/** Export a book as DOCX, using the book's DOCX template. */
|
|
208
|
+
export const exportDOCX = (
|
|
209
|
+
book: Book,
|
|
210
|
+
documentList: DocumentListEntry[],
|
|
211
|
+
context: BookExportContext
|
|
212
|
+
) => {
|
|
213
|
+
const exporter = new DOCXBookExporter(
|
|
214
|
+
context.schema,
|
|
215
|
+
context.csl,
|
|
216
|
+
book,
|
|
217
|
+
context.user,
|
|
218
|
+
documentList,
|
|
219
|
+
book.updated ?? 0
|
|
220
|
+
)
|
|
221
|
+
return runBookExport(
|
|
222
|
+
exporter,
|
|
223
|
+
documentList,
|
|
224
|
+
context,
|
|
225
|
+
exporter.mimeType,
|
|
226
|
+
true
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Export a book as ODT, using the book's ODT template. */
|
|
231
|
+
export const exportODT = (
|
|
232
|
+
book: Book,
|
|
233
|
+
documentList: DocumentListEntry[],
|
|
234
|
+
context: BookExportContext
|
|
235
|
+
) => {
|
|
236
|
+
const exporter = new ODTBookExporter(
|
|
237
|
+
context.schema,
|
|
238
|
+
context.csl,
|
|
239
|
+
book,
|
|
240
|
+
context.user,
|
|
241
|
+
documentList,
|
|
242
|
+
book.updated ?? 0
|
|
243
|
+
)
|
|
244
|
+
return runBookExport(
|
|
245
|
+
exporter,
|
|
246
|
+
documentList,
|
|
247
|
+
context,
|
|
248
|
+
exporter.mimeType,
|
|
249
|
+
true
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Open a book's print/PDF view. */
|
|
254
|
+
export const exportPrint = (
|
|
255
|
+
book: Book,
|
|
256
|
+
documentList: DocumentListEntry[],
|
|
257
|
+
context: BookExportContext
|
|
258
|
+
) =>
|
|
259
|
+
runBookExport(
|
|
260
|
+
new PrintBookExporter(
|
|
261
|
+
context.schema,
|
|
262
|
+
context.csl,
|
|
263
|
+
context.bookStyles,
|
|
264
|
+
book,
|
|
265
|
+
context.user,
|
|
266
|
+
documentList
|
|
267
|
+
),
|
|
268
|
+
documentList,
|
|
269
|
+
context,
|
|
270
|
+
"text/html"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
/** Export a book as a native `.fidusbook` file. */
|
|
274
|
+
export const exportFidusbook = (
|
|
275
|
+
book: Book,
|
|
276
|
+
documentList: DocumentListEntry[],
|
|
277
|
+
context: BookExportContext
|
|
278
|
+
) =>
|
|
279
|
+
runBookExport(
|
|
280
|
+
new NativeBookExporter(
|
|
281
|
+
context.schema,
|
|
282
|
+
book,
|
|
283
|
+
context.user,
|
|
284
|
+
documentList,
|
|
285
|
+
book.updated ?? 0
|
|
286
|
+
),
|
|
287
|
+
documentList,
|
|
288
|
+
context,
|
|
289
|
+
"application/vnd.fiduswriter.book+zip"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Model of the book dialog's export menu.
|
|
294
|
+
*
|
|
295
|
+
* Every entry saves the book first, so an export always reflects what is on
|
|
296
|
+
* screen. The DOCX and ODT entries are disabled while the book has no
|
|
297
|
+
* matching template.
|
|
298
|
+
*/
|
|
299
|
+
export const bookExportMenuModel = (): BookExportMenu => ({
|
|
300
|
+
content: [
|
|
301
|
+
{
|
|
302
|
+
type: "action",
|
|
303
|
+
title: gettext("Export as BITS"),
|
|
304
|
+
tooltip: gettext("Export book as Book Interchange Tag Set."),
|
|
305
|
+
action: (page: unknown) => {
|
|
306
|
+
const {saveBook, book, documentList, context} =
|
|
307
|
+
page as BookExportMenuArgs
|
|
308
|
+
saveBook().then(() => exportBITS(book, documentList, context))
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
type: "action",
|
|
313
|
+
title: gettext("Export as Epub"),
|
|
314
|
+
tooltip: gettext("Export book as Epub."),
|
|
315
|
+
action: (page: unknown) => {
|
|
316
|
+
const {saveBook, book, documentList, context} =
|
|
317
|
+
page as BookExportMenuArgs
|
|
318
|
+
saveBook().then(() => exportEpub(book, documentList, context))
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
type: "action",
|
|
323
|
+
title: gettext("Export as HTML"),
|
|
324
|
+
tooltip: gettext("Export book as HTML."),
|
|
325
|
+
action: (page: unknown) => {
|
|
326
|
+
const {saveBook, book, documentList, context} =
|
|
327
|
+
page as BookExportMenuArgs
|
|
328
|
+
saveBook().then(() =>
|
|
329
|
+
exportHTML(book, documentList, context, true)
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
type: "action",
|
|
335
|
+
title: gettext("Export as Unified HTML"),
|
|
336
|
+
tooltip: gettext("Export book as Single-file HTML."),
|
|
337
|
+
action: (page: unknown) => {
|
|
338
|
+
const {saveBook, book, documentList, context} =
|
|
339
|
+
page as BookExportMenuArgs
|
|
340
|
+
saveBook().then(() => exportSingleHTML(book, documentList, context))
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
type: "action",
|
|
345
|
+
title: gettext("Export as LaTeX"),
|
|
346
|
+
tooltip: gettext("Export book as LaTeX."),
|
|
347
|
+
action: (page: unknown) => {
|
|
348
|
+
const {saveBook, book, documentList, context} =
|
|
349
|
+
page as BookExportMenuArgs
|
|
350
|
+
saveBook().then(() => exportLatex(book, documentList, context))
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
type: "action",
|
|
355
|
+
title: gettext("Export as DOCX"),
|
|
356
|
+
tooltip: gettext("Export book as DOCX."),
|
|
357
|
+
action: (page: unknown) => {
|
|
358
|
+
const {saveBook, book, documentList, context} =
|
|
359
|
+
page as BookExportMenuArgs
|
|
360
|
+
saveBook().then(() => exportDOCX(book, documentList, context))
|
|
361
|
+
},
|
|
362
|
+
disabled: (page: unknown) => !(page as BookExportMenuArgs).book.docx_template
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
type: "action",
|
|
366
|
+
title: gettext("Export as ODT"),
|
|
367
|
+
tooltip: gettext("Export book as ODT."),
|
|
368
|
+
action: (page: unknown) => {
|
|
369
|
+
const {saveBook, book, documentList, context} =
|
|
370
|
+
page as BookExportMenuArgs
|
|
371
|
+
saveBook().then(() => exportODT(book, documentList, context))
|
|
372
|
+
},
|
|
373
|
+
disabled: (page: unknown) => !(page as BookExportMenuArgs).book.odt_template
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
type: "action",
|
|
377
|
+
title: gettext("Export to Print/PDF"),
|
|
378
|
+
tooltip: gettext("Export book to the print dialog."),
|
|
379
|
+
action: (page: unknown) => {
|
|
380
|
+
const {saveBook, book, documentList, context} =
|
|
381
|
+
page as BookExportMenuArgs
|
|
382
|
+
saveBook().then(() => exportPrint(book, documentList, context))
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
type: "action",
|
|
387
|
+
title: gettext("Export as Fidusbook"),
|
|
388
|
+
tooltip: gettext(
|
|
389
|
+
"Export book as a .fidusbook file (for moving to another server)."
|
|
390
|
+
),
|
|
391
|
+
action: (page: unknown) => {
|
|
392
|
+
const {saveBook, book, documentList, context} =
|
|
393
|
+
page as BookExportMenuArgs
|
|
394
|
+
saveBook().then(() => exportFidusbook(book, documentList, context))
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
]
|
|
398
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The book edit dialog.
|
|
3
|
+
*
|
|
4
|
+
* A reusable, backend-agnostic tabbed dialog for editing a book's metadata,
|
|
5
|
+
* settings, cover image and chapter list, running a sanity check and starting
|
|
6
|
+
* an export. Import it from the `@fiduswriter/books-document/ui/book_dialog`
|
|
7
|
+
* subpath so that the main entry stays free of DOM code and remains usable
|
|
8
|
+
* from Node (as `@fiduswriter/cli` needs).
|
|
9
|
+
*
|
|
10
|
+
* ```js
|
|
11
|
+
* import {BookDialog} from "@fiduswriter/books-document/ui/book_dialog"
|
|
12
|
+
*
|
|
13
|
+
* new BookDialog({
|
|
14
|
+
* book, // omit to create a new book
|
|
15
|
+
* documentList,
|
|
16
|
+
* schema,
|
|
17
|
+
* chapterLoader, // @fiduswriter/books-document
|
|
18
|
+
* e2ee, // @fiduswriter/books-document
|
|
19
|
+
* backend: {
|
|
20
|
+
* saveBook: book => postJson("/api/book/save/", {book}),
|
|
21
|
+
* selectCoverImage: id => showImagePicker(id),
|
|
22
|
+
* saveDocxTemplate: (book, file) => uploadTemplate(book, file)
|
|
23
|
+
* },
|
|
24
|
+
* exportContext: {schema, chapterLoader, e2ee, csl, bookStyles, user, deliver: saveFile}
|
|
25
|
+
* }).open()
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* `ChapterLoader` and `E2EEStrategy` are the same interfaces the book
|
|
29
|
+
* exporters already accept, so a host that can export a book needs no extra
|
|
30
|
+
* wiring to run a sanity check.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export {BookDialog} from "./dialog.js"
|
|
34
|
+
export {
|
|
35
|
+
bookSanityCheck
|
|
36
|
+
} from "./sanity_check.js"
|
|
37
|
+
export {
|
|
38
|
+
bookBasicInfoTemplate,
|
|
39
|
+
bookBibliographyDataTemplate,
|
|
40
|
+
bookChapterDialogTemplate,
|
|
41
|
+
bookChapterListTemplate,
|
|
42
|
+
bookDialogChaptersTemplate,
|
|
43
|
+
bookDialogTemplate,
|
|
44
|
+
bookDOCXDataRowTemplate,
|
|
45
|
+
bookDOCXDataTemplate,
|
|
46
|
+
bookEpubDataCoverTemplate,
|
|
47
|
+
bookEpubDataTemplate,
|
|
48
|
+
bookODTDataRowTemplate,
|
|
49
|
+
bookODTDataTemplate,
|
|
50
|
+
bookPrintDataTemplate,
|
|
51
|
+
bookSanityCheckTemplate
|
|
52
|
+
} from "./templates.js"
|
|
53
|
+
export {
|
|
54
|
+
bookExportMenuModel,
|
|
55
|
+
exportBITS,
|
|
56
|
+
exportDOCX,
|
|
57
|
+
exportEpub,
|
|
58
|
+
exportFidusbook,
|
|
59
|
+
exportHTML,
|
|
60
|
+
exportLatex,
|
|
61
|
+
exportODT,
|
|
62
|
+
exportPrint,
|
|
63
|
+
exportSingleHTML,
|
|
64
|
+
runBookExport
|
|
65
|
+
} from "./export.js"
|
|
66
|
+
export type {
|
|
67
|
+
BookExportMenu,
|
|
68
|
+
BookExportMenuArgs,
|
|
69
|
+
BookExporterLike
|
|
70
|
+
} from "./export.js"
|
|
71
|
+
export type {
|
|
72
|
+
BookDataContext,
|
|
73
|
+
BookDialogBackend,
|
|
74
|
+
BookDialogCoverImage,
|
|
75
|
+
BookDialogImageDB,
|
|
76
|
+
BookDialogInfo,
|
|
77
|
+
BookDialogOptions,
|
|
78
|
+
BookDialogPart,
|
|
79
|
+
BookExportContext,
|
|
80
|
+
BookExportDeliverer
|
|
81
|
+
} from "./types.js"
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Book sanity check.
|
|
3
|
+
*
|
|
4
|
+
* Loads whatever chapter data is missing, decrypts encrypted chapters through
|
|
5
|
+
* the injected {@link BookDataContext} and reports unresolved tracked changes,
|
|
6
|
+
* comments, cross references and internal links.
|
|
7
|
+
*
|
|
8
|
+
* Only the context touches the host; the analysis itself is entirely
|
|
9
|
+
* host-agnostic and shared by every consumer of this package.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {getMissingChapterData} from "../../exporter/tools.js"
|
|
13
|
+
import type {Book, Chapter, DocumentListEntry} from "../../types.js"
|
|
14
|
+
import {gettext, longFilePath} from "fwtoolkit"
|
|
15
|
+
import type {BookDataContext} from "./types.js"
|
|
16
|
+
|
|
17
|
+
/** A node of a chapter's raw ProseMirror content. */
|
|
18
|
+
interface RawNode {
|
|
19
|
+
type?: string
|
|
20
|
+
attrs?: Record<string, never> & {
|
|
21
|
+
track?: unknown[]
|
|
22
|
+
href?: string
|
|
23
|
+
title?: string
|
|
24
|
+
id?: string
|
|
25
|
+
approved?: boolean
|
|
26
|
+
}
|
|
27
|
+
marks?: RawMark[]
|
|
28
|
+
content?: RawNode[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A mark of a raw ProseMirror node. */
|
|
32
|
+
interface RawMark {
|
|
33
|
+
type?: string
|
|
34
|
+
attrs?: {
|
|
35
|
+
href?: string
|
|
36
|
+
title?: string
|
|
37
|
+
id?: string
|
|
38
|
+
approved?: boolean
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The chapters' collected issues, split by severity. */
|
|
43
|
+
interface SanityMessages {
|
|
44
|
+
warnings: string[]
|
|
45
|
+
errors: string[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* Chapters whose document the user cannot see have no `doc` to take a title
|
|
50
|
+
* and path from, so fall back to the same placeholder `longFilePath` uses for
|
|
51
|
+
* an untitled document.
|
|
52
|
+
*/
|
|
53
|
+
const labelChapter = (chapter: Chapter, doc?: DocumentListEntry): string =>
|
|
54
|
+
`(${gettext("Chapter")} ${chapter.number}, ${longFilePath(
|
|
55
|
+
doc?.title ?? gettext("Untitled"),
|
|
56
|
+
doc?.path ?? "/"
|
|
57
|
+
)})`
|
|
58
|
+
|
|
59
|
+
function findContentIssues(
|
|
60
|
+
node: RawNode,
|
|
61
|
+
chapter: Chapter,
|
|
62
|
+
doc: DocumentListEntry,
|
|
63
|
+
messages: SanityMessages
|
|
64
|
+
): void {
|
|
65
|
+
if (node.attrs?.track?.length) {
|
|
66
|
+
messages.warnings.push(
|
|
67
|
+
`${gettext("Unresolved tracked changes")} ${labelChapter(chapter, doc)}`
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
if (node.marks) {
|
|
71
|
+
node.marks.forEach(mark => {
|
|
72
|
+
if (
|
|
73
|
+
mark.type == "link" &&
|
|
74
|
+
mark.attrs?.href?.charAt(0) === "#" &&
|
|
75
|
+
!mark.attrs?.title
|
|
76
|
+
) {
|
|
77
|
+
messages.warnings.push(
|
|
78
|
+
`${gettext("Internal links without target")} ${labelChapter(
|
|
79
|
+
chapter,
|
|
80
|
+
doc
|
|
81
|
+
)}`
|
|
82
|
+
)
|
|
83
|
+
} else if (
|
|
84
|
+
mark.type == "comment" &&
|
|
85
|
+
(doc.comments as Array<{resolved?: boolean}> | undefined)?.[
|
|
86
|
+
Number.parseInt(mark.attrs?.id as string)
|
|
87
|
+
]?.resolved === false
|
|
88
|
+
) {
|
|
89
|
+
messages.warnings.push(
|
|
90
|
+
`${gettext("Unresolved comments")} ${labelChapter(chapter, doc)}`
|
|
91
|
+
)
|
|
92
|
+
} else if (
|
|
93
|
+
mark.type === "deletion" ||
|
|
94
|
+
(mark.type === "insertion" && mark.attrs?.approved === false)
|
|
95
|
+
) {
|
|
96
|
+
messages.warnings.push(
|
|
97
|
+
`${gettext("Unresolved tracked changes")} ${labelChapter(
|
|
98
|
+
chapter,
|
|
99
|
+
doc
|
|
100
|
+
)}`
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
if (node.type === "cross_reference" && !node.attrs?.title) {
|
|
106
|
+
messages.warnings.push(
|
|
107
|
+
`${gettext("Cross references without targets")} ${labelChapter(
|
|
108
|
+
chapter,
|
|
109
|
+
doc
|
|
110
|
+
)}`
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (node.content) {
|
|
115
|
+
node.content.forEach(subNode =>
|
|
116
|
+
findContentIssues(subNode, chapter, doc, messages)
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Run a sanity check on a book.
|
|
123
|
+
*
|
|
124
|
+
* @param book - The book to check.
|
|
125
|
+
* @param documentList - Document entries for the book's chapters.
|
|
126
|
+
* @param context - The host's schema, chapter loader and E2EE strategy.
|
|
127
|
+
* @returns A promise resolving to the report as an HTML fragment.
|
|
128
|
+
*/
|
|
129
|
+
export const bookSanityCheck = (
|
|
130
|
+
book: Book,
|
|
131
|
+
documentList: DocumentListEntry[],
|
|
132
|
+
{schema, chapterLoader, e2ee}: BookDataContext
|
|
133
|
+
): Promise<string> => {
|
|
134
|
+
if (!book.chapters.length) {
|
|
135
|
+
return Promise.resolve(
|
|
136
|
+
`<ul class="fw-errorlist"><li>${gettext(
|
|
137
|
+
"The book contains no chapters."
|
|
138
|
+
)}</li></ul>`
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
return getMissingChapterData(book, documentList, schema, {
|
|
142
|
+
rawContent: true,
|
|
143
|
+
loader: chapterLoader,
|
|
144
|
+
e2ee
|
|
145
|
+
})
|
|
146
|
+
.then(() => {
|
|
147
|
+
const messages: SanityMessages = {
|
|
148
|
+
warnings: [],
|
|
149
|
+
errors: []
|
|
150
|
+
}
|
|
151
|
+
book.chapters.forEach(chapter => {
|
|
152
|
+
const doc = documentList.find(doc => doc.id === chapter.text)
|
|
153
|
+
if (!doc || !doc.rawContent) {
|
|
154
|
+
messages.errors.push(
|
|
155
|
+
`${gettext("No access")} ${labelChapter(chapter, doc)}`
|
|
156
|
+
)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
if (!doc.title || !doc.title.length) {
|
|
160
|
+
messages.warnings.push(
|
|
161
|
+
`${gettext("No chapter title")} ${labelChapter(chapter, doc)}`
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
findContentIssues(
|
|
165
|
+
doc.rawContent as unknown as RawNode,
|
|
166
|
+
chapter,
|
|
167
|
+
doc,
|
|
168
|
+
messages
|
|
169
|
+
)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const warnings = Array.from(new Set(messages.warnings))
|
|
173
|
+
const errors = Array.from(new Set(messages.errors))
|
|
174
|
+
if (!warnings.length && !errors.length) {
|
|
175
|
+
return `<p>${gettext("No issues were found:")}</p>
|
|
176
|
+
<p>${gettext("The book contains chapters.")}</p>
|
|
177
|
+
<p>${gettext("Each chapter has a title.")}</p>
|
|
178
|
+
<p>${gettext("There are no unresolved tracked changes.")}</p>
|
|
179
|
+
<p>${gettext("There are no unresolved comments.")}</p>
|
|
180
|
+
<p>${gettext("All cross references have working targets.")}</p>
|
|
181
|
+
<p>${gettext(
|
|
182
|
+
"All internal links have working targets."
|
|
183
|
+
)}</p>`
|
|
184
|
+
}
|
|
185
|
+
return `<ul class="warninglist">
|
|
186
|
+
${warnings.map(warning => `<li>${warning}</li>`).join("")}
|
|
187
|
+
</ul>
|
|
188
|
+
<ul class="fw-errorlist">
|
|
189
|
+
${errors.map(error => `<li>${error}</li>`).join("")}
|
|
190
|
+
</ul>`
|
|
191
|
+
})
|
|
192
|
+
.catch((err: Error) => {
|
|
193
|
+
// Passphrase unavailable or user cancelled the unlock dialog.
|
|
194
|
+
if (
|
|
195
|
+
err.message &&
|
|
196
|
+
err.message.toLowerCase().includes("passphrase is required")
|
|
197
|
+
) {
|
|
198
|
+
return `<ul class="fw-errorlist"><li>${gettext(
|
|
199
|
+
"This book contains encrypted chapters. A personal passphrase is required to perform a sanity check. Please set up or unlock your personal passphrase in your profile settings."
|
|
200
|
+
)}</li></ul>`
|
|
201
|
+
}
|
|
202
|
+
// Individual chapter decryption failure — alerts were already
|
|
203
|
+
// shown by decryptE2EEChapters; show a summary here.
|
|
204
|
+
return `<ul class="fw-errorlist"><li>${gettext(
|
|
205
|
+
"One or more encrypted chapters could not be decrypted. See the error notifications for details."
|
|
206
|
+
)}</li></ul>`
|
|
207
|
+
})
|
|
208
|
+
}
|