@bendyline/squisq-formats 2.0.1 → 2.2.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.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +20 -0
  3. package/README.md +1 -1
  4. package/dist/{chunk-CRAVSMPZ.js → chunk-26ISNJ7Y.js} +356 -114
  5. package/dist/{chunk-NKAJPJ4G.js → chunk-2JJ5RFDZ.js} +0 -1
  6. package/dist/{chunk-HTW2M27H.js → chunk-3NKXBZSR.js} +193 -42
  7. package/dist/{chunk-U32AG3G3.js → chunk-4V3KCHAP.js} +3 -4
  8. package/dist/{chunk-MLX2BOJC.js → chunk-6RQOV3B3.js} +1 -2
  9. package/dist/{chunk-QRVN6A6E.js → chunk-6S6GU3ZG.js} +5 -6
  10. package/dist/{chunk-FE6OJV6O.js → chunk-7AWFHP5U.js} +1 -1
  11. package/dist/{chunk-RFAPOKHJ.js → chunk-AD2WT564.js} +59 -9
  12. package/dist/{chunk-O3GVVND4.js → chunk-AONELFLA.js} +0 -1
  13. package/dist/{chunk-XKUMNGBW.js → chunk-EJTNGKEA.js} +5 -8
  14. package/dist/chunk-GX7RAUME.js +121 -0
  15. package/dist/{chunk-SSUPBUF5.js → chunk-IIQYS2YH.js} +0 -1
  16. package/dist/{chunk-ABVI556T.js → chunk-IPN56VLW.js} +83 -58
  17. package/dist/{chunk-LXYLOOST.js → chunk-JE6LSIHE.js} +83 -22
  18. package/dist/{chunk-U4MRIFKL.js → chunk-JU2RHXUB.js} +0 -1
  19. package/dist/{chunk-4VUWTSGM.js → chunk-K6XRMVPW.js} +64 -31
  20. package/dist/{chunk-ODL3SSPT.js → chunk-KXOZMWBS.js} +0 -1
  21. package/dist/chunk-OGS5VCGJ.js +446 -0
  22. package/dist/{chunk-GVS2XXV6.js → chunk-PJXJI2LY.js} +449 -57
  23. package/dist/{chunk-2KPARF2P.js → chunk-PU7REGWV.js} +5 -8
  24. package/dist/{chunk-PN52A5AA.js → chunk-SBUW7NHR.js} +0 -1
  25. package/dist/{chunk-VSYHZECT.js → chunk-TAAENIRB.js} +5 -8
  26. package/dist/{chunk-WC7WULGV.js → chunk-X2DEAXNK.js} +62 -2
  27. package/dist/container/index.js +1 -2
  28. package/dist/csv/index.d.ts +27 -2
  29. package/dist/csv/index.js +1 -2
  30. package/dist/docx/index.d.ts +5 -1
  31. package/dist/docx/index.js +9 -10
  32. package/dist/epub/index.d.ts +2 -0
  33. package/dist/epub/index.js +5 -6
  34. package/dist/{export-D2NkylDT.d.ts → export-D9msROJS.d.ts} +18 -6
  35. package/dist/extract-MN7LA3NL.js +13 -0
  36. package/dist/html/index.d.ts +11 -4
  37. package/dist/html/index.js +3 -4
  38. package/dist/images-ESPQKVTW.js +6 -0
  39. package/dist/{import-K8mfc0fz.d.ts → import-C3htUTss.d.ts} +5 -1
  40. package/dist/{import-DTkDxHmZ.d.ts → import-C8whCC7_.d.ts} +6 -0
  41. package/dist/index.d.ts +7 -7
  42. package/dist/index.js +28 -26
  43. package/dist/infer/index.d.ts +3 -3
  44. package/dist/infer/index.js +7 -9
  45. package/dist/{layouts-BHrgZ5FS.d.ts → layouts-CTdPlB-u.d.ts} +1 -1
  46. package/dist/layouts-DRWZGSPD.js +10 -0
  47. package/dist/{mapTheme-IR27S6IV.js → mapTheme-4TWH25FT.js} +1 -2
  48. package/dist/ooxml/index.d.ts +3 -3
  49. package/dist/ooxml/index.js +14 -13
  50. package/dist/pdf/index.d.ts +18 -0
  51. package/dist/pdf/index.js +2 -3
  52. package/dist/pptx/index.d.ts +4 -4
  53. package/dist/pptx/index.js +11 -13
  54. package/dist/{reader-B9L8Ucbj.d.ts → reader-B_m1aKZC.d.ts} +30 -1
  55. package/dist/registry/index.d.ts +21 -5
  56. package/dist/registry/index.js +9 -6
  57. package/dist/{themeReader-DJKErl_j.d.ts → themeReader-DCtwC83Q.d.ts} +1 -1
  58. package/dist/xlsx/index.d.ts +3 -3
  59. package/dist/xlsx/index.js +6 -7
  60. package/package.json +6 -3
  61. package/dist/chunk-2KPARF2P.js.map +0 -1
  62. package/dist/chunk-4VUWTSGM.js.map +0 -1
  63. package/dist/chunk-6M7Z25LA.js +0 -46
  64. package/dist/chunk-6M7Z25LA.js.map +0 -1
  65. package/dist/chunk-ABVI556T.js.map +0 -1
  66. package/dist/chunk-CRAVSMPZ.js.map +0 -1
  67. package/dist/chunk-FE6OJV6O.js.map +0 -1
  68. package/dist/chunk-GVS2XXV6.js.map +0 -1
  69. package/dist/chunk-HTW2M27H.js.map +0 -1
  70. package/dist/chunk-LXYLOOST.js.map +0 -1
  71. package/dist/chunk-MLX2BOJC.js.map +0 -1
  72. package/dist/chunk-NKAJPJ4G.js.map +0 -1
  73. package/dist/chunk-O3GVVND4.js.map +0 -1
  74. package/dist/chunk-ODL3SSPT.js.map +0 -1
  75. package/dist/chunk-PN52A5AA.js.map +0 -1
  76. package/dist/chunk-QRVN6A6E.js.map +0 -1
  77. package/dist/chunk-RFAPOKHJ.js.map +0 -1
  78. package/dist/chunk-SSUPBUF5.js.map +0 -1
  79. package/dist/chunk-U32AG3G3.js.map +0 -1
  80. package/dist/chunk-U4MRIFKL.js.map +0 -1
  81. package/dist/chunk-VJJM2SSH.js +0 -275
  82. package/dist/chunk-VJJM2SSH.js.map +0 -1
  83. package/dist/chunk-VSYHZECT.js.map +0 -1
  84. package/dist/chunk-WC7WULGV.js.map +0 -1
  85. package/dist/chunk-XKUMNGBW.js.map +0 -1
  86. package/dist/chunk-YRT7GQ5Y.js +0 -28
  87. package/dist/chunk-YRT7GQ5Y.js.map +0 -1
  88. package/dist/container/index.js.map +0 -1
  89. package/dist/csv/index.js.map +0 -1
  90. package/dist/docx/index.js.map +0 -1
  91. package/dist/epub/index.js.map +0 -1
  92. package/dist/extract-H6RXJMHP.js +0 -15
  93. package/dist/extract-H6RXJMHP.js.map +0 -1
  94. package/dist/html/index.js.map +0 -1
  95. package/dist/images-7FBWPKE3.js +0 -7
  96. package/dist/images-7FBWPKE3.js.map +0 -1
  97. package/dist/index.js.map +0 -1
  98. package/dist/infer/index.js.map +0 -1
  99. package/dist/layouts-QVPK3ZCU.js +0 -12
  100. package/dist/layouts-QVPK3ZCU.js.map +0 -1
  101. package/dist/mapTheme-IR27S6IV.js.map +0 -1
  102. package/dist/ooxml/index.js.map +0 -1
  103. package/dist/pdf/index.js.map +0 -1
  104. package/dist/pptx/index.js.map +0 -1
  105. package/dist/registry/index.js.map +0 -1
  106. package/dist/xlsx/index.js.map +0 -1
  107. package/src/__tests__/container.test.ts +0 -230
  108. package/src/__tests__/convert.test.ts +0 -495
  109. package/src/__tests__/csvImport.test.ts +0 -84
  110. package/src/__tests__/docxExport.test.ts +0 -457
  111. package/src/__tests__/docxImport.test.ts +0 -410
  112. package/src/__tests__/epub.test.ts +0 -649
  113. package/src/__tests__/exportThemeReconciliation.test.ts +0 -87
  114. package/src/__tests__/formatRegistry.test.ts +0 -174
  115. package/src/__tests__/html.test.ts +0 -435
  116. package/src/__tests__/htmlImport.test.ts +0 -57
  117. package/src/__tests__/inferTheme.test.ts +0 -135
  118. package/src/__tests__/lossyWarnings.test.ts +0 -146
  119. package/src/__tests__/ooxml.test.ts +0 -271
  120. package/src/__tests__/ooxmlCancellation.test.ts +0 -113
  121. package/src/__tests__/ooxmlThemeReader.test.ts +0 -92
  122. package/src/__tests__/pdfExport.test.ts +0 -322
  123. package/src/__tests__/pdfImport.test.ts +0 -384
  124. package/src/__tests__/plainHtml.test.ts +0 -417
  125. package/src/__tests__/plainHtmlBundle.test.ts +0 -253
  126. package/src/__tests__/pptxExport.test.ts +0 -138
  127. package/src/__tests__/pptxImport.test.ts +0 -145
  128. package/src/__tests__/pptxInferFixtures.ts +0 -314
  129. package/src/__tests__/pptxLayoutInfer.test.ts +0 -395
  130. package/src/__tests__/roundTrip.test.ts +0 -201
  131. package/src/__tests__/roundTripAssets.test.ts +0 -50
  132. package/src/__tests__/roundTripMatrix.fixtures.ts +0 -86
  133. package/src/__tests__/roundTripMatrix.helpers.ts +0 -154
  134. package/src/__tests__/roundTripMatrix.test.ts +0 -142
  135. package/src/__tests__/sharedContainer.test.ts +0 -41
  136. package/src/__tests__/sharedImages.test.ts +0 -61
  137. package/src/__tests__/xlsxExport.test.ts +0 -164
  138. package/src/__tests__/xlsxImport.test.ts +0 -80
  139. package/src/__tests__/zipSafety.test.ts +0 -317
  140. package/src/container/index.ts +0 -94
  141. package/src/csv/index.ts +0 -188
  142. package/src/docx/export.ts +0 -1267
  143. package/src/docx/import.ts +0 -995
  144. package/src/docx/index.ts +0 -26
  145. package/src/docx/styles.ts +0 -145
  146. package/src/epub/export.ts +0 -968
  147. package/src/epub/index.ts +0 -20
  148. package/src/html/docsHtmlBundle.ts +0 -373
  149. package/src/html/htmlTemplate.ts +0 -385
  150. package/src/html/imageUtils.ts +0 -61
  151. package/src/html/import.ts +0 -297
  152. package/src/html/index.ts +0 -212
  153. package/src/html/plainHtml.ts +0 -790
  154. package/src/html/plainHtmlBundle.ts +0 -421
  155. package/src/index.ts +0 -109
  156. package/src/infer/extract.ts +0 -127
  157. package/src/infer/index.ts +0 -199
  158. package/src/infer/mapTheme.ts +0 -176
  159. package/src/infer/types.ts +0 -27
  160. package/src/ooxml/index.ts +0 -111
  161. package/src/ooxml/namespaces.ts +0 -196
  162. package/src/ooxml/readUtils.ts +0 -44
  163. package/src/ooxml/reader.ts +0 -318
  164. package/src/ooxml/themeReader.ts +0 -197
  165. package/src/ooxml/types.ts +0 -103
  166. package/src/ooxml/writer.ts +0 -339
  167. package/src/ooxml/xmlUtils.ts +0 -123
  168. package/src/pdf/export.ts +0 -1084
  169. package/src/pdf/import.ts +0 -1164
  170. package/src/pdf/index.ts +0 -29
  171. package/src/pdf/styles.ts +0 -180
  172. package/src/pptx/export.ts +0 -1184
  173. package/src/pptx/import.ts +0 -455
  174. package/src/pptx/index.ts +0 -52
  175. package/src/pptx/layouts.ts +0 -1222
  176. package/src/pptx/styles.ts +0 -96
  177. package/src/pptx/templates.ts +0 -187
  178. package/src/registry/convert.ts +0 -433
  179. package/src/registry/defaultFormats.ts +0 -413
  180. package/src/registry/errors.ts +0 -46
  181. package/src/registry/index.ts +0 -43
  182. package/src/registry/registry.ts +0 -48
  183. package/src/registry/types.ts +0 -170
  184. package/src/shared/boundedZipArchive.ts +0 -383
  185. package/src/shared/container.ts +0 -28
  186. package/src/shared/fidelity.ts +0 -130
  187. package/src/shared/images.ts +0 -44
  188. package/src/shared/inlineRuns.ts +0 -99
  189. package/src/shared/text.ts +0 -41
  190. package/src/shared/zipEntryCount.ts +0 -151
  191. package/src/shared/zipLimits.ts +0 -296
  192. package/src/shared/zipSafety.ts +0 -19
  193. package/src/xlsx/export.ts +0 -253
  194. package/src/xlsx/import.ts +0 -160
  195. package/src/xlsx/index.ts +0 -35
package/src/epub/index.ts DELETED
@@ -1,20 +0,0 @@
1
- /**
2
- * @bendyline/squisq-formats EPUB Module
3
- *
4
- * Export squisq documents (MarkdownDocument / Doc) to EPUB 3 e-book files.
5
- *
6
- * Content is split into chapters at H1/H2 heading boundaries. Images
7
- * referenced in the markdown are embedded in the archive when provided
8
- * via the `images` option.
9
- *
10
- * @example
11
- * ```ts
12
- * import { markdownDocToEpub, docToEpub } from '@bendyline/squisq-formats/epub';
13
- *
14
- * const epub = await markdownDocToEpub(markdownDoc, { title: 'My Book' });
15
- * const epub2 = await docToEpub(doc, { author: 'Jane Doe' });
16
- * ```
17
- */
18
-
19
- export { markdownDocToEpub, docToEpub } from './export.js';
20
- export type { EpubExportOptions } from './export.js';
@@ -1,373 +0,0 @@
1
- /**
2
- * Recursive rendered-HTML bundle export.
3
- *
4
- * `markdownDocsToHtmlBundle` is the rendered-pipeline counterpart of
5
- * `markdownDocsToPlainHtmlBundle`. Starting from a single entry markdown
6
- * file, it walks every relative `[…](other.md)` link, builds a Doc for
7
- * each visited page via `markdownToDoc`, rewrites cross-doc link URLs
8
- * inside the Doc tree from `.md` → relative `.html`, then renders each
9
- * Doc to a SquisqPlayer-backed HTML page sharing one `squisq-player.js`.
10
- * Images preserve their authored relative paths so per-doc folders
11
- * (`resume_files/hero.png`, etc.) sit beside the rendered HTML.
12
- *
13
- * The rewrite happens on the Doc, *before* JSON serialization, because
14
- * the rendered HTML embeds the Doc as JSON and SquisqPlayer emits the
15
- * `<a href>` at runtime — there is no post-render link rewrite hook in
16
- * the player path.
17
- */
18
-
19
- import JSZip from 'jszip';
20
- import { parseMarkdown, inferDocumentTitle } from '@bendyline/squisq/markdown';
21
- import type {
22
- MarkdownDocument,
23
- MarkdownBlockNode,
24
- MarkdownInlineNode,
25
- HtmlNode,
26
- } from '@bendyline/squisq/markdown';
27
- import { markdownToDoc } from '@bendyline/squisq/doc';
28
- import type { Block, Doc, ThemeRegistry } from '@bendyline/squisq/schemas';
29
- import { generateExternalHtml } from './htmlTemplate.js';
30
- import {
31
- collectLinkRefs,
32
- isInScope,
33
- normalizePath,
34
- posixDirname,
35
- relativeFrom,
36
- resolveRelative,
37
- } from './plainHtmlBundle.js';
38
-
39
- // ── Public Types ───────────────────────────────────────────────────
40
-
41
- export interface HtmlBundleOptions {
42
- /** Entry document path relative to the container root (e.g. `'home.md'`). */
43
- entryPath: string;
44
- /** Reads a UTF-8 markdown file from the container. Returns null when absent. */
45
- readDocument: (path: string) => Promise<string | null>;
46
- /** Reads a binary asset (image) from the container. Returns null when absent. */
47
- readBinary: (path: string) => Promise<ArrayBuffer | null>;
48
- /** SquisqPlayer IIFE bundle. Written once as `squisq-player.js` in the zip. */
49
- playerScript: string;
50
- /** Optional document title for the entry. Other pages derive from filename. */
51
- title?: string;
52
- /** Theme id applied uniformly to every page. */
53
- themeId?: string;
54
- /** Explicit caller-owned registry for non-document custom themes. */
55
- themeRegistry?: ThemeRegistry;
56
- /** Rendering mode for every page (default: 'static' — scrollable, link-friendly). */
57
- mode?: 'slideshow' | 'static';
58
- /** Maximum recursion depth (default: unlimited; cycles always handled). */
59
- maxDepth?: number;
60
- /**
61
- * Emit the entry doc as `index.html` (preserving its parent directory)
62
- * instead of `<basename>.html`. Cross-doc links pointing at the entry
63
- * also rewrite to `index.html`, so a sibling `resume.md → home.md`
64
- * link doesn't 404 after the rename. Convenient for static-site
65
- * deploys where the landing page must be named `index.html`.
66
- * Default: false.
67
- */
68
- entryAsIndex?: boolean;
69
- }
70
-
71
- // ── Public API ─────────────────────────────────────────────────────
72
-
73
- /**
74
- * Render an entry markdown document and every reachable sibling/child
75
- * `.md` document it links to, bundled as a single ZIP with one rendered
76
- * HTML page per doc, per-document asset folders, a shared
77
- * `squisq-player.js`, and cross-doc `<a href>` references rewritten
78
- * from `.md` to `.html` inside each Doc's serialized AST.
79
- */
80
- export async function markdownDocsToHtmlBundle(options: HtmlBundleOptions): Promise<Blob> {
81
- const {
82
- entryPath,
83
- readDocument,
84
- readBinary,
85
- playerScript,
86
- title,
87
- themeId,
88
- themeRegistry,
89
- mode = 'static',
90
- maxDepth = Infinity,
91
- entryAsIndex = false,
92
- } = options;
93
-
94
- const entry = normalizePath(entryPath);
95
- if (!entry) {
96
- throw new Error('markdownDocsToHtmlBundle: entryPath is required');
97
- }
98
- const scopeRoot = posixDirname(entry);
99
- // When `entryAsIndex`, the entry doc writes to `<entryDir>/index.html`
100
- // and every cross-doc link pointing at it rewrites to that path too.
101
- // Other docs keep the `<basename>.html` convention.
102
- const entryHtmlPath = entryAsIndex
103
- ? scopeRoot
104
- ? `${scopeRoot}/index.html`
105
- : 'index.html'
106
- : entry.slice(0, -3) + '.html';
107
- const htmlPathFor = (mdPath: string): string =>
108
- mdPath === entry ? entryHtmlPath : mdPath.slice(0, -3) + '.html';
109
-
110
- const zip = new JSZip();
111
- zip.file('squisq-player.js', playerScript);
112
-
113
- const visited = new Set<string>();
114
- const queue: Array<{ path: string; depth: number }> = [{ path: entry, depth: 0 }];
115
-
116
- while (queue.length > 0) {
117
- const { path, depth } = queue.shift()!;
118
- if (visited.has(path)) continue;
119
- visited.add(path);
120
-
121
- const source = await readDocument(path);
122
- if (source === null) {
123
- throw new Error(`markdownDocsToHtmlBundle: failed to read "${path}"`);
124
- }
125
- const mdDoc = parseMarkdown(source);
126
-
127
- const docDir = posixDirname(path);
128
- const linkMap = new Map<string, string>();
129
- for (const raw of collectLinkRefs(mdDoc)) {
130
- const parsed = parseInternalRef(raw);
131
- if (!parsed) continue;
132
- const resolved = resolveRelative(docDir, parsed.path);
133
- if (resolved === null) continue;
134
- if (!isInScope(resolved, scopeRoot)) continue;
135
- if (!resolved.toLowerCase().endsWith('.md')) continue;
136
-
137
- const htmlTarget = htmlPathFor(resolved);
138
- const relHref = relativeFrom(docDir, htmlTarget) + parsed.fragment;
139
- linkMap.set(raw, relHref);
140
-
141
- if (depth + 1 <= maxDepth && !visited.has(resolved)) {
142
- queue.push({ path: resolved, depth: depth + 1 });
143
- }
144
- }
145
-
146
- // Gather and stash images per doc. Same convention as the plain
147
- // bundle: keep authored relative paths so `<img src>` paths inside
148
- // the Doc still resolve after unzip. Missing assets are non-fatal.
149
- const imageRewriteMap = new Map<string, string>();
150
- for (const authored of collectImageRefs(mdDoc)) {
151
- if (/^[a-z][a-z0-9+.-]*:/i.test(authored) || authored.startsWith('//')) continue;
152
- const clean = authored.replace(/[#?].*$/, '');
153
- const resolvedImg = resolveRelative(docDir, clean);
154
- if (resolvedImg === null) continue;
155
- const data = await readBinary(resolvedImg);
156
- if (!data) continue;
157
- const safe = sanitizeZipPath(resolvedImg);
158
- if (!safe) continue;
159
- zip.file(safe, data);
160
- imageRewriteMap.set(authored, relativeFrom(docDir, resolvedImg));
161
- }
162
-
163
- // markdownToDoc embeds the markdown AST in `block.contents`, so the
164
- // link rewrite must walk that tree on every Doc before we serialize
165
- // it into the rendered HTML.
166
- let doc = markdownToDoc(mdDoc);
167
- if (linkMap.size > 0) {
168
- doc = rewriteDocLinks(doc, linkMap);
169
- }
170
-
171
- const pageTitle = depth === 0 ? (title ?? titleForDoc(path, mdDoc)) : titleForDoc(path, mdDoc);
172
- const htmlPath = htmlPathFor(path);
173
- const playerScriptPath = relativeFrom(docDir, 'squisq-player.js');
174
-
175
- const html = generateExternalHtml(doc, {
176
- playerScriptPath,
177
- // Convert Map → plain record for the template (which expects a
178
- // record). The renderer reads this map at runtime to swap image
179
- // sources before rendering each block.
180
- imagePathMap:
181
- imageRewriteMap.size > 0 ? Object.fromEntries(imageRewriteMap.entries()) : undefined,
182
- mode,
183
- title: pageTitle,
184
- themeId,
185
- themeRegistry,
186
- });
187
- zip.file(htmlPath, html);
188
- }
189
-
190
- return zip.generateAsync({
191
- type: 'blob',
192
- compression: 'DEFLATE',
193
- compressionOptions: { level: 6 },
194
- });
195
- }
196
-
197
- // ── Link rewriting ─────────────────────────────────────────────────
198
-
199
- /**
200
- * Return a deep-cloned Doc with every `[text](url)` link URL inside
201
- * `block.contents` rewritten through `linkMap`, including raw HTML
202
- * `<a href="…">` tags carried inside `htmlBlock`/`htmlInline` nodes.
203
- * Authoring keys that aren't in the map pass through untouched.
204
- */
205
- function rewriteDocLinks(doc: Doc, linkMap: Map<string, string>): Doc {
206
- if (linkMap.size === 0) return doc;
207
-
208
- function rewriteHtmlNodes(nodes: HtmlNode[]): HtmlNode[] {
209
- return nodes.map((n) => {
210
- if (n.type !== 'htmlElement') return n;
211
- const tag = n.tagName.toLowerCase();
212
- const nextAttrs =
213
- tag === 'a' && typeof n.attributes.href === 'string' && linkMap.has(n.attributes.href)
214
- ? { ...n.attributes, href: linkMap.get(n.attributes.href)! }
215
- : n.attributes;
216
- return {
217
- ...n,
218
- attributes: nextAttrs,
219
- children: rewriteHtmlNodes(n.children),
220
- };
221
- });
222
- }
223
-
224
- function rewriteInline(node: MarkdownInlineNode): MarkdownInlineNode {
225
- if (node.type === 'link') {
226
- const nextUrl = linkMap.get(node.url) ?? node.url;
227
- return {
228
- ...node,
229
- url: nextUrl,
230
- children: node.children.map(rewriteInline),
231
- };
232
- }
233
- if (node.type === 'htmlInline') {
234
- const withChildren = node as MarkdownInlineNode & { htmlChildren?: HtmlNode[] };
235
- if (Array.isArray(withChildren.htmlChildren)) {
236
- return {
237
- ...node,
238
- htmlChildren: rewriteHtmlNodes(withChildren.htmlChildren),
239
- } as MarkdownInlineNode;
240
- }
241
- return node;
242
- }
243
- if ('children' in node && Array.isArray((node as { children?: unknown }).children)) {
244
- const children = (node as { children: MarkdownInlineNode[] }).children.map(rewriteInline);
245
- return { ...node, children } as MarkdownInlineNode;
246
- }
247
- return node;
248
- }
249
-
250
- function rewriteBlockNode(node: MarkdownBlockNode): MarkdownBlockNode {
251
- if (node.type === 'htmlBlock') {
252
- const withChildren = node as MarkdownBlockNode & { htmlChildren?: HtmlNode[] };
253
- if (Array.isArray(withChildren.htmlChildren)) {
254
- return {
255
- ...node,
256
- htmlChildren: rewriteHtmlNodes(withChildren.htmlChildren),
257
- } as MarkdownBlockNode;
258
- }
259
- return node;
260
- }
261
- if ('children' in node && Array.isArray((node as { children?: unknown }).children)) {
262
- const rewritten = (node as { children: unknown[] }).children.map((c) => {
263
- const child = c as { type?: string };
264
- if (child && typeof child === 'object' && 'type' in child) {
265
- if (isBlockNodeType(child.type as string)) {
266
- return rewriteBlockNode(c as MarkdownBlockNode);
267
- }
268
- return rewriteInline(c as MarkdownInlineNode);
269
- }
270
- return c;
271
- });
272
- return { ...node, children: rewritten } as MarkdownBlockNode;
273
- }
274
- return node;
275
- }
276
-
277
- function rewriteBlock(block: Block): Block {
278
- const next: Block = { ...block };
279
- if (Array.isArray(block.contents)) {
280
- next.contents = block.contents.map(rewriteBlockNode);
281
- }
282
- if (Array.isArray(block.children)) {
283
- next.children = block.children.map(rewriteBlock);
284
- }
285
- return next;
286
- }
287
-
288
- return {
289
- ...doc,
290
- blocks: doc.blocks.map(rewriteBlock),
291
- };
292
- }
293
-
294
- /** Block-level markdown node `type` discriminator. Inline node types are
295
- * everything else (text, link, emphasis, strong, image, etc.). Keeps
296
- * the recursive rewrite from mistakenly applying block logic to inline
297
- * link descendants. */
298
- function isBlockNodeType(t: string): boolean {
299
- return (
300
- t === 'paragraph' ||
301
- t === 'heading' ||
302
- t === 'list' ||
303
- t === 'listItem' ||
304
- t === 'blockquote' ||
305
- t === 'code' ||
306
- t === 'thematicBreak' ||
307
- t === 'table' ||
308
- t === 'tableRow' ||
309
- t === 'tableCell' ||
310
- t === 'htmlBlock'
311
- );
312
- }
313
-
314
- // ── Helpers (kept local to avoid widening plainHtmlBundle's API) ───
315
-
316
- interface ParsedRef {
317
- path: string;
318
- fragment: string;
319
- }
320
-
321
- function parseInternalRef(url: string): ParsedRef | null {
322
- if (!url) return null;
323
- if (url.startsWith('#')) return null;
324
- if (/^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith('//') || url.startsWith('/')) {
325
- return null;
326
- }
327
- const hashIdx = url.indexOf('#');
328
- const queryIdx = url.indexOf('?');
329
- const cut =
330
- hashIdx >= 0 && queryIdx >= 0 ? Math.min(hashIdx, queryIdx) : hashIdx >= 0 ? hashIdx : queryIdx;
331
- const path = cut >= 0 ? url.slice(0, cut) : url;
332
- const fragment = hashIdx >= 0 ? url.slice(hashIdx) : '';
333
- return { path, fragment };
334
- }
335
-
336
- function collectImageRefs(doc: MarkdownDocument): Set<string> {
337
- const refs = new Set<string>();
338
- function visitHtml(nodes: HtmlNode[]): void {
339
- for (const n of nodes) {
340
- if (n.type !== 'htmlElement') continue;
341
- if (n.tagName.toLowerCase() === 'img') {
342
- const src = n.attributes.src;
343
- if (typeof src === 'string' && src) refs.add(src);
344
- }
345
- visitHtml(n.children);
346
- }
347
- }
348
- function visit(node: unknown): void {
349
- if (!node || typeof node !== 'object') return;
350
- const n = node as Record<string, unknown>;
351
- if (n.type === 'image' && typeof n.url === 'string' && n.url) refs.add(n.url);
352
- if ((n.type === 'htmlBlock' || n.type === 'htmlInline') && Array.isArray(n.htmlChildren)) {
353
- visitHtml(n.htmlChildren as HtmlNode[]);
354
- }
355
- if (Array.isArray(n.children)) for (const c of n.children) visit(c);
356
- }
357
- for (const child of doc.children) visit(child);
358
- return refs;
359
- }
360
-
361
- function sanitizeZipPath(path: string): string | null {
362
- const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
363
- if (!normalized) return null;
364
- if (normalized.split('/').some((seg) => seg === '..')) return null;
365
- return normalized;
366
- }
367
-
368
- function titleForDoc(path: string, mdDoc: MarkdownDocument): string {
369
- const inferred = inferDocumentTitle(mdDoc);
370
- if (inferred) return inferred;
371
- const base = path.split('/').pop() ?? path;
372
- return base.replace(/\.md$/i, '');
373
- }