@bendyline/squisq-formats 1.2.3 → 1.3.0
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/__tests__/html.test.js +78 -4
- package/dist/__tests__/html.test.js.map +1 -1
- package/dist/__tests__/pdfExport.test.js +1 -1
- package/dist/__tests__/pdfExport.test.js.map +1 -1
- package/dist/__tests__/plainHtml.test.d.ts +5 -0
- package/dist/__tests__/plainHtml.test.d.ts.map +1 -0
- package/dist/__tests__/plainHtml.test.js +327 -0
- package/dist/__tests__/plainHtml.test.js.map +1 -0
- package/dist/__tests__/plainHtmlBundle.test.d.ts +11 -0
- package/dist/__tests__/plainHtmlBundle.test.d.ts.map +1 -0
- package/dist/__tests__/plainHtmlBundle.test.js +210 -0
- package/dist/__tests__/plainHtmlBundle.test.js.map +1 -0
- package/dist/chunk-33YRFXZZ.js +1187 -0
- package/dist/chunk-33YRFXZZ.js.map +1 -0
- package/dist/{chunk-EUQQYBZZ.js → chunk-ERZ627GR.js} +4 -4
- package/dist/chunk-ERZ627GR.js.map +1 -0
- package/dist/{chunk-MEZF76JA.js → chunk-UDS45KUJ.js} +68 -25
- package/dist/chunk-UDS45KUJ.js.map +1 -0
- package/dist/{chunk-NGWHV77G.js → chunk-VN2KEOYB.js} +7 -6
- package/dist/chunk-VN2KEOYB.js.map +1 -0
- package/dist/docx/export.d.ts.map +1 -1
- package/dist/docx/export.js +153 -22
- package/dist/docx/export.js.map +1 -1
- package/dist/epub/export.js +3 -3
- package/dist/epub/export.js.map +1 -1
- package/dist/html/docsHtmlBundle.d.ts +53 -0
- package/dist/html/docsHtmlBundle.d.ts.map +1 -0
- package/dist/html/docsHtmlBundle.js +299 -0
- package/dist/html/docsHtmlBundle.js.map +1 -0
- package/dist/html/htmlTemplate.d.ts.map +1 -1
- package/dist/html/htmlTemplate.js +43 -0
- package/dist/html/htmlTemplate.js.map +1 -1
- package/dist/html/index.d.ts +6 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +31 -3
- package/dist/html/index.js.map +1 -1
- package/dist/html/plainHtml.d.ts +79 -0
- package/dist/html/plainHtml.d.ts.map +1 -0
- package/dist/html/plainHtml.js +614 -0
- package/dist/html/plainHtml.js.map +1 -0
- package/dist/html/plainHtmlBundle.d.ts +93 -0
- package/dist/html/plainHtmlBundle.d.ts.map +1 -0
- package/dist/html/plainHtmlBundle.js +357 -0
- package/dist/html/plainHtmlBundle.js.map +1 -0
- package/dist/pptx/export.d.ts.map +1 -1
- package/dist/pptx/export.js +10 -8
- package/dist/pptx/export.js.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/html.test.ts +82 -4
- package/src/__tests__/pdfExport.test.ts +1 -1
- package/src/__tests__/plainHtml.test.ts +372 -0
- package/src/__tests__/plainHtmlBundle.test.ts +235 -0
- package/src/docx/export.ts +163 -22
- package/src/epub/export.ts +3 -3
- package/src/html/docsHtmlBundle.ts +369 -0
- package/src/html/htmlTemplate.ts +40 -0
- package/src/html/index.ts +32 -3
- package/src/html/plainHtml.ts +736 -0
- package/src/html/plainHtmlBundle.ts +419 -0
- package/src/pptx/export.ts +10 -9
- package/dist/chunk-EUQQYBZZ.js.map +0 -1
- package/dist/chunk-MEZF76JA.js.map +0 -1
- package/dist/chunk-NGWHV77G.js.map +0 -1
- package/dist/chunk-UM5V2XZG.js +0 -242
- package/dist/chunk-UM5V2XZG.js.map +0 -1
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursive plain-HTML bundle export.
|
|
3
|
+
*
|
|
4
|
+
* `markdownDocsToPlainHtmlBundle` starts from a single entry markdown
|
|
5
|
+
* file, walks its relative `[…](other.md)` links, recursively pulls in
|
|
6
|
+
* any sibling/child documents (scope-limited to the entry doc's
|
|
7
|
+
* directory tree), renders every visited file via
|
|
8
|
+
* `markdownDocToPlainHtml`, rewrites cross-doc references from `.md`
|
|
9
|
+
* to `.html`, and ships everything as a single ZIP.
|
|
10
|
+
*
|
|
11
|
+
* The function is provider-agnostic — callers pass `readDocument` and
|
|
12
|
+
* `readBinary` callbacks that resolve relative paths against whatever
|
|
13
|
+
* storage they use (`FileSystemContentContainer`, `MemoryContent-
|
|
14
|
+
* Container`, in-memory map for tests, …). Failure to read any
|
|
15
|
+
* discovered file aborts the whole export with a thrown error.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import JSZip from 'jszip';
|
|
19
|
+
import { parseMarkdown, inferDocumentTitle } from '@bendyline/squisq/markdown';
|
|
20
|
+
import type { MarkdownDocument, HtmlNode } from '@bendyline/squisq/markdown';
|
|
21
|
+
import type { Theme } from '@bendyline/squisq/schemas';
|
|
22
|
+
import { resolveTheme } from '@bendyline/squisq/schemas';
|
|
23
|
+
import { markdownDocToPlainHtml } from './plainHtml.js';
|
|
24
|
+
|
|
25
|
+
// ── Public Types ───────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export interface PlainHtmlBundleOptions {
|
|
28
|
+
/** Entry document path relative to the container root (e.g. `'home.md'`). */
|
|
29
|
+
entryPath: string;
|
|
30
|
+
/** Reads a UTF-8 markdown file from the container. Returns null when absent. */
|
|
31
|
+
readDocument: (path: string) => Promise<string | null>;
|
|
32
|
+
/** Reads a binary asset (image) from the container. Returns null when absent. */
|
|
33
|
+
readBinary: (path: string) => Promise<ArrayBuffer | null>;
|
|
34
|
+
/** Optional document title for the entry. Others derive from filename. */
|
|
35
|
+
title?: string;
|
|
36
|
+
/** Optional theme applied uniformly to every page. Overrides {@link themeId}. */
|
|
37
|
+
theme?: Theme;
|
|
38
|
+
/**
|
|
39
|
+
* Optional theme id (e.g. `'warm-earth'`, `'gezellig'`). Resolved via
|
|
40
|
+
* `resolveTheme` and applied to every page. Convenient for callers
|
|
41
|
+
* that track themes by id (like the host export dialog) without
|
|
42
|
+
* having to resolve to a `Theme` object first. When both `theme` and
|
|
43
|
+
* `themeId` are supplied, `theme` wins.
|
|
44
|
+
*/
|
|
45
|
+
themeId?: string;
|
|
46
|
+
/** Maximum recursion depth (default: unlimited; cycles always handled). */
|
|
47
|
+
maxDepth?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Emit the entry doc as `index.html` (preserving its parent directory)
|
|
50
|
+
* instead of `<basename>.html`. Cross-doc links pointing at the entry
|
|
51
|
+
* also rewrite to `index.html`, so a sibling `resume.md → home.md`
|
|
52
|
+
* link doesn't 404 after the rename. Convenient for static-site
|
|
53
|
+
* deploys where the landing page must be named `index.html`.
|
|
54
|
+
* Default: false.
|
|
55
|
+
*/
|
|
56
|
+
entryAsIndex?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Public API ─────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Render an entry markdown document and every reachable sibling/child
|
|
63
|
+
* `.md` document it links to, bundled as a single ZIP with plain-HTML
|
|
64
|
+
* pages, per-document asset folders, and cross-doc `<a href>`
|
|
65
|
+
* references rewritten from `.md` to `.html`.
|
|
66
|
+
*/
|
|
67
|
+
export async function markdownDocsToPlainHtmlBundle(
|
|
68
|
+
options: PlainHtmlBundleOptions,
|
|
69
|
+
): Promise<Blob> {
|
|
70
|
+
const {
|
|
71
|
+
entryPath,
|
|
72
|
+
readDocument,
|
|
73
|
+
readBinary,
|
|
74
|
+
title,
|
|
75
|
+
theme,
|
|
76
|
+
themeId,
|
|
77
|
+
maxDepth = Infinity,
|
|
78
|
+
entryAsIndex = false,
|
|
79
|
+
} = options;
|
|
80
|
+
const resolvedTheme = theme ?? (themeId ? resolveTheme(themeId) : undefined);
|
|
81
|
+
const entry = normalizePath(entryPath);
|
|
82
|
+
if (!entry) {
|
|
83
|
+
throw new Error('markdownDocsToPlainHtmlBundle: entryPath is required');
|
|
84
|
+
}
|
|
85
|
+
const scopeRoot = posixDirname(entry); // '' for root-level files
|
|
86
|
+
// When `entryAsIndex`, the entry doc writes to `<entryDir>/index.html`
|
|
87
|
+
// and every cross-doc link pointing at it rewrites to that path too.
|
|
88
|
+
// Other docs keep the `<basename>.html` convention.
|
|
89
|
+
const entryHtmlPath = entryAsIndex
|
|
90
|
+
? scopeRoot
|
|
91
|
+
? `${scopeRoot}/index.html`
|
|
92
|
+
: 'index.html'
|
|
93
|
+
: entry.slice(0, -3) + '.html';
|
|
94
|
+
const htmlPathFor = (mdPath: string): string =>
|
|
95
|
+
mdPath === entry ? entryHtmlPath : mdPath.slice(0, -3) + '.html';
|
|
96
|
+
|
|
97
|
+
const zip = new JSZip();
|
|
98
|
+
const visited = new Set<string>();
|
|
99
|
+
const queue: Array<{ path: string; depth: number }> = [{ path: entry, depth: 0 }];
|
|
100
|
+
|
|
101
|
+
while (queue.length > 0) {
|
|
102
|
+
const { path, depth } = queue.shift()!;
|
|
103
|
+
if (visited.has(path)) continue;
|
|
104
|
+
visited.add(path);
|
|
105
|
+
|
|
106
|
+
const source = await readDocument(path);
|
|
107
|
+
if (source === null) {
|
|
108
|
+
throw new Error(`markdownDocsToPlainHtmlBundle: failed to read "${path}"`);
|
|
109
|
+
}
|
|
110
|
+
const mdDoc = parseMarkdown(source);
|
|
111
|
+
|
|
112
|
+
// Discover this doc's relative .md links — both for enqueueing new
|
|
113
|
+
// targets and for building the per-doc link rewrite map. We resolve
|
|
114
|
+
// each link to a canonical container-relative path so the same doc
|
|
115
|
+
// referenced two different ways (./resume.md vs resume.md) is
|
|
116
|
+
// visited once and rewritten consistently.
|
|
117
|
+
const docDir = posixDirname(path);
|
|
118
|
+
const linkMap = new Map<string, string>();
|
|
119
|
+
for (const raw of collectLinkRefs(mdDoc)) {
|
|
120
|
+
const parsed = parseLinkRef(raw);
|
|
121
|
+
if (!parsed) continue;
|
|
122
|
+
const resolved = resolveRelative(docDir, parsed.path);
|
|
123
|
+
if (resolved === null) continue; // escaped via ..
|
|
124
|
+
if (!isInScope(resolved, scopeRoot)) continue;
|
|
125
|
+
if (!resolved.toLowerCase().endsWith('.md')) continue;
|
|
126
|
+
|
|
127
|
+
// Compute the .html replacement relative to the current doc so
|
|
128
|
+
// the rewritten href stays local (e.g. `subdir/notes.html`, not
|
|
129
|
+
// an absolute container path). `htmlPathFor` also handles the
|
|
130
|
+
// entry-as-index case so a sibling linking to the entry lands
|
|
131
|
+
// on `index.html` after rename.
|
|
132
|
+
const htmlTarget = htmlPathFor(resolved);
|
|
133
|
+
const relHref = relativeFrom(docDir, htmlTarget) + parsed.fragment;
|
|
134
|
+
linkMap.set(raw, relHref);
|
|
135
|
+
|
|
136
|
+
if (depth + 1 <= maxDepth && !visited.has(resolved)) {
|
|
137
|
+
queue.push({ path: resolved, depth: depth + 1 });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Walk images, fetch each one, write at the original relative path
|
|
142
|
+
// resolved against the container root (so `resume_files/hero.png`
|
|
143
|
+
// sits next to `resume.html` in the zip). Failures here are *not*
|
|
144
|
+
// fatal — images can be missing without breaking the document
|
|
145
|
+
// structure — but we surface them as console warnings.
|
|
146
|
+
const images = await readImagesForDoc(mdDoc, docDir, readBinary);
|
|
147
|
+
for (const [, { data, zipPath }] of images) {
|
|
148
|
+
const safe = sanitizeZipPath(zipPath);
|
|
149
|
+
if (!safe) continue;
|
|
150
|
+
zip.file(safe, data);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Render the document. The image rewrite map keys are the URLs
|
|
154
|
+
// exactly as authored (so `markdownDocToPlainHtml` can match them
|
|
155
|
+
// verbatim), and the values are paths relative to the rendered
|
|
156
|
+
// doc's own location — so `<img src>` works after unzip without
|
|
157
|
+
// any post-processing.
|
|
158
|
+
const imageRewriteMap = new Map<string, string>();
|
|
159
|
+
for (const [authored, { zipPath }] of images) {
|
|
160
|
+
imageRewriteMap.set(authored, relativeFrom(docDir, zipPath));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const docTitle = depth === 0 ? title : undefined;
|
|
164
|
+
const html = markdownDocToPlainHtml(mdDoc, {
|
|
165
|
+
title: docTitle ?? titleForFilename(path, mdDoc),
|
|
166
|
+
images: imageRewriteMap,
|
|
167
|
+
links: linkMap,
|
|
168
|
+
theme: resolvedTheme,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const htmlPath = htmlPathFor(path);
|
|
172
|
+
zip.file(htmlPath, html);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return zip.generateAsync({
|
|
176
|
+
type: 'blob',
|
|
177
|
+
compression: 'DEFLATE',
|
|
178
|
+
compressionOptions: { level: 6 },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Link discovery ─────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Collect every `<a>`-style link URL referenced in a document. Markdown
|
|
186
|
+
* `link` nodes plus any raw HTML `<a href>` tags. Returns the raw URLs
|
|
187
|
+
* as authored, so callers can use them as both the linkMap *key* and
|
|
188
|
+
* the basis for resolution.
|
|
189
|
+
*/
|
|
190
|
+
export function collectLinkRefs(doc: MarkdownDocument): Set<string> {
|
|
191
|
+
const refs = new Set<string>();
|
|
192
|
+
|
|
193
|
+
function visitHtml(nodes: HtmlNode[]): void {
|
|
194
|
+
for (const n of nodes) {
|
|
195
|
+
if (n.type !== 'htmlElement') continue;
|
|
196
|
+
if (n.tagName.toLowerCase() === 'a') {
|
|
197
|
+
const href = n.attributes.href;
|
|
198
|
+
if (typeof href === 'string' && href) refs.add(href);
|
|
199
|
+
}
|
|
200
|
+
visitHtml(n.children);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function visit(node: unknown): void {
|
|
205
|
+
if (!node || typeof node !== 'object') return;
|
|
206
|
+
const n = node as Record<string, unknown>;
|
|
207
|
+
if (n.type === 'link' && typeof n.url === 'string' && n.url) {
|
|
208
|
+
refs.add(n.url);
|
|
209
|
+
}
|
|
210
|
+
if ((n.type === 'htmlBlock' || n.type === 'htmlInline') && Array.isArray(n.htmlChildren)) {
|
|
211
|
+
visitHtml(n.htmlChildren as HtmlNode[]);
|
|
212
|
+
}
|
|
213
|
+
if (Array.isArray(n.children)) {
|
|
214
|
+
for (const child of n.children) visit(child);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const child of doc.children) visit(child);
|
|
219
|
+
return refs;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function collectImageRefs(doc: MarkdownDocument): Set<string> {
|
|
223
|
+
const refs = new Set<string>();
|
|
224
|
+
function visitHtml(nodes: HtmlNode[]): void {
|
|
225
|
+
for (const n of nodes) {
|
|
226
|
+
if (n.type !== 'htmlElement') continue;
|
|
227
|
+
const tag = n.tagName.toLowerCase();
|
|
228
|
+
// <img>/<video>/<audio>/<source> all reference media via `src`;
|
|
229
|
+
// we feed them into the same `images` map (effectively a generic
|
|
230
|
+
// media map — see header comment) so the export pipeline rewrites
|
|
231
|
+
// and bundles each one the same way.
|
|
232
|
+
if (tag === 'img' || tag === 'video' || tag === 'audio' || tag === 'source') {
|
|
233
|
+
const src = n.attributes.src;
|
|
234
|
+
if (typeof src === 'string' && src) refs.add(src);
|
|
235
|
+
}
|
|
236
|
+
if (tag === 'video' || tag === 'audio') {
|
|
237
|
+
const poster = n.attributes.poster;
|
|
238
|
+
if (typeof poster === 'string' && poster) refs.add(poster);
|
|
239
|
+
}
|
|
240
|
+
visitHtml(n.children);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function visit(node: unknown): void {
|
|
244
|
+
if (!node || typeof node !== 'object') return;
|
|
245
|
+
const n = node as Record<string, unknown>;
|
|
246
|
+
if (n.type === 'image' && typeof n.url === 'string' && n.url) refs.add(n.url);
|
|
247
|
+
if ((n.type === 'htmlBlock' || n.type === 'htmlInline') && Array.isArray(n.htmlChildren)) {
|
|
248
|
+
visitHtml(n.htmlChildren as HtmlNode[]);
|
|
249
|
+
}
|
|
250
|
+
if (Array.isArray(n.children)) for (const c of n.children) visit(c);
|
|
251
|
+
}
|
|
252
|
+
for (const child of doc.children) visit(child);
|
|
253
|
+
return refs;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
interface ParsedLinkRef {
|
|
257
|
+
/** Pathname portion before any `#` or `?`. */
|
|
258
|
+
path: string;
|
|
259
|
+
/** `#fragment` suffix (with leading `#`) or empty string. */
|
|
260
|
+
fragment: string;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Split an authored URL into path + fragment, returning null for
|
|
265
|
+
* external / non-document references (http(s), mailto, data, blob,
|
|
266
|
+
* absolute paths, fragment-only). The fragment is preserved so a
|
|
267
|
+
* link like `resume.md#experience` rewrites cleanly to
|
|
268
|
+
* `resume.html#experience`.
|
|
269
|
+
*/
|
|
270
|
+
function parseLinkRef(url: string): ParsedLinkRef | null {
|
|
271
|
+
if (!url) return null;
|
|
272
|
+
if (url.startsWith('#')) return null; // intra-doc anchor
|
|
273
|
+
if (
|
|
274
|
+
/^[a-z][a-z0-9+.-]*:/.test(url) || // http:, https:, mailto:, ftp:, …
|
|
275
|
+
url.startsWith('//') ||
|
|
276
|
+
url.startsWith('/')
|
|
277
|
+
) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
const hashIdx = url.indexOf('#');
|
|
281
|
+
const queryIdx = url.indexOf('?');
|
|
282
|
+
const cut =
|
|
283
|
+
hashIdx >= 0 && queryIdx >= 0 ? Math.min(hashIdx, queryIdx) : hashIdx >= 0 ? hashIdx : queryIdx;
|
|
284
|
+
const path = cut >= 0 ? url.slice(0, cut) : url;
|
|
285
|
+
const fragment = hashIdx >= 0 ? url.slice(hashIdx) : '';
|
|
286
|
+
return { path, fragment };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── Image gathering ────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Resolve every image referenced by a document to container-relative
|
|
293
|
+
* paths and fetch their bytes. The returned map keys are the authored
|
|
294
|
+
* URLs (so the renderer's `images` map can substitute them); the values
|
|
295
|
+
* are tuples of `[bytes, zipPath]` where `zipPath` is the path used
|
|
296
|
+
* inside the zip.
|
|
297
|
+
*
|
|
298
|
+
* Images that can't be read are silently dropped — the rendered HTML
|
|
299
|
+
* will keep its `<img src>` pointing at the authored URL, which a
|
|
300
|
+
* reader's browser will 404. That matches the user-facing "abort on
|
|
301
|
+
* missing linked doc" rule which applies to `.md` links only.
|
|
302
|
+
*/
|
|
303
|
+
async function readImagesForDoc(
|
|
304
|
+
mdDoc: MarkdownDocument,
|
|
305
|
+
docDir: string,
|
|
306
|
+
readBinary: (path: string) => Promise<ArrayBuffer | null>,
|
|
307
|
+
): Promise<Map<string, { data: ArrayBuffer; zipPath: string }>> {
|
|
308
|
+
const out = new Map<string, { data: ArrayBuffer; zipPath: string }>();
|
|
309
|
+
for (const authored of collectImageRefs(mdDoc)) {
|
|
310
|
+
if (/^[a-z][a-z0-9+.-]*:/.test(authored) || authored.startsWith('//')) continue;
|
|
311
|
+
const cleanAuthored = authored.replace(/[#?].*$/, '');
|
|
312
|
+
const resolved = resolveRelative(docDir, cleanAuthored);
|
|
313
|
+
if (resolved === null) continue;
|
|
314
|
+
const data = await readBinary(resolved);
|
|
315
|
+
if (!data) continue;
|
|
316
|
+
out.set(authored, { data, zipPath: resolved });
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ── Path helpers (POSIX-style, no Node `path`) ─────────────────────
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Strip a path to its parent directory (POSIX-style). Returns `''`
|
|
325
|
+
* for root-level files so subsequent joins read like a fresh path.
|
|
326
|
+
*/
|
|
327
|
+
export function posixDirname(p: string): string {
|
|
328
|
+
const idx = p.lastIndexOf('/');
|
|
329
|
+
return idx < 0 ? '' : p.slice(0, idx);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Normalize a POSIX path: collapse `./`, resolve `..`, drop trailing
|
|
334
|
+
* slashes, never produce a leading `/`. Returns null when the path
|
|
335
|
+
* escapes the start ("..").
|
|
336
|
+
*/
|
|
337
|
+
export function normalizePath(p: string): string | null {
|
|
338
|
+
const parts = p.split('/');
|
|
339
|
+
const out: string[] = [];
|
|
340
|
+
for (const segment of parts) {
|
|
341
|
+
if (segment === '' || segment === '.') continue;
|
|
342
|
+
if (segment === '..') {
|
|
343
|
+
if (out.length === 0) return null;
|
|
344
|
+
out.pop();
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
out.push(segment);
|
|
348
|
+
}
|
|
349
|
+
return out.join('/');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Resolve `rel` against `baseDir`. Returns null when the result
|
|
354
|
+
* escapes the container root via `..`.
|
|
355
|
+
*/
|
|
356
|
+
export function resolveRelative(baseDir: string, rel: string): string | null {
|
|
357
|
+
if (rel.startsWith('/')) return null; // absolute paths are out of scope
|
|
358
|
+
const joined = baseDir ? `${baseDir}/${rel}` : rel;
|
|
359
|
+
return normalizePath(joined);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* True when `target` is inside `root` (or equal to it). Empty root
|
|
364
|
+
* means "entire container is in scope".
|
|
365
|
+
*/
|
|
366
|
+
export function isInScope(target: string, root: string): boolean {
|
|
367
|
+
if (!root) return true;
|
|
368
|
+
return target === root || target.startsWith(root + '/');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Compute a relative path from `fromDir` to `toPath` (both POSIX-
|
|
373
|
+
* normalized, container-relative). Used to emit hrefs that work after
|
|
374
|
+
* unzipping anywhere — `home.md → subfolder/notes.md` becomes
|
|
375
|
+
* `subfolder/notes.html`; `subfolder/intro.md → resume.md` becomes
|
|
376
|
+
* `../resume.html`.
|
|
377
|
+
*/
|
|
378
|
+
export function relativeFrom(fromDir: string, toPath: string): string {
|
|
379
|
+
const fromParts = fromDir ? fromDir.split('/') : [];
|
|
380
|
+
const toParts = toPath.split('/');
|
|
381
|
+
let common = 0;
|
|
382
|
+
while (
|
|
383
|
+
common < fromParts.length &&
|
|
384
|
+
common < toParts.length - 1 &&
|
|
385
|
+
fromParts[common] === toParts[common]
|
|
386
|
+
) {
|
|
387
|
+
common++;
|
|
388
|
+
}
|
|
389
|
+
const up = fromParts.length - common;
|
|
390
|
+
const down = toParts.slice(common);
|
|
391
|
+
const prefix = Array(up).fill('..').join('/');
|
|
392
|
+
if (!prefix) return down.join('/');
|
|
393
|
+
return `${prefix}/${down.join('/')}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Sanitize a path for use as a zip entry. Strips leading slashes,
|
|
398
|
+
* normalizes backslashes (Windows-authored paths), and rejects any
|
|
399
|
+
* path containing `..` segments after normalization (defensive — the
|
|
400
|
+
* resolver above already filters those out for links).
|
|
401
|
+
*/
|
|
402
|
+
function sanitizeZipPath(path: string): string | null {
|
|
403
|
+
const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
404
|
+
if (!normalized) return null;
|
|
405
|
+
if (normalized.split('/').some((seg) => seg === '..')) return null;
|
|
406
|
+
return normalized;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Derive a default page title for a non-entry doc: prefer frontmatter
|
|
411
|
+
* `title`, fall back to the shallowest heading text, then the filename
|
|
412
|
+
* without extension.
|
|
413
|
+
*/
|
|
414
|
+
function titleForFilename(path: string, mdDoc: MarkdownDocument): string {
|
|
415
|
+
const inferred = inferDocumentTitle(mdDoc);
|
|
416
|
+
if (inferred) return inferred;
|
|
417
|
+
const base = path.split('/').pop() ?? path;
|
|
418
|
+
return base.replace(/\.md$/i, '');
|
|
419
|
+
}
|
package/src/pptx/export.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import type { Doc } from '@bendyline/squisq/schemas';
|
|
31
|
-
import { resolveTheme } from '@bendyline/squisq/schemas';
|
|
31
|
+
import { resolveTheme, resolveFontFamily } from '@bendyline/squisq/schemas';
|
|
32
32
|
import type { Theme } from '@bendyline/squisq/schemas';
|
|
33
33
|
import { docToMarkdown } from '@bendyline/squisq/doc';
|
|
34
34
|
import type {
|
|
@@ -48,6 +48,7 @@ import type {
|
|
|
48
48
|
MarkdownLink,
|
|
49
49
|
MarkdownImage,
|
|
50
50
|
} from '@bendyline/squisq/markdown';
|
|
51
|
+
import { readFrontmatterThemeId } from '@bendyline/squisq/markdown';
|
|
51
52
|
|
|
52
53
|
import { createPackage } from '../ooxml/writer.js';
|
|
53
54
|
import { xmlDeclaration, escapeXml } from '../ooxml/xmlUtils.js';
|
|
@@ -137,11 +138,11 @@ export async function markdownDocToPptx(
|
|
|
137
138
|
doc: MarkdownDocument,
|
|
138
139
|
options: PptxExportOptions = {},
|
|
139
140
|
): Promise<ArrayBuffer> {
|
|
140
|
-
// Resolve theme from options or frontmatter
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
141
|
+
// Resolve theme from options or frontmatter. The frontmatter lookup
|
|
142
|
+
// accepts the editor's canonical `squisq-theme` key as well as the
|
|
143
|
+
// shorter legacy `themeId` / `theme` aliases — see
|
|
144
|
+
// `readFrontmatterThemeId` for the precedence.
|
|
145
|
+
const themeId = options.themeId ?? readFrontmatterThemeId(doc.frontmatter);
|
|
145
146
|
const style = resolveSlideStyle(themeId, options);
|
|
146
147
|
|
|
147
148
|
const slides = segmentIntoSlides(doc.children, options.slideBreak ?? 'h2');
|
|
@@ -218,9 +219,9 @@ function resolveSlideStyle(themeId: string | undefined, options: PptxExportOptio
|
|
|
218
219
|
text: stripHash(c.text),
|
|
219
220
|
titleColor: stripHash(c.highlight || c.secondary || c.text),
|
|
220
221
|
mutedColor: stripHash(c.textMuted || c.text),
|
|
221
|
-
titleFont: theme.typography?.
|
|
222
|
-
bodyFont: theme.typography?.
|
|
223
|
-
codeFont: theme.typography?.
|
|
222
|
+
titleFont: resolveFontFamily(theme.typography?.titleFont, DEFAULT_TITLE_FONT),
|
|
223
|
+
bodyFont: resolveFontFamily(theme.typography?.bodyFont, options.defaultFont || DEFAULT_FONT),
|
|
224
|
+
codeFont: resolveFontFamily(theme.typography?.monoFont, DEFAULT_CODE_FONT),
|
|
224
225
|
codeColor: stripHash(c.textMuted || c.text),
|
|
225
226
|
hasTheme: true,
|
|
226
227
|
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/epub/export.ts"],"sourcesContent":["/**\n * EPUB 3 Export\n *\n * Converts a MarkdownDocument (or Doc) to an EPUB 3 file (.epub).\n *\n * An EPUB is a ZIP archive containing XHTML chapter files, images,\n * a package manifest (content.opf), and a navigation document (toc.xhtml).\n * Content is split into chapters at H1/H2 heading boundaries.\n *\n * Uses JSZip for packaging (already a dependency), escapeXml from the\n * OOXML utils, and image utilities from the HTML exporter.\n *\n * @example\n * ```ts\n * import { markdownDocToEpub } from '@bendyline/squisq-formats/epub';\n *\n * const epub = await markdownDocToEpub(markdownDoc, {\n * title: 'My Book',\n * author: 'Jane Doe',\n * });\n * ```\n */\n\nimport JSZip from 'jszip';\nimport type { Doc, AudioSegment } from '@bendyline/squisq/schemas';\nimport { resolveTheme } from '@bendyline/squisq/schemas';\nimport type {\n MarkdownDocument,\n MarkdownBlockNode,\n MarkdownInlineNode,\n MarkdownHeading,\n MarkdownParagraph,\n MarkdownListItem,\n MarkdownTable,\n MarkdownTableRow,\n MarkdownTableCell,\n} from '@bendyline/squisq/markdown';\nimport { escapeXml } from '../ooxml/xmlUtils.js';\nimport { inferMimeType, extractFilename } from '../html/imageUtils.js';\n\n// ── Public API ────────────────────────────────────────────────────\n\nexport interface EpubExportOptions {\n /** Book title (default: 'Untitled') */\n title?: string;\n /** Author name */\n author?: string;\n /** Book description / summary */\n description?: string;\n /** BCP-47 language code (default: 'en') */\n language?: string;\n /** Publisher name */\n publisher?: string;\n /** Squisq theme ID for CSS styling */\n themeId?: string;\n /** Pre-resolved image data keyed by relative path as it appears in the markdown */\n images?: Map<string, ArrayBuffer>;\n /** Cover image data (JPEG or PNG) */\n coverImage?: ArrayBuffer;\n /**\n * Audio narration data keyed by segment src/name.\n * When provided alongside audioSegments, EPUB 3 Media Overlays (SMIL)\n * are generated for synchronized audio playback.\n */\n audio?: Map<string, ArrayBuffer>;\n /**\n * Audio segment metadata (from Doc.audio.segments).\n * Required together with `audio` to generate Media Overlays.\n * Each segment's duration and startTime are used to build SMIL timing.\n */\n audioSegments?: AudioSegment[];\n /** Total document duration in seconds (used for Media Overlay metadata) */\n totalDuration?: number;\n}\n\n/**\n * Convert a MarkdownDocument to an EPUB 3 file.\n *\n * Chapters are split at H1/H2 heading boundaries. All referenced images\n * (provided via `options.images`) are embedded in the archive.\n */\nexport async function markdownDocToEpub(\n doc: MarkdownDocument,\n options: EpubExportOptions = {},\n): Promise<ArrayBuffer> {\n const fmTitle = doc.frontmatter?.title;\n const fmAuthor = doc.frontmatter?.author;\n const title = options.title ?? (typeof fmTitle === 'string' ? fmTitle : 'Untitled');\n const author = options.author ?? (typeof fmAuthor === 'string' ? fmAuthor : '');\n const language = options.language ?? 'en';\n const description = options.description ?? '';\n const publisher = options.publisher ?? '';\n const uuid = crypto.randomUUID();\n\n // Split document into chapters\n const chapters = splitIntoChapters(doc.children);\n\n // Collect images referenced in the document, deduplicating filenames\n const imageEntries = collectDocImages(doc.children);\n const resolvedImages = new Map<string, { data: ArrayBuffer; mime: string; filename: string }>();\n if (options.images) {\n const usedNames = new Set<string>();\n for (const src of imageEntries) {\n const data = options.images.get(src);\n if (data) {\n let filename = extractFilename(src);\n // Deduplicate: if two paths share a basename (e.g. a/hero.png and b/hero.png)\n if (usedNames.has(filename)) {\n const dot = filename.lastIndexOf('.');\n const base = dot > 0 ? filename.slice(0, dot) : filename;\n const ext = dot > 0 ? filename.slice(dot) : '';\n let counter = 2;\n while (usedNames.has(`${base}-${counter}${ext}`)) counter++;\n filename = `${base}-${counter}${ext}`;\n }\n usedNames.add(filename);\n resolvedImages.set(src, { data, mime: inferMimeType(filename), filename });\n }\n }\n }\n\n // Generate theme CSS\n const css = generateStylesheet(options.themeId);\n\n // Build the ZIP\n const zip = new JSZip();\n\n // mimetype must be first entry, stored (not compressed)\n zip.file('mimetype', 'application/epub+zip', { compression: 'STORE' });\n\n // META-INF/container.xml\n zip.file('META-INF/container.xml', CONTAINER_XML);\n\n // OEBPS/styles.css\n zip.file('OEBPS/styles.css', css);\n\n // OEBPS/images/*\n for (const [, img] of resolvedImages) {\n zip.file(`OEBPS/images/${img.filename}`, img.data);\n }\n\n // Cover image — detect PNG vs JPEG from magic bytes.\n // When provided, generates a cover.xhtml page in the spine so e-readers\n // (especially Kindle) display the cover as both thumbnail and first page.\n let coverFilename: string | undefined;\n if (options.coverImage) {\n const bytes = new Uint8Array(options.coverImage);\n const isPng =\n bytes.length >= 4 &&\n bytes[0] === 0x89 &&\n bytes[1] === 0x50 &&\n bytes[2] === 0x4e &&\n bytes[3] === 0x47;\n coverFilename = isPng ? 'cover.png' : 'cover.jpg';\n zip.file(`OEBPS/images/${coverFilename}`, options.coverImage);\n\n // Generate cover XHTML page — full-bleed image, no margins\n const coverXhtml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n<head>\n <meta charset=\"UTF-8\"/>\n <title>Cover</title>\n <style>\n body { margin: 0; padding: 0; text-align: center; }\n img { max-width: 100%; max-height: 100%; }\n </style>\n</head>\n<body>\n <img src=\"../images/${escapeXml(coverFilename)}\" alt=\"Cover\"/>\n</body>\n</html>`;\n zip.file('OEBPS/chapters/cover.xhtml', coverXhtml);\n }\n\n // ── Audio narration ──────────────────────────────────────────────\n const audioMap = options.audio;\n const audioSegments = options.audioSegments;\n const hasAudio = audioMap && audioSegments && audioSegments.length > 0;\n // Per-segment audio file info, indexed by segment index (null if data missing)\n const segmentAudioFiles: ({ filename: string; mime: string } | null)[] = [];\n const allAudioFiles: { filename: string; mime: string }[] = [];\n\n if (hasAudio) {\n for (const seg of audioSegments) {\n const data = audioMap.get(seg.src) ?? audioMap.get(seg.name);\n if (data) {\n const filename = extractFilename(seg.src);\n const finalName = filename.includes('.') ? filename : `${filename}.mp3`;\n zip.file(`OEBPS/audio/${finalName}`, data);\n const info = { filename: finalName, mime: inferMimeType(finalName) };\n segmentAudioFiles.push(info);\n allAudioFiles.push(info);\n } else {\n segmentAudioFiles.push(null);\n }\n }\n }\n\n // Build chapter-to-audio mapping for SMIL overlays\n const chapterAudio: (ChapterAudioInfo | null)[] = [];\n if (hasAudio && allAudioFiles.length > 0) {\n if (chapters.length !== audioSegments.length) {\n console.warn(\n `EPUB: ${chapters.length} chapters but ${audioSegments.length} audio segments — ` +\n `extra chapters will reuse the last segment's audio`,\n );\n }\n for (let i = 0; i < chapters.length; i++) {\n const segIdx = Math.min(i, audioSegments.length - 1);\n const seg = audioSegments[segIdx];\n const audioFile = segmentAudioFiles[segIdx];\n if (audioFile) {\n chapterAudio.push({\n audioFilename: audioFile.filename,\n clipStart: 0,\n clipEnd: seg.duration,\n duration: seg.duration,\n });\n } else {\n chapterAudio.push(null);\n }\n }\n }\n\n // OEBPS/chapters/*.xhtml + optional SMIL overlays\n const chapterFiles: ChapterFileInfo[] = [];\n for (let i = 0; i < chapters.length; i++) {\n const chap = chapters[i];\n const num = String(i + 1).padStart(3, '0');\n const id = `chapter-${num}`;\n const filename = `${id}.xhtml`;\n const audioInfo = chapterAudio[i] ?? null;\n\n // Render XHTML with element IDs for SMIL references when audio is present\n const xhtml = renderChapterXhtml(chap.nodes, title, resolvedImages, audioInfo !== null);\n zip.file(`OEBPS/chapters/${filename}`, xhtml);\n\n let smilFilename: string | undefined;\n if (audioInfo) {\n smilFilename = `${id}.smil`;\n const smil = generateSmil(filename, audioInfo, chap.nodes);\n zip.file(`OEBPS/chapters/${smilFilename}`, smil);\n }\n\n chapterFiles.push({\n id,\n filename,\n title: chap.title,\n smilFilename,\n duration: audioInfo?.duration,\n });\n }\n\n // OEBPS/toc.xhtml (EPUB 3 nav)\n zip.file('OEBPS/toc.xhtml', generateTocXhtml(chapterFiles, title));\n\n // OEBPS/content.opf\n zip.file(\n 'OEBPS/content.opf',\n generateContentOpf({\n uuid,\n title,\n author,\n language,\n description,\n publisher,\n chapters: chapterFiles,\n images: resolvedImages,\n coverFilename,\n audioFiles: allAudioFiles,\n totalDuration: options.totalDuration,\n }),\n );\n\n const blob = await zip.generateAsync({\n type: 'arraybuffer',\n compression: 'DEFLATE',\n compressionOptions: { level: 6 },\n // mimetype was already set to STORE above; JSZip respects per-file options\n });\n\n return blob;\n}\n\n/**\n * Convert a squisq Doc to an EPUB 3 file.\n *\n * Convenience wrapper: Doc → MarkdownDocument → EPUB.\n * When the Doc has audio segments and `options.audio` is provided,\n * EPUB 3 Media Overlays are generated for narrated playback.\n */\nexport async function docToEpub(doc: Doc, options: EpubExportOptions = {}): Promise<ArrayBuffer> {\n const { docToMarkdown } = await import('@bendyline/squisq/doc');\n const markdownDoc = docToMarkdown(doc);\n\n // Thread audio segment metadata from the Doc into options\n const epubOptions: EpubExportOptions = { ...options };\n if (doc.audio?.segments?.length && !epubOptions.audioSegments) {\n epubOptions.audioSegments = doc.audio.segments;\n }\n if (doc.duration && !epubOptions.totalDuration) {\n epubOptions.totalDuration = doc.duration;\n }\n\n return markdownDocToEpub(markdownDoc, epubOptions);\n}\n\n// ── Chapter Splitting ─────────────────────────────────────────────\n\ninterface Chapter {\n title: string;\n nodes: MarkdownBlockNode[];\n}\n\ninterface ChapterFileInfo {\n id: string;\n filename: string;\n title: string;\n smilFilename?: string;\n duration?: number;\n}\n\ninterface ChapterAudioInfo {\n audioFilename: string;\n clipStart: number;\n clipEnd: number;\n duration: number;\n}\n\nfunction splitIntoChapters(nodes: MarkdownBlockNode[]): Chapter[] {\n const chapters: Chapter[] = [];\n let currentNodes: MarkdownBlockNode[] = [];\n let currentTitle = 'Untitled';\n\n for (const node of nodes) {\n if (node.type === 'heading' && node.depth <= 2) {\n // Flush previous chapter\n if (currentNodes.length > 0) {\n chapters.push({ title: currentTitle, nodes: currentNodes });\n }\n currentTitle = extractHeadingText(node);\n currentNodes = [node];\n } else {\n currentNodes.push(node);\n }\n }\n\n // Flush remaining\n if (currentNodes.length > 0) {\n chapters.push({ title: currentTitle, nodes: currentNodes });\n }\n\n // If no chapters were created, wrap everything as one\n if (chapters.length === 0) {\n chapters.push({ title: 'Untitled', nodes: [] });\n }\n\n return chapters;\n}\n\nfunction extractHeadingText(heading: MarkdownHeading): string {\n return heading.children.map(inlineToText).join('');\n}\n\nfunction inlineToText(node: MarkdownInlineNode): string {\n switch (node.type) {\n case 'text':\n return node.value;\n case 'emphasis':\n case 'strong':\n case 'delete':\n return node.children.map(inlineToText).join('');\n case 'inlineCode':\n return node.value;\n case 'link':\n return node.children.map(inlineToText).join('');\n case 'image':\n return node.alt ?? '';\n case 'break':\n return ' ';\n default:\n return '';\n }\n}\n\n// ── Image Collection ──────────────────────────────────────────────\n\nfunction collectDocImages(nodes: MarkdownBlockNode[]): Set<string> {\n const images = new Set<string>();\n\n function walkBlock(node: MarkdownBlockNode): void {\n switch (node.type) {\n case 'paragraph':\n case 'heading':\n node.children.forEach(walkInline);\n break;\n case 'blockquote':\n node.children.forEach(walkBlock);\n break;\n case 'list':\n node.children.forEach((item) => item.children.forEach(walkBlock));\n break;\n case 'table':\n node.children.forEach((row) =>\n row.children.forEach((cell) => cell.children.forEach(walkInline)),\n );\n break;\n default:\n break;\n }\n }\n\n function walkInline(node: MarkdownInlineNode): void {\n if (node.type === 'image' && node.url && !node.url.startsWith('data:')) {\n images.add(node.url);\n }\n if ('children' in node && Array.isArray(node.children)) {\n (node.children as MarkdownInlineNode[]).forEach(walkInline);\n }\n }\n\n nodes.forEach(walkBlock);\n return images;\n}\n\n// ── XHTML Rendering ───────────────────────────────────────────────\n\ntype ImageMap = Map<string, { data: ArrayBuffer; mime: string; filename: string }>;\n\nfunction renderChapterXhtml(\n nodes: MarkdownBlockNode[],\n bookTitle: string,\n images: ImageMap,\n addIds = false,\n): string {\n let elementCounter = 0;\n const nextId = () => `p${++elementCounter}`;\n const body = nodes.map((n) => blockToXhtml(n, images, addIds ? nextId : undefined)).join('\\n');\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n<head>\n <meta charset=\"UTF-8\"/>\n <title>${escapeXml(bookTitle)}</title>\n <link rel=\"stylesheet\" type=\"text/css\" href=\"../styles.css\"/>\n</head>\n<body>\n${body}\n</body>\n</html>`;\n}\n\nfunction blockToXhtml(node: MarkdownBlockNode, images: ImageMap, nextId?: () => string): string {\n const idAttr = nextId ? ` id=\"${nextId()}\"` : '';\n\n switch (node.type) {\n case 'heading': {\n const tag = `h${node.depth}`;\n return `<${tag}${idAttr}>${inlinesToXhtml(node.children, images)}</${tag}>`;\n }\n\n case 'paragraph':\n return `<p${idAttr}>${inlinesToXhtml(node.children, images)}</p>`;\n\n case 'blockquote':\n return `<blockquote${idAttr}>\\n${node.children.map((c) => blockToXhtml(c, images, nextId)).join('\\n')}\\n</blockquote>`;\n\n case 'list': {\n const tag = node.ordered ? 'ol' : 'ul';\n const startAttr =\n node.ordered && node.start && node.start !== 1 ? ` start=\"${node.start}\"` : '';\n const items = node.children.map((item) => listItemToXhtml(item, images)).join('\\n');\n return `<${tag}${idAttr}${startAttr}>\\n${items}\\n</${tag}>`;\n }\n\n case 'code': {\n const langAttr = node.lang ? ` class=\"language-${escapeXml(node.lang)}\"` : '';\n return `<pre${idAttr}><code${langAttr}>${escapeXml(node.value)}</code></pre>`;\n }\n\n case 'thematicBreak':\n return `<hr${idAttr}/>`;\n\n case 'table':\n return tableToXhtml(node as MarkdownTable, images, idAttr);\n\n case 'htmlBlock':\n // Strip HTML tags for XHTML safety — raw HTML may not be well-formed XML\n return `<p${idAttr}>${escapeXml(node.rawHtml.replace(/<[^>]+>/g, ''))}</p>`;\n\n case 'math':\n return `<p${idAttr} class=\"math\">${escapeXml(node.value)}</p>`;\n\n default:\n return '';\n }\n}\n\nfunction listItemToXhtml(item: MarkdownListItem, images: ImageMap): string {\n const content = item.children.map((c) => blockToXhtml(c, images)).join('\\n');\n // Unwrap single <p> inside <li> for cleaner output\n const unwrapped =\n item.children.length === 1 && item.children[0].type === 'paragraph'\n ? inlinesToXhtml((item.children[0] as MarkdownParagraph).children, images)\n : content;\n return `<li>${unwrapped}</li>`;\n}\n\nfunction tableToXhtml(table: MarkdownTable, images: ImageMap, idAttr = ''): string {\n const rows = table.children;\n if (rows.length === 0) return `<table${idAttr}></table>`;\n\n const headerRow = rows[0];\n const bodyRows = rows.slice(1);\n const align = table.align ?? [];\n\n function cellToXhtml(cell: MarkdownTableCell, tag: 'th' | 'td', colIndex: number): string {\n const a = align[colIndex];\n const style = a ? ` style=\"text-align: ${a}\"` : '';\n return `<${tag}${style}>${inlinesToXhtml(cell.children, images)}</${tag}>`;\n }\n\n const thead = `<thead><tr>${headerRow.children.map((c, i) => cellToXhtml(c, 'th', i)).join('')}</tr></thead>`;\n const tbody =\n bodyRows.length > 0\n ? `<tbody>${bodyRows.map((row: MarkdownTableRow) => `<tr>${row.children.map((c, i) => cellToXhtml(c, 'td', i)).join('')}</tr>`).join('')}</tbody>`\n : '';\n\n return `<table${idAttr}>${thead}${tbody}</table>`;\n}\n\nfunction inlinesToXhtml(nodes: MarkdownInlineNode[], images: ImageMap): string {\n return nodes.map((n) => inlineToXhtml(n, images)).join('');\n}\n\nfunction inlineToXhtml(node: MarkdownInlineNode, images: ImageMap): string {\n switch (node.type) {\n case 'text':\n return escapeXml(node.value);\n\n case 'strong':\n return `<strong>${inlinesToXhtml(node.children, images)}</strong>`;\n\n case 'emphasis':\n return `<em>${inlinesToXhtml(node.children, images)}</em>`;\n\n case 'delete':\n return `<del>${inlinesToXhtml(node.children, images)}</del>`;\n\n case 'inlineCode':\n return `<code>${escapeXml(node.value)}</code>`;\n\n case 'link': {\n const titleAttr = node.title ? ` title=\"${escapeXml(node.title)}\"` : '';\n return `<a href=\"${escapeXml(node.url)}\"${titleAttr}>${inlinesToXhtml(node.children, images)}</a>`;\n }\n\n case 'image': {\n const alt = escapeXml(node.alt ?? '');\n const resolved = images.get(node.url);\n const src = resolved ? `../images/${resolved.filename}` : escapeXml(node.url);\n return `<img src=\"${src}\" alt=\"${alt}\"/>`;\n }\n\n case 'break':\n return '<br/>';\n\n case 'inlineMath':\n return `<span class=\"math\">${escapeXml(node.value)}</span>`;\n\n case 'htmlInline':\n // Strip tags for XHTML safety\n return escapeXml(node.rawHtml.replace(/<[^>]+>/g, ''));\n\n default:\n return '';\n }\n}\n\n// ── SMIL Media Overlays ───────────────────────────────────────────\n\n/**\n * Generate an EPUB 3 Media Overlay (SMIL) file for a chapter.\n * Maps block-level elements to audio clip ranges for synchronized narration.\n */\nfunction generateSmil(\n chapterFilename: string,\n audioInfo: ChapterAudioInfo,\n nodes: MarkdownBlockNode[],\n): string {\n // Count block elements to match the IDs generated by blockToXhtml.\n // Must mirror blockToXhtml's recursion: each block gets an ID,\n // blockquote children recurse (they pass nextId), but list items do not.\n let elementCount = 0;\n function countBlocks(node: MarkdownBlockNode): void {\n elementCount++;\n if (node.type === 'blockquote') node.children.forEach(countBlocks);\n }\n nodes.forEach(countBlocks);\n\n if (elementCount === 0) elementCount = 1;\n\n // Distribute audio duration evenly across elements (best effort without word-level timing)\n const clipDuration = audioInfo.duration / elementCount;\n const pars: string[] = [];\n\n for (let i = 0; i < elementCount; i++) {\n const clipStart = formatTime(audioInfo.clipStart + i * clipDuration, true);\n const clipEnd = formatTime(audioInfo.clipStart + (i + 1) * clipDuration, true);\n pars.push(\n ` <par id=\"par-${i + 1}\">` +\n `\\n <text src=\"${chapterFilename}#p${i + 1}\"/>` +\n `\\n <audio src=\"../audio/${escapeXml(audioInfo.audioFilename)}\" clipBegin=\"${clipStart}\" clipEnd=\"${clipEnd}\"/>` +\n `\\n </par>`,\n );\n }\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<smil xmlns=\"http://www.w3.org/ns/SMIL\" xmlns:epub=\"http://www.idpf.org/2007/ops\" version=\"3.0\">\n <body>\n <seq id=\"seq-1\" epub:textref=\"${chapterFilename}\">\n${pars.join('\\n')}\n </seq>\n </body>\n</smil>`;\n}\n\nfunction formatTime(seconds: number, fractional = false): string {\n const h = Math.floor(seconds / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n const s = seconds % 60;\n const sPart = fractional ? s.toFixed(3).padStart(6, '0') : String(Math.floor(s)).padStart(2, '0');\n return `${h}:${String(m).padStart(2, '0')}:${sPart}`;\n}\n\n// ── Package Documents ─────────────────────────────────────────────\n\nconst CONTAINER_XML = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">\n <rootfiles>\n <rootfile full-path=\"OEBPS/content.opf\" media-type=\"application/oebps-package+xml\"/>\n </rootfiles>\n</container>`;\n\ninterface OpfParams {\n uuid: string;\n title: string;\n author: string;\n language: string;\n description: string;\n publisher: string;\n chapters: ChapterFileInfo[];\n images: ImageMap;\n coverFilename?: string;\n audioFiles?: { filename: string; mime: string }[];\n totalDuration?: number;\n}\n\nfunction generateContentOpf(params: OpfParams): string {\n const {\n uuid,\n title,\n author,\n language,\n description,\n publisher,\n chapters,\n images,\n coverFilename,\n audioFiles,\n totalDuration,\n } = params;\n const modified = new Date().toISOString().replace(/\\.\\d+Z$/, 'Z');\n const hasOverlays = chapters.some((c) => c.smilFilename);\n\n // Manifest items\n const manifestItems: string[] = [\n ' <item id=\"toc\" href=\"toc.xhtml\" media-type=\"application/xhtml+xml\" properties=\"nav\"/>',\n ' <item id=\"css\" href=\"styles.css\" media-type=\"text/css\"/>',\n ];\n\n if (coverFilename) {\n const mime = inferMimeType(coverFilename);\n manifestItems.push(\n ` <item id=\"cover-image\" href=\"images/${escapeXml(coverFilename)}\" media-type=\"${mime}\" properties=\"cover-image\"/>`,\n );\n manifestItems.push(\n ' <item id=\"cover-page\" href=\"chapters/cover.xhtml\" media-type=\"application/xhtml+xml\"/>',\n );\n }\n\n for (const chap of chapters) {\n const overlayAttr = chap.smilFilename ? ` media-overlay=\"${chap.id}-overlay\"` : '';\n manifestItems.push(\n ` <item id=\"${chap.id}\" href=\"chapters/${escapeXml(chap.filename)}\" media-type=\"application/xhtml+xml\"${overlayAttr}/>`,\n );\n if (chap.smilFilename) {\n manifestItems.push(\n ` <item id=\"${chap.id}-overlay\" href=\"chapters/${escapeXml(chap.smilFilename)}\" media-type=\"application/smil+xml\"/>`,\n );\n }\n }\n\n // Audio files in manifest\n if (audioFiles) {\n const usedAudioNames = new Set<string>();\n for (const af of audioFiles) {\n if (usedAudioNames.has(af.filename)) continue;\n usedAudioNames.add(af.filename);\n const audioId = `audio-${af.filename.replace(/[^a-zA-Z0-9]/g, '-')}`;\n manifestItems.push(\n ` <item id=\"${audioId}\" href=\"audio/${escapeXml(af.filename)}\" media-type=\"${af.mime}\"/>`,\n );\n }\n }\n\n const usedFilenames = new Set<string>();\n for (const [, img] of images) {\n if (usedFilenames.has(img.filename)) continue;\n usedFilenames.add(img.filename);\n const imgId = `img-${img.filename.replace(/[^a-zA-Z0-9]/g, '-')}`;\n manifestItems.push(\n ` <item id=\"${imgId}\" href=\"images/${escapeXml(img.filename)}\" media-type=\"${img.mime}\"/>`,\n );\n }\n\n // Spine — cover page first (if present), then chapters\n const spineEntries: string[] = [];\n if (coverFilename) {\n spineEntries.push(' <itemref idref=\"cover-page\"/>');\n }\n for (const chap of chapters) {\n spineEntries.push(` <itemref idref=\"${chap.id}\"/>`);\n }\n const spineItems = spineEntries.join('\\n');\n\n // Metadata\n const metaParts = [\n ` <dc:identifier id=\"uid\">urn:uuid:${uuid}</dc:identifier>`,\n ` <dc:title>${escapeXml(title)}</dc:title>`,\n ` <dc:language>${escapeXml(language)}</dc:language>`,\n ` <meta property=\"dcterms:modified\">${modified}</meta>`,\n ];\n if (author) metaParts.push(` <dc:creator>${escapeXml(author)}</dc:creator>`);\n if (description) metaParts.push(` <dc:description>${escapeXml(description)}</dc:description>`);\n if (publisher) metaParts.push(` <dc:publisher>${escapeXml(publisher)}</dc:publisher>`);\n\n // Media Overlay metadata\n if (hasOverlays && totalDuration) {\n metaParts.push(` <meta property=\"media:duration\">${formatTime(totalDuration)}</meta>`);\n metaParts.push(' <meta property=\"media:active-class\">epub-media-overlay-active</meta>');\n }\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<package xmlns=\"http://www.idpf.org/2007/opf\" version=\"3.0\" unique-identifier=\"uid\">\n <metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n${metaParts.join('\\n')}\n </metadata>\n <manifest>\n${manifestItems.join('\\n')}\n </manifest>\n <spine>\n${spineItems}\n </spine>\n</package>`;\n}\n\nfunction generateTocXhtml(chapters: ChapterFileInfo[], bookTitle: string): string {\n const navItems = chapters\n .map(\n (chap) =>\n ` <li><a href=\"chapters/${escapeXml(chap.filename)}\">${escapeXml(chap.title)}</a></li>`,\n )\n .join('\\n');\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n<head>\n <meta charset=\"UTF-8\"/>\n <title>${escapeXml(bookTitle)}</title>\n <link rel=\"stylesheet\" type=\"text/css\" href=\"styles.css\"/>\n</head>\n<body>\n <nav epub:type=\"toc\">\n <h1>Table of Contents</h1>\n <ol>\n${navItems}\n </ol>\n </nav>\n</body>\n</html>`;\n}\n\n// ── Stylesheet ────────────────────────────────────────────────────\n\nfunction generateStylesheet(themeId?: string): string {\n let themeVars = '';\n if (themeId) {\n const theme = resolveTheme(themeId);\n themeVars = `\n --epub-bg: ${theme.colors.background};\n --epub-text: ${theme.colors.text};\n --epub-primary: ${theme.colors.primary};\n --epub-heading-font: ${theme.typography.titleFontFamily};\n --epub-body-font: ${theme.typography.bodyFontFamily};`;\n }\n\n return `/* Squisq EPUB Stylesheet */\n:root {${themeVars}\n}\n\nbody {\n font-family: var(--epub-body-font, Georgia, 'Times New Roman', serif);\n color: var(--epub-text, #1a1a1a);\n line-height: 1.7;\n margin: 1em 2em;\n max-width: 40em;\n}\n\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--epub-heading-font, system-ui, sans-serif);\n color: var(--epub-primary, #1a1a1a);\n margin-top: 1.5em;\n margin-bottom: 0.5em;\n line-height: 1.3;\n}\n\nh1 { font-size: 2em; }\nh2 { font-size: 1.5em; }\nh3 { font-size: 1.25em; }\n\np {\n margin: 0.8em 0;\n}\n\na {\n color: var(--epub-primary, #2563eb);\n}\n\nimg {\n max-width: 100%;\n height: auto;\n}\n\npre {\n background: #f5f5f5;\n padding: 1em;\n overflow-x: auto;\n border-radius: 4px;\n font-size: 0.9em;\n line-height: 1.4;\n}\n\ncode {\n font-family: 'Courier New', Courier, monospace;\n font-size: 0.9em;\n}\n\np > code, li > code {\n background: #f0f0f0;\n padding: 0.1em 0.3em;\n border-radius: 3px;\n}\n\nblockquote {\n border-left: 3px solid var(--epub-primary, #d1d5db);\n margin: 1em 0;\n padding: 0.5em 1em;\n color: #4b5563;\n}\n\ntable {\n border-collapse: collapse;\n width: 100%;\n margin: 1em 0;\n}\n\nth, td {\n border: 1px solid #d1d5db;\n padding: 0.5em 0.75em;\n text-align: left;\n}\n\nth {\n background: #f3f4f6;\n font-weight: 600;\n}\n\nhr {\n border: none;\n border-top: 1px solid #d1d5db;\n margin: 2em 0;\n}\n\nul, ol {\n margin: 0.8em 0;\n padding-left: 1.5em;\n}\n\nli {\n margin: 0.3em 0;\n}\n\n.math {\n font-family: 'Courier New', Courier, monospace;\n font-style: italic;\n}\n\n/* Media Overlay active highlight (narration sync) */\n.epub-media-overlay-active {\n background-color: rgba(37, 99, 235, 0.12);\n}\n`;\n}\n"],"mappings":"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","names":[]}
|