@uniweb/press 0.4.1 → 0.4.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/package.json +1 -1
- package/src/adapters/docx.js +22 -13
- package/src/adapters/epub.js +6 -2
- package/src/assets/fetch.js +32 -1
package/package.json
CHANGED
package/src/adapters/docx.js
CHANGED
|
@@ -129,8 +129,13 @@ export async function buildDocument(input, options = {}) {
|
|
|
129
129
|
if (footer) collectFootnotes(footer, footnotesState)
|
|
130
130
|
|
|
131
131
|
// Flatten all blocks' IR trees into one array of section children.
|
|
132
|
-
// Use async conversion for image support.
|
|
133
|
-
|
|
132
|
+
// Use async conversion for image support. The host-supplied loadAsset
|
|
133
|
+
// (when present) is threaded down to irToImageParagraph → fetchImageData
|
|
134
|
+
// → fetchAsset, so config-level paths like `assets/diagram.png` resolve
|
|
135
|
+
// to bytes via the framework's content-collector manifest the same way
|
|
136
|
+
// the typst path does.
|
|
137
|
+
const { loadAsset } = options
|
|
138
|
+
const children = await convertChildren(sections.flat(), loadAsset)
|
|
134
139
|
|
|
135
140
|
const sectionOptions = {
|
|
136
141
|
properties: {
|
|
@@ -144,7 +149,7 @@ export async function buildDocument(input, options = {}) {
|
|
|
144
149
|
}
|
|
145
150
|
|
|
146
151
|
if (header) {
|
|
147
|
-
const headerChildren = await convertChildren(header)
|
|
152
|
+
const headerChildren = await convertChildren(header, loadAsset)
|
|
148
153
|
const headerObj = new Header({ children: headerChildren })
|
|
149
154
|
const defaultHeaderObj = createDefaultHeaderFooter(true)
|
|
150
155
|
|
|
@@ -160,7 +165,7 @@ export async function buildDocument(input, options = {}) {
|
|
|
160
165
|
}
|
|
161
166
|
|
|
162
167
|
if (footer) {
|
|
163
|
-
const footerChildren = await convertChildren(footer)
|
|
168
|
+
const footerChildren = await convertChildren(footer, loadAsset)
|
|
164
169
|
const footerObj = new Footer({ children: footerChildren })
|
|
165
170
|
const defaultFooterObj = createDefaultHeaderFooter(false)
|
|
166
171
|
|
|
@@ -280,10 +285,14 @@ export function createDefaultHeaderFooter(isHeader) {
|
|
|
280
285
|
|
|
281
286
|
/**
|
|
282
287
|
* Convert an array of IR nodes to section children, handling async
|
|
283
|
-
* image fetches via Promise.all.
|
|
288
|
+
* image fetches via Promise.all. `loadAsset` (when provided) is forwarded
|
|
289
|
+
* to image-emitting paths so host-supplied byte loaders take precedence
|
|
290
|
+
* over the URL-based fetch fallback.
|
|
284
291
|
*/
|
|
285
|
-
async function convertChildren(nodes) {
|
|
286
|
-
const results = await Promise.all(
|
|
292
|
+
async function convertChildren(nodes, loadAsset) {
|
|
293
|
+
const results = await Promise.all(
|
|
294
|
+
nodes.map((node) => irToSectionChildrenAsync(node, loadAsset)),
|
|
295
|
+
)
|
|
287
296
|
return results.flat()
|
|
288
297
|
}
|
|
289
298
|
|
|
@@ -294,12 +303,12 @@ async function convertChildren(nodes) {
|
|
|
294
303
|
/**
|
|
295
304
|
* Convert an IR node into section-level docx children (async for images).
|
|
296
305
|
*/
|
|
297
|
-
async function irToSectionChildrenAsync(node) {
|
|
306
|
+
async function irToSectionChildrenAsync(node, loadAsset) {
|
|
298
307
|
switch (node.type) {
|
|
299
308
|
case 'table':
|
|
300
309
|
return [await irToTableAsync(node)]
|
|
301
310
|
case 'image':
|
|
302
|
-
return [await irToImageParagraph(node)]
|
|
311
|
+
return [await irToImageParagraph(node, loadAsset)]
|
|
303
312
|
case 'tableOfContents':
|
|
304
313
|
return [irToTableOfContents(node)]
|
|
305
314
|
case 'webOnly':
|
|
@@ -804,12 +813,12 @@ function inferImageType(src, data) {
|
|
|
804
813
|
* valid type). Do not simplify the altText spread — `name: ''` looks
|
|
805
814
|
* like a no-op but Word-for-Mac rejects the file without it.
|
|
806
815
|
*/
|
|
807
|
-
async function irToImageParagraph(node) {
|
|
816
|
+
async function irToImageParagraph(node, loadAsset) {
|
|
808
817
|
try {
|
|
809
818
|
const src = node.src || ''
|
|
810
819
|
if (!src) return new DocxParagraph({})
|
|
811
820
|
|
|
812
|
-
const imageData = await fetchImageData(src)
|
|
821
|
+
const imageData = await fetchImageData(src, loadAsset)
|
|
813
822
|
|
|
814
823
|
const width = toInt(node.transformation?.width) ?? 400
|
|
815
824
|
const height = toInt(node.transformation?.height) ?? 300
|
|
@@ -847,7 +856,7 @@ async function irToImageParagraph(node) {
|
|
|
847
856
|
* adapter use; see principle 6 ("Extract shared logic when a second
|
|
848
857
|
* adapter needs it") in docs/architecture/principles.md.
|
|
849
858
|
*/
|
|
850
|
-
async function fetchImageData(url) {
|
|
851
|
-
const { bytes } = await fetchAsset(url)
|
|
859
|
+
async function fetchImageData(url, loadAsset) {
|
|
860
|
+
const { bytes } = await fetchAsset(url, { loadAsset })
|
|
852
861
|
return bytes
|
|
853
862
|
}
|
package/src/adapters/epub.js
CHANGED
|
@@ -59,7 +59,7 @@ import { fetchAssets } from '../assets/fetch.js'
|
|
|
59
59
|
*/
|
|
60
60
|
export async function compileEpub(input, options = {}) {
|
|
61
61
|
const { sections = [], metadata = null } = input || {}
|
|
62
|
-
const { meta = {}, stylesheet, identifier, cover } = options
|
|
62
|
+
const { meta = {}, stylesheet, identifier, cover, loadAsset } = options
|
|
63
63
|
|
|
64
64
|
const resolvedMeta = { ...(metadata || {}), ...meta }
|
|
65
65
|
const css = stylesheet || DEFAULT_STYLESHEET
|
|
@@ -74,13 +74,17 @@ export async function compileEpub(input, options = {}) {
|
|
|
74
74
|
// Deduplicate image URLs across all chapters — and the cover, if any —
|
|
75
75
|
// and fetch them in parallel. Failed fetches are logged and the original
|
|
76
76
|
// URL is left in place; the EPUB still opens but may show broken images.
|
|
77
|
+
// The host-supplied `loadAsset` (when present) is tried before fetch —
|
|
78
|
+
// browser hosts wrap fetch; Node hosts (unipress) wrap node:fs so config-
|
|
79
|
+
// level paths like `assets/cover.jpg` resolve to bytes via the manifest
|
|
80
|
+
// the framework's content collector populated.
|
|
77
81
|
const imageUrls = new Set()
|
|
78
82
|
for (const ch of chapters) {
|
|
79
83
|
for (const url of ch.images) imageUrls.add(url)
|
|
80
84
|
}
|
|
81
85
|
if (coverUrl) imageUrls.add(coverUrl)
|
|
82
86
|
const fetched = imageUrls.size
|
|
83
|
-
? await fetchAssets(imageUrls)
|
|
87
|
+
? await fetchAssets(imageUrls, { loadAsset })
|
|
84
88
|
: new Map()
|
|
85
89
|
|
|
86
90
|
// Assign a manifest-relative path to every successfully-fetched image,
|
package/src/assets/fetch.js
CHANGED
|
@@ -22,16 +22,47 @@
|
|
|
22
22
|
/**
|
|
23
23
|
* Fetch a single asset URL and return normalized bytes + metadata.
|
|
24
24
|
*
|
|
25
|
+
* Resolution order:
|
|
26
|
+
* 1. `options.loadAsset(url)` — host-supplied byte loader. Browser hosts
|
|
27
|
+
* typically wrap `fetch`; Node hosts (unipress) wrap `node:fs`. When
|
|
28
|
+
* present, tried first; on throw or null/undefined return, falls
|
|
29
|
+
* through to `fetch`. This is how foundations stay environment-
|
|
30
|
+
* agnostic — they declare `book.covers.front: assets/front.jpg` and
|
|
31
|
+
* the host's loadAsset turns that into bytes the same way the host
|
|
32
|
+
* handles every other config-level asset.
|
|
33
|
+
* 2. `fetch()` (or `options.fetch` for tests) — the legacy URL-based
|
|
34
|
+
* path. Required for absolute http(s) URLs and as a fallback for
|
|
35
|
+
* anything `loadAsset` can't handle.
|
|
36
|
+
*
|
|
25
37
|
* @param {string} url
|
|
26
38
|
* @param {Object} [options]
|
|
39
|
+
* @param {(src: string) => Promise<Uint8Array | null>} [options.loadAsset]
|
|
40
|
+
* - host-supplied byte loader. See above.
|
|
27
41
|
* @param {typeof fetch} [options.fetch] - override for tests / node-fetch.
|
|
28
42
|
* @returns {Promise<{ bytes: Uint8Array, mime: string, hash: string, ext: string }>}
|
|
29
43
|
*/
|
|
30
44
|
export async function fetchAsset(url, options = {}) {
|
|
45
|
+
const { loadAsset } = options
|
|
46
|
+
|
|
47
|
+
if (typeof loadAsset === 'function') {
|
|
48
|
+
try {
|
|
49
|
+
const bytes = await loadAsset(url)
|
|
50
|
+
if (bytes && bytes.length > 0) {
|
|
51
|
+
const mime = detectMime(url, bytes)
|
|
52
|
+
const ext = extForMime(mime) || extFromUrl(url) || 'bin'
|
|
53
|
+
const hash = await hashContent(bytes)
|
|
54
|
+
return { bytes, mime, hash, ext }
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Fall through to fetch — loadAsset may not handle every URL
|
|
58
|
+
// shape (e.g., absolute http(s) URLs in a Node host).
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
31
62
|
const fetchImpl = options.fetch || globalThis.fetch
|
|
32
63
|
if (typeof fetchImpl !== 'function') {
|
|
33
64
|
throw new Error(
|
|
34
|
-
'fetchAsset: no fetch() available. Pass options.fetch or run in an environment that provides globalThis.fetch.',
|
|
65
|
+
'fetchAsset: no fetch() available. Pass options.fetch / options.loadAsset or run in an environment that provides globalThis.fetch.',
|
|
35
66
|
)
|
|
36
67
|
}
|
|
37
68
|
const res = await fetchImpl(url)
|