bookish-press 0.8.2 → 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 +48 -9
- package/dist/components/chapter/Code.svelte +1 -1
- package/dist/components/editor/EmbedEditor.svelte +1 -1
- package/dist/components/page/DownloadEPUB.svelte +234 -0
- package/dist/components/page/DownloadEPUB.svelte.d.ts +3 -0
- package/dist/components/page/Rows.svelte +1 -1
- package/dist/components/page/Search.svelte +1 -1
- package/dist/components/page/TableOfContents.svelte +45 -30
- 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/book/ChapterID.d.ts +1 -1
- package/dist/models/book/Edition.js +1 -0
- package/dist/models/chapter/TextNode.js +8 -5
- package/dist/models/chapter/TextNode.test.js +8 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -12,6 +12,44 @@ The app is a SvelteKit app. Server-side rendering runs on a [Cloud Run](https://
|
|
|
12
12
|
|
|
13
13
|
One-time setup on a new machine: install the [gcloud CLI](https://cloud.google.com/sdk/docs/install) and run `gcloud auth login`. (This replaced the experimental Firebase Hosting "web frameworks" integration, which required `firebase experiments:enable webframeworks` and capped the Vite version; the old auto-generated `ssrbookishprod` Cloud Function can be deleted once the Cloud Run deploy is verified.)
|
|
14
14
|
|
|
15
|
+
## The demo book
|
|
16
|
+
|
|
17
|
+
`npm run demo` seeds a book that uses every feature Bookish has into a set of
|
|
18
|
+
dedicated emulators, then serves it at <http://localhost:5173/demo>. Nothing is
|
|
19
|
+
uploaded and no sign-in is needed to read it; sign in as `demo@example.com` at
|
|
20
|
+
`/login` to edit it at `/write/demo/1`.
|
|
21
|
+
|
|
22
|
+
It exists so a change to the reading experience can be checked against
|
|
23
|
+
something real without hunting for a book that happens to exercise it. The
|
|
24
|
+
chapters cover every Bookdown construct — formatting, links of each kind,
|
|
25
|
+
footnotes, citations, definitions, lists, tables, quotes, callouts, code,
|
|
26
|
+
symbols, labels, comments — and the images are chosen to stress a small screen:
|
|
27
|
+
a photograph with fine texture, a diagram of hairlines, a stepped grey wedge,
|
|
28
|
+
extreme aspect ratios, transparency, and an SVG. That makes it the fastest way
|
|
29
|
+
to see what an EPUB export actually looks like on an e-reader.
|
|
30
|
+
|
|
31
|
+
The content lives in `scripts/demo-book.js` and the images are drawn by
|
|
32
|
+
`scripts/demo-images.js` rather than committed. Chapters must not use the ids
|
|
33
|
+
the reader reserves for its built-in pages (`references`, `search`, `media`,
|
|
34
|
+
`index`, `glossary`, `unknown`, `cover`); the seed script refuses to run if one
|
|
35
|
+
does.
|
|
36
|
+
|
|
37
|
+
It uses the offset emulator ports from `firebase.test.json`, so it does not
|
|
38
|
+
collide with another project's emulators on the default ports, and it builds
|
|
39
|
+
the app rather than running `vite dev` — the reader route's server render pulls
|
|
40
|
+
the chapter model through a module cycle that Vite's dev SSR evaluator cannot
|
|
41
|
+
order, though the production build resolves it fine.
|
|
42
|
+
|
|
43
|
+
## The demo book
|
|
44
|
+
|
|
45
|
+
`npm run demo` seeds a book that uses every feature Bookish has into a set of dedicated emulators, then serves it at <http://localhost:5173/demo>. Nothing is uploaded and no sign-in is needed to read it; sign in as `demo@example.com` at `/login` to edit it at `/write/demo/1`.
|
|
46
|
+
|
|
47
|
+
It exists so a change to the reading experience can be checked against something real without hunting for a book that happens to exercise it. The chapters cover every Bookdown construct — formatting, links of each kind, footnotes, citations, definitions, lists, tables, quotes, callouts, code, symbols, labels, comments — and the images are chosen to stress a small screen: a photograph with fine texture, a diagram of hairlines, a stepped grey wedge, extreme aspect ratios, transparency, and an SVG. That makes it the fastest way to see what an EPUB export actually looks like on an e-reader.
|
|
48
|
+
|
|
49
|
+
The content lives in `scripts/demo-book.js` and the images are drawn by `scripts/demo-images.js` rather than committed. Chapters must not use the ids the reader reserves for its built-in pages (`references`, `search`, `media`, `index`, `glossary`, `unknown`, `cover`); the seed script refuses to run if one does.
|
|
50
|
+
|
|
51
|
+
It uses the offset emulator ports from `firebase.test.json`, so it doesn't collide with another project's emulators on the default ports, and it builds the app rather than running `vite dev` — the reader route's server render pulls the chapter model through a module cycle that Vite's dev SSR evaluator can't order, though the production build resolves it fine.
|
|
52
|
+
|
|
15
53
|
## Deployment notes
|
|
16
54
|
|
|
17
55
|
There are two major components to Bookish: the authoring platform and the reading front end. Changes to the authoring platform that do not affect reading can be verified and deployed to Firebase without any other coordination. However, changes to the reading experience have downstream dependencies that need to be managed. Here's the general deployment workflow, which I'm currently using as reminders for myself:
|
|
@@ -19,14 +57,15 @@ There are two major components to Bookish: the authoring platform and the readin
|
|
|
19
57
|
0. Ensure you're working in the `dev` branch
|
|
20
58
|
1. Fix bugs, add features
|
|
21
59
|
2. Ensure there are no type errors (`npm run check`) or failed tests (`npm run test`).
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
60
|
+
3. Test with emulator (`npm run emu`), if possible. (Some features aren't possible to perfectly emulate, such as authentication, hosting configurations, permissions.)
|
|
61
|
+
4. Test on the [staging server](https://bookish-dev-21ac3.web.app/`) (`npm run stage`).
|
|
62
|
+
5. Bump the version in `package.json`
|
|
63
|
+
6. Describe the changes in `CHANGELOG.md`, linking to any issues closed
|
|
64
|
+
7. Commit changes to `dev`, referring to any issues addressed
|
|
65
|
+
8. Release to production (`npm run release`)
|
|
66
|
+
9. Verify the change in production on [bookish.press](https://bookish.press)
|
|
67
|
+
10. Merge dev to main (`npm run merge`)
|
|
68
|
+
|
|
30
69
|
> **Svelte 5 note**: as of 0.8.0, `bookish-press` requires Svelte 5 (declared as a peer dependency), which is a breaking change for consumers. Until `bookish-reader` has been migrated to Svelte 5, publish with `npm publish --tag next` so `latest` keeps pointing at the last Svelte 4 compatible release; flip the tag once the reader has migrated. The `0.7.x` branch exists for emergency maintenance releases of the Svelte 4 line.
|
|
31
70
|
|
|
32
71
|
10. If any of the changes affected the reading experience (essentially if any `.svelte` file or its dependencies in `/src/lib/components` changed), then we need to update the reader package for authors who pre-build their books manually:
|
|
@@ -41,4 +80,4 @@ There are two major components to Bookish: the authoring platform and the readin
|
|
|
41
80
|
1. Cloning the reader repo: `git clone https://github.com/amyjko/bookish-reader`
|
|
42
81
|
2. Entering the repo directory: `cd bookish-reader`
|
|
43
82
|
3. Binding the book: `bind.sh`
|
|
44
|
-
4. Deploying the bound book in the newly created `build` folder to wherever it's hosted.
|
|
83
|
+
4. Deploying the bound book in the newly created `build` folder to wherever it's hosted.
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
<script lang="ts">import { onDestroy } from "svelte";
|
|
2
|
+
import { getBase, getEdition } from "./Contexts";
|
|
3
|
+
import { buildEPUB } from "../../epub/build";
|
|
4
|
+
import { DEFAULT_SIZE, SIZES } from "../../epub/images";
|
|
5
|
+
let edition = getEdition();
|
|
6
|
+
let base = getBase();
|
|
7
|
+
let sizeID = $state(DEFAULT_SIZE.id);
|
|
8
|
+
let building = $state(false);
|
|
9
|
+
let progress = $state(void 0);
|
|
10
|
+
let error = $state(void 0);
|
|
11
|
+
let warnings = $state([]);
|
|
12
|
+
let url = $state(void 0);
|
|
13
|
+
let filename = $state("book.epub");
|
|
14
|
+
function release() {
|
|
15
|
+
if (url !== void 0) {
|
|
16
|
+
URL.revokeObjectURL(url);
|
|
17
|
+
url = void 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function chooseSize(id) {
|
|
21
|
+
sizeID = id;
|
|
22
|
+
release();
|
|
23
|
+
warnings = [];
|
|
24
|
+
error = void 0;
|
|
25
|
+
}
|
|
26
|
+
async function build() {
|
|
27
|
+
if ($edition === void 0 || building) return;
|
|
28
|
+
building = true;
|
|
29
|
+
release();
|
|
30
|
+
error = void 0;
|
|
31
|
+
warnings = [];
|
|
32
|
+
progress = { phase: "images", done: 0, total: 0 };
|
|
33
|
+
try {
|
|
34
|
+
const result = await buildEPUB($edition, {
|
|
35
|
+
base: $base ?? "",
|
|
36
|
+
size: SIZES[sizeID] ?? DEFAULT_SIZE,
|
|
37
|
+
onProgress: (update) => progress = update
|
|
38
|
+
});
|
|
39
|
+
url = URL.createObjectURL(result.blob);
|
|
40
|
+
filename = result.filename;
|
|
41
|
+
warnings = result.warnings;
|
|
42
|
+
} catch (problem) {
|
|
43
|
+
console.error(problem);
|
|
44
|
+
error = "Something went wrong building the e-book.";
|
|
45
|
+
} finally {
|
|
46
|
+
building = false;
|
|
47
|
+
progress = void 0;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let status = $derived.by(() => {
|
|
51
|
+
if (progress === void 0) return void 0;
|
|
52
|
+
if (progress.phase === "images")
|
|
53
|
+
return progress.total === 0 ? "Gathering images\u2026" : `Shrinking image ${progress.done} of ${progress.total}\u2026`;
|
|
54
|
+
if (progress.phase === "chapters")
|
|
55
|
+
return `Preparing chapter ${progress.done} of ${progress.total}\u2026`;
|
|
56
|
+
return "Packaging the book\u2026";
|
|
57
|
+
});
|
|
58
|
+
onDestroy(release);
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
{#if $edition}
|
|
62
|
+
<div class="epub">
|
|
63
|
+
<label>
|
|
64
|
+
Image size
|
|
65
|
+
<!-- The wrapper carries the disclosure arrow: turning off native
|
|
66
|
+
appearance to get the book's font also removes the browser's. -->
|
|
67
|
+
<span class="select">
|
|
68
|
+
<select
|
|
69
|
+
value={sizeID}
|
|
70
|
+
disabled={building}
|
|
71
|
+
onchange={(event) => chooseSize(event.currentTarget.value)}
|
|
72
|
+
>
|
|
73
|
+
{#each Object.values(SIZES) as size}
|
|
74
|
+
<option value={size.id}>{size.label}</option>
|
|
75
|
+
{/each}
|
|
76
|
+
</select>
|
|
77
|
+
</span>
|
|
78
|
+
</label>
|
|
79
|
+
<!-- A plain button rather than the app's, whose --app-* variables are
|
|
80
|
+
only defined in the (app) layout, so it renders unstyled here and
|
|
81
|
+
in books compiled by bookish-reader. -->
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
title="Build an EPUB of this book to read on an e-reader"
|
|
85
|
+
aria-label="Build an EPUB of this book to read on an e-reader"
|
|
86
|
+
disabled={building}
|
|
87
|
+
onclick={build}>↓ EPUB</button
|
|
88
|
+
>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{#if error}
|
|
92
|
+
<p class="message error" role="alert">{error}</p>
|
|
93
|
+
{:else if status}
|
|
94
|
+
<p class="message" role="status" aria-live="polite">{status}</p>
|
|
95
|
+
{:else if url}
|
|
96
|
+
<!-- Rendered only once the blob exists, so this link is never present
|
|
97
|
+
in prerendered HTML for SvelteKit's crawler to follow. -->
|
|
98
|
+
<p class="ready">
|
|
99
|
+
<a href={url} download={filename}>Download {filename}</a>
|
|
100
|
+
</p>
|
|
101
|
+
{#if warnings.length > 0}
|
|
102
|
+
<p class="warnings">
|
|
103
|
+
{warnings.length}
|
|
104
|
+
{warnings.length === 1 ? 'image' : 'images'} couldn't be included,
|
|
105
|
+
usually because they're hosted somewhere that doesn't allow downloads.
|
|
106
|
+
Their captions are still in the book.
|
|
107
|
+
</p>
|
|
108
|
+
{/if}
|
|
109
|
+
{/if}
|
|
110
|
+
{/if}
|
|
111
|
+
|
|
112
|
+
<style>
|
|
113
|
+
/* Everything here is drawn with the book's own --bookish-* variables, which
|
|
114
|
+
are defined on the .bookish ancestor in Edition.svelte and so are in
|
|
115
|
+
scope in both the app and in books compiled by bookish-reader. The
|
|
116
|
+
--app-* chrome variables are not: they exist only in the (app) layout. */
|
|
117
|
+
|
|
118
|
+
.epub {
|
|
119
|
+
display: flex;
|
|
120
|
+
flex-direction: row;
|
|
121
|
+
flex-wrap: wrap;
|
|
122
|
+
align-items: center;
|
|
123
|
+
gap: 0.5em;
|
|
124
|
+
margin-block: var(--bookish-paragraph-spacing);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
label {
|
|
128
|
+
display: inline-flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
gap: 0.5em;
|
|
131
|
+
font-family: var(--bookish-paragraph-font-family);
|
|
132
|
+
font-size: var(--bookish-small-font-size);
|
|
133
|
+
color: var(--bookish-muted-color);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.select {
|
|
137
|
+
position: relative;
|
|
138
|
+
display: inline-block;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* The disclosure arrow, in place of the native one. It inherits the text
|
|
142
|
+
color, so it follows the theme into dark mode. */
|
|
143
|
+
.select::after {
|
|
144
|
+
content: '▾';
|
|
145
|
+
position: absolute;
|
|
146
|
+
right: 0.6em;
|
|
147
|
+
top: 50%;
|
|
148
|
+
transform: translateY(-50%);
|
|
149
|
+
pointer-events: none;
|
|
150
|
+
font-size: 0.9em;
|
|
151
|
+
color: var(--bookish-paragraph-color);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* A <select> is a native menulist, and WebKit renders its face in the
|
|
155
|
+
system font whatever the cascade says until appearance is turned off. */
|
|
156
|
+
select {
|
|
157
|
+
appearance: none;
|
|
158
|
+
-webkit-appearance: none;
|
|
159
|
+
font-family: var(--bookish-paragraph-font-family);
|
|
160
|
+
font-size: var(--bookish-small-font-size);
|
|
161
|
+
font-weight: var(--bookish-paragraph-font-weight);
|
|
162
|
+
color: var(--bookish-paragraph-color);
|
|
163
|
+
background: var(--bookish-block-background-color);
|
|
164
|
+
border: 1px solid var(--bookish-border-color-light);
|
|
165
|
+
border-radius: var(--bookish-roundedness);
|
|
166
|
+
padding: var(--bookish-inline-padding);
|
|
167
|
+
padding-right: 2em;
|
|
168
|
+
max-width: 18em;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
button {
|
|
172
|
+
font-family: var(--bookish-paragraph-font-family);
|
|
173
|
+
font-size: var(--bookish-small-font-size);
|
|
174
|
+
font-weight: var(--bookish-bold-font-weight);
|
|
175
|
+
color: var(--bookish-link-color);
|
|
176
|
+
background: var(--bookish-block-background-color);
|
|
177
|
+
border: 1px solid var(--bookish-border-color-light);
|
|
178
|
+
border-radius: var(--bookish-roundedness);
|
|
179
|
+
padding: var(--bookish-inline-padding)
|
|
180
|
+
calc(var(--bookish-inline-padding) * 3);
|
|
181
|
+
cursor: pointer;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
button:hover:not(:disabled) {
|
|
185
|
+
border-color: var(--bookish-highlight-color);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* focus-visible, so a mouse click doesn't leave the ring behind while
|
|
189
|
+
keyboard users still get one. */
|
|
190
|
+
select:focus-visible,
|
|
191
|
+
button:focus-visible {
|
|
192
|
+
outline: none;
|
|
193
|
+
border-color: var(--bookish-highlight-color);
|
|
194
|
+
box-shadow: 0 0 0 2px var(--bookish-highlight-color);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
select:disabled,
|
|
198
|
+
button:disabled {
|
|
199
|
+
opacity: 0.5;
|
|
200
|
+
cursor: auto;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.message,
|
|
204
|
+
.ready,
|
|
205
|
+
.warnings {
|
|
206
|
+
font-family: var(--bookish-paragraph-font-family);
|
|
207
|
+
line-height: var(--bookish-paragraph-line-height);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.message {
|
|
211
|
+
color: var(--bookish-muted-color);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.error {
|
|
215
|
+
color: var(--bookish-error-color);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* Matches a book link, from components/Link.svelte. */
|
|
219
|
+
.ready a {
|
|
220
|
+
color: var(--bookish-link-color);
|
|
221
|
+
font-weight: var(--bookish-link-font-weight);
|
|
222
|
+
text-decoration: none;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.ready a:hover {
|
|
226
|
+
text-decoration: underline;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.warnings {
|
|
230
|
+
font-size: var(--bookish-small-font-size);
|
|
231
|
+
font-style: italic;
|
|
232
|
+
color: var(--bookish-muted-color);
|
|
233
|
+
}
|
|
234
|
+
</style>
|
|
@@ -139,7 +139,7 @@ let results = $derived.by(() => {
|
|
|
139
139
|
<style>
|
|
140
140
|
.query {
|
|
141
141
|
font-family: var(--bookish-header-font-family);
|
|
142
|
-
font-size: var(--bookish-header-font-size);
|
|
142
|
+
font-size: var(--bookish-header-1-font-size);
|
|
143
143
|
border: none;
|
|
144
144
|
border-bottom: 3px solid var(--bookish-border-color-light);
|
|
145
145
|
padding: var(--bookish-inline-padding);
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "./Contexts";
|
|
25
25
|
import Muted from "./Muted.svelte";
|
|
26
26
|
import Button from "../app/Button.svelte";
|
|
27
|
+
import DownloadEPUB from "./DownloadEPUB.svelte";
|
|
27
28
|
import PageHeader from "./PageHeader.svelte";
|
|
28
29
|
import Rows from "./Rows.svelte";
|
|
29
30
|
import PageParagraph from "./PageParagraph.svelte";
|
|
@@ -84,21 +85,18 @@ onMount(() => {
|
|
|
84
85
|
$edition ? edition.set($edition.withTitle(text)) : undefined}
|
|
85
86
|
>
|
|
86
87
|
{#snippet before()}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
{/snippet}
|
|
88
|
+
{#if editable}
|
|
89
|
+
<SubdomainEditor />
|
|
90
|
+
{/if}
|
|
91
|
+
{/snippet}
|
|
93
92
|
{#snippet outline()}
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
<Outline
|
|
96
94
|
previous={null}
|
|
97
95
|
next={$edition.getNextChapterID('', editable)}
|
|
98
96
|
/>
|
|
99
|
-
|
|
97
|
+
{/snippet}
|
|
100
98
|
{#snippet after()}
|
|
101
|
-
|
|
99
|
+
<div>
|
|
102
100
|
<Authors
|
|
103
101
|
editable={isBookEditable()}
|
|
104
102
|
authors={$edition.getAuthors()}
|
|
@@ -108,7 +106,9 @@ onMount(() => {
|
|
|
108
106
|
: undefined}
|
|
109
107
|
edit={(index, text) =>
|
|
110
108
|
$edition
|
|
111
|
-
? edition.set(
|
|
109
|
+
? edition.set(
|
|
110
|
+
$edition.withAuthorName(index, text),
|
|
111
|
+
)
|
|
112
112
|
: undefined}
|
|
113
113
|
remove={(index) =>
|
|
114
114
|
$edition
|
|
@@ -118,7 +118,7 @@ onMount(() => {
|
|
|
118
118
|
/>
|
|
119
119
|
<Note>{$edition.getEditionLabel()} edition</Note>
|
|
120
120
|
</div>
|
|
121
|
-
|
|
121
|
+
{/snippet}
|
|
122
122
|
</Header>
|
|
123
123
|
|
|
124
124
|
<Instructions {editable}>
|
|
@@ -130,8 +130,8 @@ onMount(() => {
|
|
|
130
130
|
|
|
131
131
|
<Instructions {editable}>
|
|
132
132
|
This will appear on the <Link to="/read">book browsing</Link>
|
|
133
|
-
page and in your table of contents. Write an informative description
|
|
134
|
-
|
|
133
|
+
page and in your table of contents. Write an informative description of
|
|
134
|
+
what your book is about.
|
|
135
135
|
</Instructions>
|
|
136
136
|
|
|
137
137
|
<Instructions {editable}>
|
|
@@ -183,7 +183,7 @@ onMount(() => {
|
|
|
183
183
|
forthcoming={chapter.isForthcoming()}
|
|
184
184
|
>
|
|
185
185
|
{#snippet annotation()}
|
|
186
|
-
|
|
186
|
+
<span>
|
|
187
187
|
{#if editable}
|
|
188
188
|
<TextEditor
|
|
189
189
|
label={'Chapter section editor'}
|
|
@@ -202,9 +202,9 @@ onMount(() => {
|
|
|
202
202
|
You can edit this chapter.
|
|
203
203
|
{/if}
|
|
204
204
|
</span>
|
|
205
|
-
|
|
205
|
+
{/snippet}
|
|
206
206
|
{#snippet etc()}
|
|
207
|
-
|
|
207
|
+
<span class="etc">
|
|
208
208
|
<Muted>
|
|
209
209
|
{#if editable}
|
|
210
210
|
<Toggle
|
|
@@ -223,44 +223,44 @@ onMount(() => {
|
|
|
223
223
|
{/if}
|
|
224
224
|
</Muted>
|
|
225
225
|
</span>
|
|
226
|
-
|
|
226
|
+
{/snippet}
|
|
227
227
|
</TableOfContentsRow>
|
|
228
228
|
{/each}
|
|
229
229
|
{#if $edition.hasReferences() || editable}
|
|
230
230
|
<TableOfContentsRow chapter={ChapterIDs.ReferencesID}>
|
|
231
231
|
{#snippet annotation()}
|
|
232
|
-
|
|
233
|
-
|
|
232
|
+
<span>Everything cited</span>
|
|
233
|
+
{/snippet}
|
|
234
234
|
</TableOfContentsRow>
|
|
235
235
|
{/if}
|
|
236
236
|
{#if ($edition.getGlossary() && Object.keys($edition.getGlossary()).length > 0) || editable}
|
|
237
237
|
<TableOfContentsRow chapter={ChapterIDs.GlossaryID}>
|
|
238
238
|
{#snippet annotation()}
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
<span>Definitions</span>
|
|
240
|
+
{/snippet}
|
|
241
241
|
</TableOfContentsRow>
|
|
242
242
|
{/if}
|
|
243
243
|
<TableOfContentsRow chapter={ChapterIDs.IndexID}>
|
|
244
244
|
{#snippet annotation()}
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
<span>Common words and where they are</span>
|
|
246
|
+
{/snippet}
|
|
247
247
|
</TableOfContentsRow>
|
|
248
248
|
<TableOfContentsRow chapter={ChapterIDs.SearchID}>
|
|
249
249
|
{#snippet annotation()}
|
|
250
|
-
|
|
251
|
-
|
|
250
|
+
<span>Find where words occur</span>
|
|
251
|
+
{/snippet}
|
|
252
252
|
</TableOfContentsRow>
|
|
253
253
|
|
|
254
254
|
<TableOfContentsRow chapter={ChapterIDs.MediaID}>
|
|
255
255
|
{#snippet annotation()}
|
|
256
|
-
|
|
257
|
-
|
|
256
|
+
<span>Images and video in the book</span>
|
|
257
|
+
{/snippet}
|
|
258
258
|
</TableOfContentsRow>
|
|
259
259
|
{#if editable}
|
|
260
260
|
<TableOfContentsRow chapter={ChapterIDs.UnknownID}>
|
|
261
261
|
{#snippet annotation()}
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
<span>Customize bad links</span>
|
|
263
|
+
{/snippet}
|
|
264
264
|
</TableOfContentsRow>
|
|
265
265
|
{/if}
|
|
266
266
|
</Rows>
|
|
@@ -281,6 +281,21 @@ onMount(() => {
|
|
|
281
281
|
> and then print or export. Long books can take some time to render.
|
|
282
282
|
</PageParagraph>
|
|
283
283
|
|
|
284
|
+
<EditableHeader id="epub" label="Edit e-book header" />
|
|
285
|
+
|
|
286
|
+
<Instructions {editable}>
|
|
287
|
+
This builds an EPUB in the reader's browser. Nothing is uploaded and
|
|
288
|
+
no server is involved.
|
|
289
|
+
</Instructions>
|
|
290
|
+
|
|
291
|
+
<PageParagraph>
|
|
292
|
+
Reading on an e-reader? Build an EPUB of this book. Images are
|
|
293
|
+
shrunk to fit the size you choose, so pick the one closest to your
|
|
294
|
+
device. Long books with many images can take a minute.
|
|
295
|
+
</PageParagraph>
|
|
296
|
+
|
|
297
|
+
<DownloadEPUB />
|
|
298
|
+
|
|
284
299
|
<EditableHeader id="citation" label="Edit citation header" />
|
|
285
300
|
|
|
286
301
|
<Instructions {editable}>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a complete EPUB from a loaded edition, entirely in the browser.
|
|
3
|
+
*
|
|
4
|
+
* No server is involved: the whole book's text is already in memory by the time
|
|
5
|
+
* the table of contents renders, and images are fetched directly (Firebase's
|
|
6
|
+
* download URLs allow cross-origin reads, and compiled books serve theirs from
|
|
7
|
+
* the same origin).
|
|
8
|
+
*/
|
|
9
|
+
import type Edition from '../models/book/Edition';
|
|
10
|
+
import { type ImageSize } from './images';
|
|
11
|
+
import { chapterPath } from './xhtml';
|
|
12
|
+
export type BuildPhase = 'images' | 'chapters' | 'packaging';
|
|
13
|
+
export type BuildProgress = {
|
|
14
|
+
phase: BuildPhase;
|
|
15
|
+
done: number;
|
|
16
|
+
total: number;
|
|
17
|
+
};
|
|
18
|
+
export type BuildResult = {
|
|
19
|
+
blob: Blob;
|
|
20
|
+
filename: string;
|
|
21
|
+
/** Images that couldn't be included, so the reader knows what's missing. */
|
|
22
|
+
warnings: string[];
|
|
23
|
+
};
|
|
24
|
+
export type BuildOptions = {
|
|
25
|
+
/** The book's base path, for resolving local image URLs. */
|
|
26
|
+
base?: string;
|
|
27
|
+
size?: ImageSize;
|
|
28
|
+
onProgress?: (progress: BuildProgress) => void;
|
|
29
|
+
/** Fixed timestamp, so tests can compare output. */
|
|
30
|
+
modified?: Date;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* A stable, unique identifier for this edition. EPUB requires one, and readers
|
|
34
|
+
* use it to recognize the same book across downloads.
|
|
35
|
+
*/
|
|
36
|
+
export declare function identifierFor(edition: Edition): string;
|
|
37
|
+
/** Build an EPUB for the given edition. */
|
|
38
|
+
export declare function buildEPUB(edition: Edition, options?: BuildOptions): Promise<BuildResult>;
|
|
39
|
+
export { chapterPath };
|