bookish-press 0.7.4 → 0.9.2
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 +56 -9
- package/dist/components/Auth.svelte +29 -39
- package/dist/components/Auth.svelte.d.ts +5 -17
- package/dist/components/BookPreview.svelte +14 -20
- package/dist/components/BookPreview.svelte.d.ts +6 -18
- package/dist/components/Link.svelte +19 -30
- package/dist/components/Link.svelte.d.ts +7 -20
- package/dist/components/app/BookList.svelte +2 -3
- package/dist/components/app/BookList.svelte.d.ts +6 -18
- package/dist/components/app/Button.svelte +12 -8
- package/dist/components/app/Button.svelte.d.ts +10 -23
- package/dist/components/app/Feedback.svelte +4 -4
- package/dist/components/app/Feedback.svelte.d.ts +6 -19
- package/dist/components/app/Head.svelte +2 -2
- package/dist/components/app/Head.svelte.d.ts +6 -19
- package/dist/components/app/Large.svelte +4 -1
- package/dist/components/app/Large.svelte.d.ts +5 -28
- package/dist/components/app/Lead.svelte +4 -1
- package/dist/components/app/Lead.svelte.d.ts +5 -28
- package/dist/components/app/Link.svelte +12 -9
- package/dist/components/app/Link.svelte.d.ts +9 -22
- package/dist/components/app/Options.svelte +8 -6
- package/dist/components/app/Options.svelte.d.ts +8 -20
- package/dist/components/app/Paragraph.svelte +2 -2
- package/dist/components/app/Paragraph.svelte.d.ts +6 -19
- package/dist/components/app/Section.svelte +4 -1
- package/dist/components/app/Section.svelte.d.ts +5 -28
- package/dist/components/app/Table.svelte +4 -1
- package/dist/components/app/Table.svelte.d.ts +5 -28
- package/dist/components/app/TextInput.svelte +10 -8
- package/dist/components/app/TextInput.svelte.d.ts +12 -23
- package/dist/components/chapter/Atom.svelte +4 -7
- package/dist/components/chapter/Atom.svelte.d.ts +6 -19
- package/dist/components/chapter/Block.svelte +21 -23
- package/dist/components/chapter/Block.svelte.d.ts +5 -17
- package/dist/components/chapter/Callout.svelte +3 -5
- package/dist/components/chapter/Callout.svelte.d.ts +5 -17
- package/dist/components/chapter/CaptionedCode.svelte +12 -14
- package/dist/components/chapter/CaptionedCode.svelte.d.ts +5 -17
- package/dist/components/chapter/ChapterBody.svelte +14 -17
- package/dist/components/chapter/ChapterBody.svelte.d.ts +7 -19
- package/dist/components/chapter/ChapterBody.test.d.ts +1 -0
- package/dist/components/chapter/ChapterBody.test.js +85 -0
- package/dist/components/chapter/Citations.svelte +39 -45
- package/dist/components/chapter/Citations.svelte.d.ts +5 -17
- package/dist/components/chapter/Code.svelte +158 -155
- package/dist/components/chapter/Code.svelte.d.ts +10 -23
- package/dist/components/chapter/Comment.svelte +30 -23
- package/dist/components/chapter/Comment.svelte.d.ts +5 -17
- package/dist/components/chapter/Definition.svelte +36 -34
- package/dist/components/chapter/Definition.svelte.d.ts +5 -17
- package/dist/components/chapter/Embed.svelte +78 -72
- package/dist/components/chapter/Embed.svelte.d.ts +8 -20
- package/dist/components/chapter/ErrorMessage.svelte +2 -4
- package/dist/components/chapter/ErrorMessage.svelte.d.ts +5 -17
- package/dist/components/chapter/Figure.svelte +22 -29
- package/dist/components/chapter/Figure.svelte.d.ts +9 -22
- package/dist/components/chapter/Footnote.svelte +40 -32
- package/dist/components/chapter/Footnote.svelte.d.ts +5 -17
- package/dist/components/chapter/Format.svelte +26 -41
- package/dist/components/chapter/Format.svelte.d.ts +7 -18
- package/dist/components/chapter/InlineCode.svelte +7 -8
- package/dist/components/chapter/InlineCode.svelte.d.ts +5 -17
- package/dist/components/chapter/Label.svelte +5 -11
- package/dist/components/chapter/Label.svelte.d.ts +5 -17
- package/dist/components/chapter/LineBreak.svelte +1 -3
- package/dist/components/chapter/LineBreak.svelte.d.ts +5 -17
- package/dist/components/chapter/Link.svelte +17 -23
- package/dist/components/chapter/Link.svelte.d.ts +5 -17
- package/dist/components/chapter/List.svelte +9 -9
- package/dist/components/chapter/List.svelte.d.ts +6 -17
- package/dist/components/chapter/Marginal.svelte +59 -60
- package/dist/components/chapter/Marginal.svelte.d.ts +10 -22
- package/dist/components/chapter/Paragraph.svelte +8 -23
- package/dist/components/chapter/Paragraph.svelte.d.ts +6 -18
- package/dist/components/chapter/Positioned.svelte +2 -4
- package/dist/components/chapter/Positioned.svelte.d.ts +6 -19
- package/dist/components/chapter/Problem.svelte +3 -2
- package/dist/components/chapter/Problem.svelte.d.ts +5 -28
- package/dist/components/chapter/Python.svelte +45 -58
- package/dist/components/chapter/Python.svelte.d.ts +6 -18
- package/dist/components/chapter/Quote.svelte +5 -7
- package/dist/components/chapter/Quote.svelte.d.ts +5 -17
- package/dist/components/chapter/Rule.svelte +1 -3
- package/dist/components/chapter/Rule.svelte.d.ts +5 -17
- package/dist/components/chapter/Table.svelte +6 -8
- package/dist/components/chapter/Table.svelte.d.ts +5 -17
- package/dist/components/chapter/Text.svelte +54 -69
- package/dist/components/chapter/Text.svelte.d.ts +6 -18
- package/dist/components/controls/DarkToggle.svelte +7 -8
- package/dist/components/controls/DarkToggle.svelte.d.ts +6 -18
- package/dist/components/editor/BookishEditor.svelte +583 -761
- package/dist/components/editor/BookishEditor.svelte.d.ts +14 -26
- package/dist/components/editor/CalloutEditor.svelte +3 -3
- package/dist/components/editor/CalloutEditor.svelte.d.ts +5 -17
- package/dist/components/editor/CaptionedCodeEditor.svelte +5 -5
- package/dist/components/editor/CaptionedCodeEditor.svelte.d.ts +5 -17
- package/dist/components/editor/CaretView.svelte +23 -25
- package/dist/components/editor/CaretView.svelte.d.ts +15 -27
- package/dist/components/editor/CitationsEditor.svelte +25 -28
- package/dist/components/editor/CitationsEditor.svelte.d.ts +5 -17
- package/dist/components/editor/ConfirmButton.svelte +9 -6
- package/dist/components/editor/ConfirmButton.svelte.d.ts +8 -21
- package/dist/components/editor/DefinitionEditor.svelte +11 -15
- package/dist/components/editor/DefinitionEditor.svelte.d.ts +5 -17
- package/dist/components/editor/EmbedEditor.svelte +44 -45
- package/dist/components/editor/EmbedEditor.svelte.d.ts +5 -17
- package/dist/components/editor/Icon.svelte +1 -1
- package/dist/components/editor/Icon.svelte.d.ts +5 -17
- package/dist/components/editor/ImageChooser.svelte +15 -18
- package/dist/components/editor/ImageChooser.svelte.d.ts +6 -18
- package/dist/components/editor/ImageThumbnail.svelte +29 -24
- package/dist/components/editor/ImageThumbnail.svelte.d.ts +7 -19
- package/dist/components/editor/InlineCodeEditor.svelte +3 -3
- package/dist/components/editor/InlineCodeEditor.svelte.d.ts +5 -17
- package/dist/components/editor/LabelEditor.svelte +5 -6
- package/dist/components/editor/LabelEditor.svelte.d.ts +5 -17
- package/dist/components/editor/LanguageEditor.svelte +4 -5
- package/dist/components/editor/LanguageEditor.svelte.d.ts +6 -18
- package/dist/components/editor/LinkEditor.svelte +52 -68
- package/dist/components/editor/LinkEditor.svelte.d.ts +5 -17
- package/dist/components/editor/Note.svelte +4 -1
- package/dist/components/editor/Note.svelte.d.ts +5 -28
- package/dist/components/editor/Permissions.svelte +36 -34
- package/dist/components/editor/Permissions.svelte.d.ts +9 -21
- package/dist/components/editor/PositionEditor.svelte +6 -7
- package/dist/components/editor/PositionEditor.svelte.d.ts +6 -18
- package/dist/components/editor/QuoteEditor.svelte +3 -3
- package/dist/components/editor/QuoteEditor.svelte.d.ts +5 -17
- package/dist/components/editor/Switch.svelte +16 -11
- package/dist/components/editor/Switch.svelte.d.ts +9 -21
- package/dist/components/editor/TableEditor.svelte +3 -3
- package/dist/components/editor/TableEditor.svelte.d.ts +5 -17
- package/dist/components/editor/TextEditor.svelte +74 -87
- package/dist/components/editor/TextEditor.svelte.d.ts +13 -25
- package/dist/components/editor/Toggle.svelte +18 -21
- package/dist/components/editor/Toggle.svelte.d.ts +7 -20
- package/dist/components/editor/Toolbar.svelte +140 -145
- package/dist/components/editor/Toolbar.svelte.d.ts +5 -17
- package/dist/components/editor/ToolbarGroup.svelte +4 -4
- package/dist/components/editor/ToolbarGroup.svelte.d.ts +6 -19
- package/dist/components/editor/ToolbarSpacer.svelte +1 -1
- package/dist/components/editor/ToolbarSpacer.svelte.d.ts +22 -21
- package/dist/components/editor/URLEditor.svelte +3 -5
- package/dist/components/editor/URLEditor.svelte.d.ts +7 -19
- package/dist/components/page/Acknowledgements.svelte +14 -8
- package/dist/components/page/Acknowledgements.svelte.d.ts +3 -16
- package/dist/components/page/Analytics.svelte +2 -3
- package/dist/components/page/Analytics.svelte.d.ts +6 -17
- package/dist/components/page/Authors.svelte +29 -29
- package/dist/components/page/Authors.svelte.d.ts +11 -23
- package/dist/components/page/BookLoader.svelte +47 -43
- package/dist/components/page/BookLoader.svelte.d.ts +5 -17
- package/dist/components/page/BulkReferenceEditor.svelte +43 -26
- package/dist/components/page/BulkReferenceEditor.svelte.d.ts +3 -16
- package/dist/components/page/Chapter.svelte +224 -249
- package/dist/components/page/Chapter.svelte.d.ts +6 -18
- package/dist/components/page/ChapterContext.d.ts +3 -1
- package/dist/components/page/ChapterNumber.svelte +4 -1
- package/dist/components/page/ChapterNumber.svelte.d.ts +5 -28
- package/dist/components/page/ChapterTitle.svelte +4 -4
- package/dist/components/page/ChapterTitle.svelte.d.ts +6 -19
- package/dist/components/page/Contexts.js +2 -2
- package/dist/components/page/DefinitionView.svelte +51 -44
- package/dist/components/page/DefinitionView.svelte.d.ts +6 -18
- package/dist/components/page/Description.svelte +10 -4
- package/dist/components/page/Description.svelte.d.ts +16 -14
- package/dist/components/page/DownloadEPUB.svelte +234 -0
- package/dist/components/page/DownloadEPUB.svelte.d.ts +3 -0
- package/dist/components/page/EditableHeader.svelte +4 -6
- package/dist/components/page/EditableHeader.svelte.d.ts +7 -18
- package/dist/components/page/Edition.svelte +57 -71
- package/dist/components/page/Edition.svelte.d.ts +7 -20
- package/dist/components/page/EditionLoader.svelte +119 -82
- package/dist/components/page/EditionLoader.svelte.d.ts +6 -19
- package/dist/components/page/Editions.svelte +26 -31
- package/dist/components/page/Editions.svelte.d.ts +5 -17
- package/dist/components/page/Glossary.svelte +31 -35
- package/dist/components/page/Glossary.svelte.d.ts +3 -16
- package/dist/components/page/Header.svelte +43 -49
- package/dist/components/page/Header.svelte.d.ts +19 -30
- package/dist/components/page/Index.svelte +46 -46
- package/dist/components/page/Index.svelte.d.ts +3 -16
- package/dist/components/page/Instructions.svelte +2 -2
- package/dist/components/page/Instructions.svelte.d.ts +6 -19
- package/dist/components/page/License.svelte +13 -7
- package/dist/components/page/License.svelte.d.ts +16 -14
- package/dist/components/page/Loading.svelte +9 -9
- package/dist/components/page/Loading.svelte.d.ts +22 -21
- package/dist/components/page/Media.svelte +36 -27
- package/dist/components/page/Media.svelte.d.ts +3 -16
- package/dist/components/page/MediaPreview.svelte +22 -19
- package/dist/components/page/MediaPreview.svelte.d.ts +8 -21
- package/dist/components/page/Muted.svelte +4 -1
- package/dist/components/page/Muted.svelte.d.ts +5 -28
- package/dist/components/page/Outline.svelte +75 -97
- package/dist/components/page/Outline.svelte.d.ts +8 -20
- package/dist/components/page/Page.svelte +23 -33
- package/dist/components/page/Page.svelte.d.ts +7 -20
- package/dist/components/page/PageHeader.svelte +2 -2
- package/dist/components/page/PageHeader.svelte.d.ts +6 -19
- package/dist/components/page/PageParagraph.svelte +4 -1
- package/dist/components/page/PageParagraph.svelte.d.ts +5 -28
- package/dist/components/page/PossibleChapter.svelte +7 -7
- package/dist/components/page/PossibleChapter.svelte.d.ts +3 -16
- package/dist/components/page/PossibleReference.svelte +4 -5
- package/dist/components/page/PossibleReference.svelte.d.ts +6 -18
- package/dist/components/page/Print.svelte +11 -12
- package/dist/components/page/Print.svelte.d.ts +3 -16
- package/dist/components/page/Publish.svelte +47 -47
- package/dist/components/page/Publish.svelte.d.ts +3 -16
- package/dist/components/page/Reference.svelte +24 -35
- package/dist/components/page/Reference.svelte.d.ts +6 -18
- package/dist/components/page/References.svelte +21 -22
- package/dist/components/page/References.svelte.d.ts +3 -16
- package/dist/components/page/Rows.svelte +5 -2
- package/dist/components/page/Rows.svelte.d.ts +5 -28
- package/dist/components/page/Search.svelte +59 -61
- package/dist/components/page/Search.svelte.d.ts +3 -16
- package/dist/components/page/Status.svelte +2 -2
- package/dist/components/page/Status.svelte.d.ts +16 -14
- package/dist/components/page/SubdomainEditor.svelte +11 -11
- package/dist/components/page/SubdomainEditor.svelte.d.ts +16 -14
- package/dist/components/page/TableOfContents.svelte +157 -124
- package/dist/components/page/TableOfContents.svelte.d.ts +3 -16
- package/dist/components/page/TableOfContentsImage.svelte +6 -7
- package/dist/components/page/TableOfContentsImage.svelte.d.ts +6 -18
- package/dist/components/page/TableOfContentsRow.svelte +32 -32
- package/dist/components/page/TableOfContentsRow.svelte.d.ts +10 -22
- package/dist/components/page/ThemeEditor.svelte +35 -28
- package/dist/components/page/ThemeEditor.svelte.d.ts +3 -16
- package/dist/components/page/ThemeEditorPreview.svelte +11 -13
- package/dist/components/page/ThemeEditorPreview.svelte.d.ts +5 -17
- package/dist/components/page/ThemeSetEditor.svelte +4 -6
- package/dist/components/page/ThemeSetEditor.svelte.d.ts +7 -19
- package/dist/components/page/ThemeVariableEditor.svelte +9 -11
- package/dist/components/page/ThemeVariableEditor.svelte.d.ts +7 -19
- package/dist/components/page/Title.svelte +4 -1
- package/dist/components/page/Title.svelte.d.ts +5 -28
- package/dist/components/page/Unknown.svelte +11 -8
- package/dist/components/page/Unknown.svelte.d.ts +5 -17
- package/dist/epub/build.d.ts +39 -0
- package/dist/epub/build.js +143 -0
- package/dist/epub/build.test.d.ts +1 -0
- package/dist/epub/build.test.js +411 -0
- package/dist/epub/images.d.ts +79 -0
- package/dist/epub/images.js +260 -0
- package/dist/epub/images.test.d.ts +1 -0
- package/dist/epub/images.test.js +129 -0
- package/dist/epub/package.d.ts +75 -0
- package/dist/epub/package.js +392 -0
- package/dist/epub/stylesheet.test.d.ts +1 -0
- package/dist/epub/stylesheet.test.js +243 -0
- package/dist/epub/xhtml.d.ts +69 -0
- package/dist/epub/xhtml.js +367 -0
- package/dist/epub/xhtml.test.d.ts +1 -0
- package/dist/epub/xhtml.test.js +283 -0
- package/dist/epub/zip.d.ts +35 -0
- package/dist/epub/zip.js +202 -0
- package/dist/epub/zip.test.d.ts +1 -0
- package/dist/epub/zip.test.js +171 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models/Firebase.js +18 -4
- package/dist/models/book/Book.js +2 -1
- package/dist/models/book/BookMedia.js +1 -2
- package/dist/models/book/Chapter.js +3 -2
- package/dist/models/book/ChapterID.d.ts +1 -1
- package/dist/models/book/Edition.d.ts +1 -1
- package/dist/models/book/Edition.js +6 -1
- package/dist/models/chapter/BlocksNode.js +3 -1
- package/dist/models/chapter/BlocksNode.test.js +5 -1
- package/dist/models/chapter/Node.test.js +2 -2
- package/dist/models/chapter/TextNode.js +8 -5
- package/dist/models/chapter/TextNode.test.js +8 -0
- package/package.json +36 -22
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializes chapter ASTs to the XHTML that EPUB requires.
|
|
3
|
+
*
|
|
4
|
+
* This deliberately does not reuse `Node.toHTML()`. That serializer is lossy by
|
|
5
|
+
* design -- footnotes, citations and definitions collapse to parenthesised text
|
|
6
|
+
* and labels vanish -- and it has no access to the edition, so it cannot number
|
|
7
|
+
* a footnote or resolve a citation to its reference. It also emits HTML, not
|
|
8
|
+
* XHTML, and EPUB readers reject documents that are not well-formed XML.
|
|
9
|
+
*
|
|
10
|
+
* The three things the web reader renders as marginal pop-ups (footnotes,
|
|
11
|
+
* citations, definitions) all become ordinary links here, because an e-reader
|
|
12
|
+
* has no margin to put them in.
|
|
13
|
+
*/
|
|
14
|
+
import type Edition from '../models/book/Edition';
|
|
15
|
+
import type ChapterNode from '../models/chapter/ChapterNode';
|
|
16
|
+
import FormatNode from '../models/chapter/FormatNode';
|
|
17
|
+
/**
|
|
18
|
+
* Escape text content. Order matters: ampersands first, or we escape our own
|
|
19
|
+
* escapes.
|
|
20
|
+
*
|
|
21
|
+
* The argument is deliberately `unknown`. Book data is JSON that predates the
|
|
22
|
+
* current types and is only loosely validated, so fields the model declares as
|
|
23
|
+
* strings arrive as numbers or null in real books -- reference years in
|
|
24
|
+
* particular. Coercing here keeps one stale field from throwing partway through
|
|
25
|
+
* an export and losing the whole book.
|
|
26
|
+
*/
|
|
27
|
+
export declare function escapeText(text: unknown): string;
|
|
28
|
+
/** Escape an attribute value, which additionally must not close its own quotes. */
|
|
29
|
+
export declare function escapeAttribute(value: unknown): string;
|
|
30
|
+
/**
|
|
31
|
+
* The context a chapter needs to resolve its cross-references: which edition it
|
|
32
|
+
* belongs to, which images made it into the package, and which chapters exist
|
|
33
|
+
* (so a link to a missing one degrades to plain text rather than a dead link).
|
|
34
|
+
*/
|
|
35
|
+
export type SerializationContext = {
|
|
36
|
+
edition: Edition;
|
|
37
|
+
/** Maps an embed's URL to its path inside the package, e.g. `images/3.jpg`. */
|
|
38
|
+
images: Map<string, string>;
|
|
39
|
+
/** Chapter IDs that have their own document in the package. */
|
|
40
|
+
chapters: Set<string>;
|
|
41
|
+
};
|
|
42
|
+
/** A footnote gathered while serializing, to be emitted as an endnote. */
|
|
43
|
+
type Endnote = {
|
|
44
|
+
id: string;
|
|
45
|
+
symbol: string;
|
|
46
|
+
content: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Serialize one chapter's blocks, returning the body markup and the endnotes
|
|
50
|
+
* that its footnotes produced. Callers place the endnotes wherever they like;
|
|
51
|
+
* `chapterDocument` puts them at the end of the chapter.
|
|
52
|
+
*/
|
|
53
|
+
export declare function serializeChapter(chapter: ChapterNode, context: SerializationContext): {
|
|
54
|
+
body: string;
|
|
55
|
+
endnotes: Endnote[];
|
|
56
|
+
};
|
|
57
|
+
/** The document filename for a chapter. */
|
|
58
|
+
export declare function chapterPath(chapterID: string): string;
|
|
59
|
+
/** Serialize a standalone format node, for titles, captions and definitions. */
|
|
60
|
+
export declare function serializeFormat(node: FormatNode, context: SerializationContext): string;
|
|
61
|
+
/** Render gathered footnotes as an endnote section with links back to the text. */
|
|
62
|
+
export declare function endnoteSection(endnotes: Endnote[]): string;
|
|
63
|
+
/**
|
|
64
|
+
* Wrap body markup in a complete XHTML document. The epub namespace is declared
|
|
65
|
+
* on every document because footnote markup uses `epub:type`, and an undeclared
|
|
66
|
+
* prefix makes the XML malformed.
|
|
67
|
+
*/
|
|
68
|
+
export declare function document_(title: string, body: string): string;
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializes chapter ASTs to the XHTML that EPUB requires.
|
|
3
|
+
*
|
|
4
|
+
* This deliberately does not reuse `Node.toHTML()`. That serializer is lossy by
|
|
5
|
+
* design -- footnotes, citations and definitions collapse to parenthesised text
|
|
6
|
+
* and labels vanish -- and it has no access to the edition, so it cannot number
|
|
7
|
+
* a footnote or resolve a citation to its reference. It also emits HTML, not
|
|
8
|
+
* XHTML, and EPUB readers reject documents that are not well-formed XML.
|
|
9
|
+
*
|
|
10
|
+
* The three things the web reader renders as marginal pop-ups (footnotes,
|
|
11
|
+
* citations, definitions) all become ordinary links here, because an e-reader
|
|
12
|
+
* has no margin to put them in.
|
|
13
|
+
*/
|
|
14
|
+
import BlockNode from '../models/chapter/BlockNode';
|
|
15
|
+
import CalloutNode from '../models/chapter/CalloutNode';
|
|
16
|
+
import CitationsNode from '../models/chapter/CitationsNode';
|
|
17
|
+
import CodeNode from '../models/chapter/CodeNode';
|
|
18
|
+
import CommentNode from '../models/chapter/CommentNode';
|
|
19
|
+
import DefinitionNode from '../models/chapter/DefinitionNode';
|
|
20
|
+
import EmbedNode from '../models/chapter/EmbedNode';
|
|
21
|
+
import ErrorNode from '../models/chapter/ErrorNode';
|
|
22
|
+
import FootnoteNode from '../models/chapter/FootnoteNode';
|
|
23
|
+
import FormatNode from '../models/chapter/FormatNode';
|
|
24
|
+
import InlineCodeNode from '../models/chapter/InlineCodeNode';
|
|
25
|
+
import LabelNode from '../models/chapter/LabelNode';
|
|
26
|
+
import LineBreakNode from '../models/chapter/LineBreakNode';
|
|
27
|
+
import LinkNode from '../models/chapter/LinkNode';
|
|
28
|
+
import ListNode from '../models/chapter/ListNode';
|
|
29
|
+
import ParagraphNode from '../models/chapter/ParagraphNode';
|
|
30
|
+
import QuoteNode from '../models/chapter/QuoteNode';
|
|
31
|
+
import RuleNode from '../models/chapter/RuleNode';
|
|
32
|
+
import TableNode from '../models/chapter/TableNode';
|
|
33
|
+
import TextNode from '../models/chapter/TextNode';
|
|
34
|
+
/**
|
|
35
|
+
* Escape text content. Order matters: ampersands first, or we escape our own
|
|
36
|
+
* escapes.
|
|
37
|
+
*
|
|
38
|
+
* The argument is deliberately `unknown`. Book data is JSON that predates the
|
|
39
|
+
* current types and is only loosely validated, so fields the model declares as
|
|
40
|
+
* strings arrive as numbers or null in real books -- reference years in
|
|
41
|
+
* particular. Coercing here keeps one stale field from throwing partway through
|
|
42
|
+
* an export and losing the whole book.
|
|
43
|
+
*/
|
|
44
|
+
export function escapeText(text) {
|
|
45
|
+
return String(text ?? '')
|
|
46
|
+
.replace(/&/g, '&')
|
|
47
|
+
.replace(/</g, '<')
|
|
48
|
+
.replace(/>/g, '>');
|
|
49
|
+
}
|
|
50
|
+
/** Escape an attribute value, which additionally must not close its own quotes. */
|
|
51
|
+
export function escapeAttribute(value) {
|
|
52
|
+
return escapeText(value).replace(/"/g, '"').replace(/'/g, ''');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Serialize one chapter's blocks, returning the body markup and the endnotes
|
|
56
|
+
* that its footnotes produced. Callers place the endnotes wherever they like;
|
|
57
|
+
* `chapterDocument` puts them at the end of the chapter.
|
|
58
|
+
*/
|
|
59
|
+
export function serializeChapter(chapter, context) {
|
|
60
|
+
const endnotes = [];
|
|
61
|
+
const footnotes = chapter.getFootnotes();
|
|
62
|
+
const headers = chapter.getHeaders();
|
|
63
|
+
const body = chapter
|
|
64
|
+
.getBlocks()
|
|
65
|
+
.map((block) => block_(block, { ...context, footnotes, headers, endnotes }))
|
|
66
|
+
.join('\n');
|
|
67
|
+
return { body, endnotes };
|
|
68
|
+
}
|
|
69
|
+
/** Serialize a block-level node. */
|
|
70
|
+
function block_(node, local) {
|
|
71
|
+
if (node instanceof ParagraphNode) {
|
|
72
|
+
const level = node.getLevel();
|
|
73
|
+
const content = inline(node.getFormat(), local);
|
|
74
|
+
if (level === 0)
|
|
75
|
+
return `<p>${content}</p>`;
|
|
76
|
+
// The chapter title is the document's h1, so headers start at h2. The
|
|
77
|
+
// id mirrors Paragraph.svelte so in-book links to headers still land.
|
|
78
|
+
const index = local.headers.indexOf(node);
|
|
79
|
+
const id = index >= 0 ? ` id="header-${index}"` : '';
|
|
80
|
+
const tag = `h${Math.min(level + 1, 6)}`;
|
|
81
|
+
return `<${tag}${id}>${content}</${tag}>`;
|
|
82
|
+
}
|
|
83
|
+
if (node instanceof ListNode)
|
|
84
|
+
return list(node, local);
|
|
85
|
+
if (node instanceof EmbedNode)
|
|
86
|
+
return embed(node, local);
|
|
87
|
+
if (node instanceof CodeNode) {
|
|
88
|
+
const language = node.getLanguage();
|
|
89
|
+
const caption = inline(node.getCaption(), local);
|
|
90
|
+
// No syntax highlighting: e-ink is greyscale, and the spans would only
|
|
91
|
+
// add weight. The language is kept as a class for readers that style it.
|
|
92
|
+
// Newlines become <br/> and leading spaces become non-breaking, so
|
|
93
|
+
// the code keeps its shape on a reader that collapses whitespace
|
|
94
|
+
// rather than honouring `white-space`. The literal newlines are
|
|
95
|
+
// dropped rather than kept alongside the <br/>, which would otherwise
|
|
96
|
+
// double-space the block on a reader that honours both.
|
|
97
|
+
const code = `<pre class="code${language && language !== 'plaintext'
|
|
98
|
+
? ` language-${escapeAttribute(language)}`
|
|
99
|
+
: ''}"><code>${preformatted(node.getCode())}</code></pre>`;
|
|
100
|
+
return caption.trim().length > 0
|
|
101
|
+
? `<figure class="code-figure">${code}<figcaption>${caption}</figcaption></figure>`
|
|
102
|
+
: code;
|
|
103
|
+
}
|
|
104
|
+
if (node instanceof QuoteNode) {
|
|
105
|
+
const credit = inline(node.getCredit(), local);
|
|
106
|
+
const blocks = node
|
|
107
|
+
.getBlocks()
|
|
108
|
+
.map((b) => block_(b, local))
|
|
109
|
+
.join('\n');
|
|
110
|
+
return credit.trim().length > 0
|
|
111
|
+
? `<figure class="quote"><blockquote>${blocks}</blockquote><figcaption><cite>${credit}</cite></figcaption></figure>`
|
|
112
|
+
: `<blockquote>${blocks}</blockquote>`;
|
|
113
|
+
}
|
|
114
|
+
if (node instanceof CalloutNode)
|
|
115
|
+
return `<aside class="callout">${node
|
|
116
|
+
.getBlocks()
|
|
117
|
+
.map((b) => block_(b, local))
|
|
118
|
+
.join('\n')}</aside>`;
|
|
119
|
+
if (node instanceof TableNode)
|
|
120
|
+
return table(node, local);
|
|
121
|
+
if (node instanceof RuleNode)
|
|
122
|
+
return '<hr/>';
|
|
123
|
+
// Editorial comments and parse errors are for authors, not readers.
|
|
124
|
+
if (node instanceof CommentNode || node instanceof ErrorNode)
|
|
125
|
+
return '';
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Escape code for a <pre>, preserving its shape without relying on the reader
|
|
130
|
+
* honouring `white-space`: line breaks become <br/> and each line's leading
|
|
131
|
+
* indentation becomes non-breaking spaces.
|
|
132
|
+
*/
|
|
133
|
+
function preformatted(code) {
|
|
134
|
+
return code
|
|
135
|
+
.split('\n')
|
|
136
|
+
.map((line) => {
|
|
137
|
+
const indent = line.length - line.trimStart().length;
|
|
138
|
+
return '\u00a0'.repeat(indent) + escapeText(line.slice(indent));
|
|
139
|
+
})
|
|
140
|
+
.join('<br/>');
|
|
141
|
+
}
|
|
142
|
+
function list(node, local) {
|
|
143
|
+
const tag = node.isNumbered() ? 'ol' : 'ul';
|
|
144
|
+
const items = node
|
|
145
|
+
.getItems()
|
|
146
|
+
.map((item) => item instanceof ListNode
|
|
147
|
+
? // A nested list belongs inside a list item, not between them.
|
|
148
|
+
`<li>${list(item, local)}</li>`
|
|
149
|
+
: `<li>${inline(item, local)}</li>`)
|
|
150
|
+
.join('');
|
|
151
|
+
return `<${tag}>${items}</${tag}>`;
|
|
152
|
+
}
|
|
153
|
+
function table(node, local) {
|
|
154
|
+
const rows = node.getRows();
|
|
155
|
+
const caption = inline(node.getCaption(), local);
|
|
156
|
+
// Row 0 is the header row, matching how Table.svelte renders it.
|
|
157
|
+
const head = rows.length > 0
|
|
158
|
+
? `<thead><tr>${rows[0]
|
|
159
|
+
.map((cell) => `<th>${inline(cell, local)}</th>`)
|
|
160
|
+
.join('')}</tr></thead>`
|
|
161
|
+
: '';
|
|
162
|
+
const body = rows
|
|
163
|
+
.slice(1)
|
|
164
|
+
.map((row) => `<tr>${row
|
|
165
|
+
.map((cell) => `<td>${inline(cell, local)}</td>`)
|
|
166
|
+
.join('')}</tr>`)
|
|
167
|
+
.join('');
|
|
168
|
+
return `<table>${caption.trim().length > 0 ? `<caption><p>${caption}</p></caption>` : ''}${head}<tbody>${body}</tbody></table>`;
|
|
169
|
+
}
|
|
170
|
+
function embed(node, local) {
|
|
171
|
+
const caption = inline(node.getCaption(), local);
|
|
172
|
+
const credit = inline(node.getCredit(), local);
|
|
173
|
+
const description = escapeAttribute(node.getDescription());
|
|
174
|
+
// The caption and credit are paragraphs rather than a bare string and a
|
|
175
|
+
// span. Simple readers drop tags they don't know but keep their contents,
|
|
176
|
+
// so text placed directly inside a <figcaption> runs into whatever follows
|
|
177
|
+
// it; a <p> is understood almost everywhere and breaks the line.
|
|
178
|
+
const figcaption = caption.trim().length > 0 || credit.trim().length > 0
|
|
179
|
+
? `<figcaption>${caption.trim().length > 0
|
|
180
|
+
? `<p class="caption">${caption}</p>`
|
|
181
|
+
: ''}${credit.trim().length > 0
|
|
182
|
+
? `<p class="credit">${credit}</p>`
|
|
183
|
+
: ''}</figcaption>`
|
|
184
|
+
: '';
|
|
185
|
+
// Video embeds are iframes on the web, which EPUB has no equivalent for, so
|
|
186
|
+
// offer the link instead of silently dropping the content.
|
|
187
|
+
if (node.isVideo()) {
|
|
188
|
+
const url = escapeAttribute(safeURL(node.getURL()));
|
|
189
|
+
// The caption belongs to the figure, so the link is labelled with the
|
|
190
|
+
// description instead; using the caption for both printed it twice.
|
|
191
|
+
const label = node.getDescription().trim().length > 0
|
|
192
|
+
? escapeText(node.getDescription())
|
|
193
|
+
: url;
|
|
194
|
+
return `<figure class="video"><p><a href="${url}">${label}</a></p>${figcaption}</figure>`;
|
|
195
|
+
}
|
|
196
|
+
const path = local.images.get(node.getURL());
|
|
197
|
+
// An image that could not be fetched or decoded keeps its caption and
|
|
198
|
+
// description, so the reader still knows what was meant to be here.
|
|
199
|
+
if (path === undefined)
|
|
200
|
+
return figcaption.length > 0
|
|
201
|
+
? `<figure class="missing-image">${figcaption}</figure>`
|
|
202
|
+
: description.length > 0
|
|
203
|
+
? `<figure class="missing-image"><figcaption>${escapeText(node.getDescription())}</figcaption></figure>`
|
|
204
|
+
: '';
|
|
205
|
+
return `<figure><img src="${escapeAttribute(path)}" alt="${description}"/>${figcaption}</figure>`;
|
|
206
|
+
}
|
|
207
|
+
/** Serialize an inline node and its descendants. */
|
|
208
|
+
function inline(node, local) {
|
|
209
|
+
if (node instanceof TextNode)
|
|
210
|
+
return escapeText(node.getText());
|
|
211
|
+
if (node instanceof FormatNode) {
|
|
212
|
+
const content = node
|
|
213
|
+
.getSegments()
|
|
214
|
+
.map((segment) => inline(segment, local))
|
|
215
|
+
.join('');
|
|
216
|
+
const format = node.getFormat();
|
|
217
|
+
return format === '*'
|
|
218
|
+
? `<strong>${content}</strong>`
|
|
219
|
+
: format === '_'
|
|
220
|
+
? `<em>${content}</em>`
|
|
221
|
+
: format === '^'
|
|
222
|
+
? `<sup>${content}</sup>`
|
|
223
|
+
: format === 'v'
|
|
224
|
+
? `<sub>${content}</sub>`
|
|
225
|
+
: content;
|
|
226
|
+
}
|
|
227
|
+
if (node instanceof LineBreakNode)
|
|
228
|
+
return '<br/>';
|
|
229
|
+
if (node instanceof InlineCodeNode)
|
|
230
|
+
return `<code>${escapeText(node.getText().getText())}</code>`;
|
|
231
|
+
if (node instanceof LinkNode)
|
|
232
|
+
return link(node, local);
|
|
233
|
+
if (node instanceof FootnoteNode)
|
|
234
|
+
return footnote(node, local);
|
|
235
|
+
if (node instanceof CitationsNode)
|
|
236
|
+
return citations(node, local);
|
|
237
|
+
if (node instanceof DefinitionNode)
|
|
238
|
+
return definition(node, local);
|
|
239
|
+
if (node instanceof LabelNode)
|
|
240
|
+
// An empty anchor, so links to this label resolve. toHTML() drops these.
|
|
241
|
+
return `<span id="${escapeAttribute(node.getMeta())}"></span>`;
|
|
242
|
+
// Editorial comments never reach readers.
|
|
243
|
+
if (node instanceof CommentNode)
|
|
244
|
+
return '';
|
|
245
|
+
if (node instanceof ErrorNode)
|
|
246
|
+
return '';
|
|
247
|
+
// A block that turned up inline (a nested list item, say).
|
|
248
|
+
if (node instanceof BlockNode)
|
|
249
|
+
return block_(node, local);
|
|
250
|
+
return '';
|
|
251
|
+
}
|
|
252
|
+
/** The document filename for a chapter. */
|
|
253
|
+
export function chapterPath(chapterID) {
|
|
254
|
+
return `${chapterID}.xhtml`;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Clean up a URL taken from book text.
|
|
258
|
+
*
|
|
259
|
+
* Two things come out of real books. Bookdown escapes punctuation with a
|
|
260
|
+
* backslash and authors write those inside URLs ("medium\\.com"), which the
|
|
261
|
+
* parser keeps verbatim. And an unclosed link swallows whatever follows it,
|
|
262
|
+
* so the address ends up containing spaces and brackets. Neither is a valid
|
|
263
|
+
* URL, and epubcheck rejects both -- which would make the whole book invalid
|
|
264
|
+
* over one typo, so the characters are encoded rather than passed through.
|
|
265
|
+
*/
|
|
266
|
+
function safeURL(url) {
|
|
267
|
+
return url
|
|
268
|
+
.replace(/\\(.)/g, '$1')
|
|
269
|
+
.replace(/[ "<>`{}|\\^\[\]]/g, (character) => encodeURIComponent(character));
|
|
270
|
+
}
|
|
271
|
+
function link(node, local) {
|
|
272
|
+
const url = safeURL(node.getMeta());
|
|
273
|
+
const content = escapeText(node.getText().getText());
|
|
274
|
+
if (url.startsWith('http'))
|
|
275
|
+
return `<a href="${escapeAttribute(url)}">${content}</a>`;
|
|
276
|
+
// Bookdown addresses another chapter as `chapter`, `chapter:label`, or a
|
|
277
|
+
// label in this chapter as `:label`.
|
|
278
|
+
const separator = url.includes(':') ? ':' : url.includes('#') ? '#' : ':';
|
|
279
|
+
const [chapter, label] = url.split(separator);
|
|
280
|
+
if (chapter.length === 0)
|
|
281
|
+
return label === undefined
|
|
282
|
+
? content
|
|
283
|
+
: `<a href="#${escapeAttribute(label)}">${content}</a>`;
|
|
284
|
+
// A link to a chapter that isn't in the package (forthcoming, or one of the
|
|
285
|
+
// interactive pages we omit) would be dead, so keep the words and drop the link.
|
|
286
|
+
if (!local.chapters.has(chapter))
|
|
287
|
+
return content;
|
|
288
|
+
const href = label === undefined || label.length === 0
|
|
289
|
+
? chapterPath(chapter)
|
|
290
|
+
: `${chapterPath(chapter)}#${escapeAttribute(label)}`;
|
|
291
|
+
return `<a href="${escapeAttribute(href)}">${content}</a>`;
|
|
292
|
+
}
|
|
293
|
+
function footnote(node, local) {
|
|
294
|
+
const index = local.footnotes.indexOf(node);
|
|
295
|
+
if (index < 0)
|
|
296
|
+
return '';
|
|
297
|
+
const symbol = local.edition.getFootnoteSymbol(index);
|
|
298
|
+
const id = `fn-${index}`;
|
|
299
|
+
local.endnotes.push({
|
|
300
|
+
id,
|
|
301
|
+
symbol,
|
|
302
|
+
content: inline(node.getMeta(), local),
|
|
303
|
+
});
|
|
304
|
+
return `<a class="noteref" epub:type="noteref" href="#${id}" id="fnref-${index}"><sup>${escapeText(symbol)}</sup></a>`;
|
|
305
|
+
}
|
|
306
|
+
function citations(node, local) {
|
|
307
|
+
const ids = node.getMeta();
|
|
308
|
+
if (ids.length === 0)
|
|
309
|
+
return '';
|
|
310
|
+
const links = ids
|
|
311
|
+
.map((id) => {
|
|
312
|
+
const reference = local.edition.getReference(id);
|
|
313
|
+
const label = escapeText(id);
|
|
314
|
+
// An unknown citation has no reference entry to link to.
|
|
315
|
+
return reference === undefined
|
|
316
|
+
? label
|
|
317
|
+
: `<a href="references.xhtml#ref-${escapeAttribute(id)}">${label}</a>`;
|
|
318
|
+
})
|
|
319
|
+
.join(', ');
|
|
320
|
+
return `<sup class="citation">${links}</sup>`;
|
|
321
|
+
}
|
|
322
|
+
function definition(node, local) {
|
|
323
|
+
const id = node.getMeta();
|
|
324
|
+
const phrase = escapeText(node.getText().getText());
|
|
325
|
+
return local.edition.getGlossary()[id] === undefined
|
|
326
|
+
? phrase
|
|
327
|
+
: `<a class="definition" href="glossary.xhtml#gloss-${escapeAttribute(id)}">${phrase}</a>`;
|
|
328
|
+
}
|
|
329
|
+
/** Serialize a standalone format node, for titles, captions and definitions. */
|
|
330
|
+
export function serializeFormat(node, context) {
|
|
331
|
+
return inline(node, {
|
|
332
|
+
...context,
|
|
333
|
+
footnotes: [],
|
|
334
|
+
headers: [],
|
|
335
|
+
endnotes: [],
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
/** Render gathered footnotes as an endnote section with links back to the text. */
|
|
339
|
+
export function endnoteSection(endnotes) {
|
|
340
|
+
if (endnotes.length === 0)
|
|
341
|
+
return '';
|
|
342
|
+
return `<section class="notes" epub:type="footnotes">
|
|
343
|
+
<h2>Notes</h2>
|
|
344
|
+
${endnotes
|
|
345
|
+
.map((note, index) => `<aside class="note" epub:type="footnote" id="${note.id}"><p><a href="#fnref-${index}"><sup>${escapeText(note.symbol)}</sup></a> ${note.content}</p></aside>`)
|
|
346
|
+
.join('\n')}
|
|
347
|
+
</section>`;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Wrap body markup in a complete XHTML document. The epub namespace is declared
|
|
351
|
+
* on every document because footnote markup uses `epub:type`, and an undeclared
|
|
352
|
+
* prefix makes the XML malformed.
|
|
353
|
+
*/
|
|
354
|
+
export function document_(title, body) {
|
|
355
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
356
|
+
<!DOCTYPE html>
|
|
357
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="en" xml:lang="en">
|
|
358
|
+
<head>
|
|
359
|
+
<meta charset="utf-8"/>
|
|
360
|
+
<title>${escapeText(title)}</title>
|
|
361
|
+
<link rel="stylesheet" type="text/css" href="style.css"/>
|
|
362
|
+
</head>
|
|
363
|
+
<body>
|
|
364
|
+
${body}
|
|
365
|
+
</body>
|
|
366
|
+
</html>`;
|
|
367
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|