@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.
Files changed (31) hide show
  1. package/dist/ui/book_dialog/dialog.d.ts +98 -0
  2. package/dist/ui/book_dialog/dialog.d.ts.map +1 -0
  3. package/dist/ui/book_dialog/dialog.js +590 -0
  4. package/dist/ui/book_dialog/dialog.js.map +1 -0
  5. package/dist/ui/book_dialog/export.d.ts +81 -0
  6. package/dist/ui/book_dialog/export.d.ts.map +1 -0
  7. package/dist/ui/book_dialog/export.js +184 -0
  8. package/dist/ui/book_dialog/export.js.map +1 -0
  9. package/dist/ui/book_dialog/index.d.ts +38 -0
  10. package/dist/ui/book_dialog/index.d.ts.map +1 -0
  11. package/dist/ui/book_dialog/index.js +36 -0
  12. package/dist/ui/book_dialog/index.js.map +1 -0
  13. package/dist/ui/book_dialog/sanity_check.d.ts +22 -0
  14. package/dist/ui/book_dialog/sanity_check.d.ts.map +1 -0
  15. package/dist/ui/book_dialog/sanity_check.js +109 -0
  16. package/dist/ui/book_dialog/sanity_check.js.map +1 -0
  17. package/dist/ui/book_dialog/templates.d.ts +74 -0
  18. package/dist/ui/book_dialog/templates.d.ts.map +1 -0
  19. package/dist/ui/book_dialog/templates.js +526 -0
  20. package/dist/ui/book_dialog/templates.js.map +1 -0
  21. package/dist/ui/book_dialog/types.d.ts +165 -0
  22. package/dist/ui/book_dialog/types.d.ts.map +1 -0
  23. package/dist/ui/book_dialog/types.js +15 -0
  24. package/dist/ui/book_dialog/types.js.map +1 -0
  25. package/package.json +5 -1
  26. package/src/ui/book_dialog/dialog.ts +765 -0
  27. package/src/ui/book_dialog/export.ts +398 -0
  28. package/src/ui/book_dialog/index.ts +81 -0
  29. package/src/ui/book_dialog/sanity_check.ts +208 -0
  30. package/src/ui/book_dialog/templates.ts +705 -0
  31. 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
+ }