@uniweb/press 0.4.7 → 0.4.9
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 +3 -3
- package/src/adapters/docx.js +8 -2
- package/src/adapters/latex.js +13 -3
- package/src/adapters/typst.js +15 -4
- package/src/assets/inline-images.js +84 -0
- package/src/compileRegistrations.js +5 -0
- package/src/ir/parser.js +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/press",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
4
4
|
"description": "Press your React components into downloadable files — Word, Excel, PDF. Registration-based pattern with lazy-loaded format adapters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"parse5": "^7.2.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"react": "^
|
|
50
|
-
"react-dom": "^
|
|
49
|
+
"react": "^19.0.0",
|
|
50
|
+
"react-dom": "^19.0.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@testing-library/react": "^16.0.0",
|
package/src/adapters/docx.js
CHANGED
|
@@ -510,6 +510,9 @@ function irToInlineChildren(node) {
|
|
|
510
510
|
// a TextRun so the OOXML parent is <w:r>.
|
|
511
511
|
return [new TextRun({ children: [new DocxTab()] })]
|
|
512
512
|
case 'externalHyperlink':
|
|
513
|
+
// A plain <a href> from same-source JSX — same destination, same
|
|
514
|
+
// emitter. See the href note in ir/parser.js.
|
|
515
|
+
case 'a':
|
|
513
516
|
return [irToExternalHyperlink(node)]
|
|
514
517
|
case 'internalHyperlink':
|
|
515
518
|
return [irToInternalHyperlink(node)]
|
|
@@ -612,10 +615,13 @@ function irToTextRunPair(node) {
|
|
|
612
615
|
}
|
|
613
616
|
|
|
614
617
|
function irToExternalHyperlink(node) {
|
|
618
|
+
// `link` is the data-* spelling (data-link); `href` arrives from a plain
|
|
619
|
+
// <a href>. Same destination either way.
|
|
620
|
+
const destination = node.link || node.href || ''
|
|
615
621
|
const children = (node.children || []).flatMap(irToInlineChildren)
|
|
616
622
|
return new ExternalHyperlink({
|
|
617
|
-
children: children.length ? children : [new TextRun({ text:
|
|
618
|
-
link:
|
|
623
|
+
children: children.length ? children : [new TextRun({ text: destination })],
|
|
624
|
+
link: destination,
|
|
619
625
|
})
|
|
620
626
|
}
|
|
621
627
|
|
package/src/adapters/latex.js
CHANGED
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
*/
|
|
48
48
|
|
|
49
49
|
import JSZip from 'jszip'
|
|
50
|
+
import { bundleInlineImages } from '../assets/inline-images.js'
|
|
50
51
|
import { RAW_BEGIN, RAW_END, markRawLatex } from '../latex/raw.js'
|
|
51
52
|
|
|
52
53
|
// Re-export so consumers reaching the adapter via the dynamic-import
|
|
@@ -84,9 +85,18 @@ export { markRawLatex }
|
|
|
84
85
|
* @returns {Promise<Blob>}
|
|
85
86
|
*/
|
|
86
87
|
export async function compileLatex(input, options = {}) {
|
|
87
|
-
const { mode = 'sources', ...rest } = options
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
const { mode = 'sources', loadAsset, ...rest } = options
|
|
89
|
+
|
|
90
|
+
// Fetch inline body images and rewrite their IR src to a bundle-local
|
|
91
|
+
// assets/<hash>.<ext> path, then merge the bytes into the bundle so
|
|
92
|
+
// latexmk/pdflatex resolves \includegraphics{...} against the extracted
|
|
93
|
+
// sources. Cover art arrives pre-bundled in rest.assets via the
|
|
94
|
+
// foundation's getOptions; this handles document-body images.
|
|
95
|
+
const inlineImageAssets = await bundleInlineImages(input, loadAsset)
|
|
96
|
+
const bundle = buildBundle(input, {
|
|
97
|
+
...rest,
|
|
98
|
+
assets: { ...(rest.assets || {}), ...inlineImageAssets },
|
|
99
|
+
})
|
|
90
100
|
|
|
91
101
|
if (mode === 'sources') {
|
|
92
102
|
return zipBundle(bundle)
|
package/src/adapters/typst.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import JSZip from 'jszip'
|
|
31
|
+
import { bundleInlineImages } from '../assets/inline-images.js'
|
|
31
32
|
|
|
32
33
|
// ============================================================================
|
|
33
34
|
// Public API
|
|
@@ -53,9 +54,18 @@ import JSZip from 'jszip'
|
|
|
53
54
|
* @returns {Promise<Blob>}
|
|
54
55
|
*/
|
|
55
56
|
export async function compileTypst(input, options = {}) {
|
|
56
|
-
const { mode = 'sources', ...rest } = options
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
const { mode = 'sources', loadAsset, ...rest } = options
|
|
58
|
+
|
|
59
|
+
// Fetch inline body images (``) and rewrite their IR src to a
|
|
60
|
+
// bundle-local `assets/<hash>.<ext>` path, then merge the bytes into the
|
|
61
|
+
// bundle so `typst compile` resolves them. Cover images arrive pre-bundled
|
|
62
|
+
// in rest.assets via the foundation's getOptions; this handles the images
|
|
63
|
+
// an author drops into the document body.
|
|
64
|
+
const inlineImageAssets = await bundleInlineImages(input, loadAsset)
|
|
65
|
+
const bundle = buildBundle(input, {
|
|
66
|
+
...rest,
|
|
67
|
+
assets: { ...(rest.assets || {}), ...inlineImageAssets },
|
|
68
|
+
})
|
|
59
69
|
|
|
60
70
|
if (mode === 'sources') {
|
|
61
71
|
return zipBundle(bundle)
|
|
@@ -364,6 +374,7 @@ function blockNodeToTypst(node) {
|
|
|
364
374
|
return inlineNodeToTypst(node)
|
|
365
375
|
|
|
366
376
|
case 'link':
|
|
377
|
+
case 'a':
|
|
367
378
|
return inlineNodeToTypst(node)
|
|
368
379
|
|
|
369
380
|
case 'math':
|
|
@@ -516,7 +527,7 @@ function inlineNodeToTypst(node) {
|
|
|
516
527
|
return text
|
|
517
528
|
}
|
|
518
529
|
|
|
519
|
-
if (node.type === 'link') {
|
|
530
|
+
if (node.type === 'link' || node.type === 'a') {
|
|
520
531
|
const href = node.href || ''
|
|
521
532
|
const inner = inlineChildrenToTypst(node.children || [])
|
|
522
533
|
return `#link(${quoteTypstString(href)})[${inner}]`
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline image bundling for source-bundle adapters (typst, latex).
|
|
3
|
+
*
|
|
4
|
+
* A source-bundle adapter emits an image reference (`#image(src)`,
|
|
5
|
+
* `\includegraphics{src}`) into a text file and ships a directory of assets
|
|
6
|
+
* alongside it. The IR carries only the author's `src` string, so the bytes
|
|
7
|
+
* have to be fetched and written into the bundle — and the src rewritten to the
|
|
8
|
+
* bundle-local path — before `typst` / `latex` can resolve them. Cover images
|
|
9
|
+
* arrive pre-bundled via the foundation's getOptions; this handles the images
|
|
10
|
+
* an author drops into the document body.
|
|
11
|
+
*
|
|
12
|
+
* Extracted from the typst adapter when latex became the second bundle adapter
|
|
13
|
+
* needing it — principle 6 in docs/architecture/principles.md, the same reason
|
|
14
|
+
* src/assets/fetch.js exists. EPUB does its own image embedding (it needs OPF
|
|
15
|
+
* manifest entries and a different on-disk layout), so it doesn't share this.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { fetchAssets } from './fetch.js'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fetch every inline image referenced in the IR and rewrite its node `src` to a
|
|
22
|
+
* bundle-local `assets/<hash>.<ext>` path, returning the map of bundle files to
|
|
23
|
+
* merge into the source bundle.
|
|
24
|
+
*
|
|
25
|
+
* The format walker emits the (rewritten) src verbatim, so the compiler
|
|
26
|
+
* resolves it against the extracted bundle directory. Byte-loading goes through
|
|
27
|
+
* the shared fetchAssets helper: the host-supplied loadAsset (node:fs in
|
|
28
|
+
* unipress, fetch in the browser) turns a resolved src into bytes. Images that
|
|
29
|
+
* fail to load are left untouched so one missing asset doesn't abort the whole
|
|
30
|
+
* document.
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} input - compileOutputs output ({ sections, header, footer }).
|
|
33
|
+
* @param {(src: string) => Promise<Uint8Array|null>} [loadAsset]
|
|
34
|
+
* @returns {Promise<Record<string, Uint8Array>>} bundlePath → bytes
|
|
35
|
+
*/
|
|
36
|
+
export async function bundleInlineImages(input, loadAsset) {
|
|
37
|
+
const imageNodes = collectImageNodes(input)
|
|
38
|
+
if (!imageNodes.length) return {}
|
|
39
|
+
|
|
40
|
+
const srcs = [...new Set(imageNodes.map((n) => n.src).filter(Boolean))]
|
|
41
|
+
if (!srcs.length) return {}
|
|
42
|
+
|
|
43
|
+
const fetched = await fetchAssets(srcs, { loadAsset })
|
|
44
|
+
|
|
45
|
+
const assets = {}
|
|
46
|
+
const rewrite = new Map()
|
|
47
|
+
for (const [src, result] of fetched) {
|
|
48
|
+
if (!result || result.error || !result.bytes) continue
|
|
49
|
+
const bundlePath = `assets/${result.hash}.${result.ext}`
|
|
50
|
+
assets[bundlePath] = result.bytes
|
|
51
|
+
rewrite.set(src, bundlePath)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Rewrite the IR in place so the walker emits the bundle path. The IR is
|
|
55
|
+
// freshly built by compileOutputs for this compile, so mutation is safe and
|
|
56
|
+
// avoids threading a rewrite map through the whole walker.
|
|
57
|
+
for (const node of imageNodes) {
|
|
58
|
+
const bundlePath = rewrite.get(node.src)
|
|
59
|
+
if (bundlePath) node.src = bundlePath
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return assets
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Collect every image IR node reachable from the compiled input, descending
|
|
67
|
+
* through `children` (paragraphs, lists, tables, figures, …) across sections,
|
|
68
|
+
* header, and footer.
|
|
69
|
+
*/
|
|
70
|
+
export function collectImageNodes(input) {
|
|
71
|
+
const found = []
|
|
72
|
+
const visit = (nodes) => {
|
|
73
|
+
if (!Array.isArray(nodes)) return
|
|
74
|
+
for (const node of nodes) {
|
|
75
|
+
if (!node || typeof node !== 'object') continue
|
|
76
|
+
if (node.type === 'image') found.push(node)
|
|
77
|
+
if (Array.isArray(node.children)) visit(node.children)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const section of input?.sections || []) visit(section)
|
|
81
|
+
visit(input?.header || [])
|
|
82
|
+
visit(input?.footer || [])
|
|
83
|
+
return found
|
|
84
|
+
}
|
|
@@ -276,6 +276,11 @@ export async function compileDocument(treeOrWebsite, options = {}) {
|
|
|
276
276
|
: {}
|
|
277
277
|
|
|
278
278
|
const mergedAdapterOptions = {
|
|
279
|
+
// Thread the host loadAsset through to the adapter so it can embed
|
|
280
|
+
// inline body images (the typst/latex bundlers fetch bytes here, the
|
|
281
|
+
// same capability the docx adapter uses). A foundation's getOptions or
|
|
282
|
+
// the caller can still override it via adapterOptions.
|
|
283
|
+
loadAsset,
|
|
279
284
|
...foundationAdapterOptions.adapterOptions,
|
|
280
285
|
...overrideAdapterOptions,
|
|
281
286
|
}
|
package/src/ir/parser.js
CHANGED
|
@@ -103,6 +103,22 @@ function nodeToIR(node) {
|
|
|
103
103
|
const properties = attributesToProperties(node.attrs || [])
|
|
104
104
|
const children = collectChildren(node)
|
|
105
105
|
|
|
106
|
+
// A plain <a href> carries its destination in a normal HTML attribute, not
|
|
107
|
+
// a data-* one, so attributesToProperties drops it. Foundations reach this
|
|
108
|
+
// shape by writing an ordinary anchor in JSX that serves both the browser
|
|
109
|
+
// preview and the compile input — the same-source property principles.md
|
|
110
|
+
// §8 asks us to pursue. Without the href the adapters still emit the link
|
|
111
|
+
// TEXT and silently lose the destination, which is precisely the
|
|
112
|
+
// preview/output drift that principle exists to eliminate.
|
|
113
|
+
//
|
|
114
|
+
// Only `href` is lifted, and only for an anchor: it is the one plain
|
|
115
|
+
// attribute whose document meaning is unambiguous. The IR stays data-*
|
|
116
|
+
// driven everywhere else.
|
|
117
|
+
if (type === 'a' && properties.href === undefined) {
|
|
118
|
+
const href = getAttr(node, 'href')
|
|
119
|
+
if (href) properties.href = href
|
|
120
|
+
}
|
|
121
|
+
|
|
106
122
|
const obj = { type, ...properties }
|
|
107
123
|
|
|
108
124
|
if (type === 'text') {
|