@uniweb/press 0.4.6 → 0.4.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/press",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
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,13 +46,13 @@
46
46
  "parse5": "^7.2.0"
47
47
  },
48
48
  "peerDependencies": {
49
- "react": "^18.0.0 || ^19.0.0",
50
- "react-dom": "^18.0.0 || ^19.0.0"
49
+ "react": "^19.0.0",
50
+ "react-dom": "^19.0.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@testing-library/react": "^16.0.0",
54
54
  "jsdom": "^25.0.0",
55
- "vitest": "^2.0.0"
55
+ "vitest": "^4.1.7"
56
56
  },
57
57
  "scripts": {
58
58
  "test": "vitest run",
@@ -3,7 +3,7 @@
3
3
  * that builders should consult when foundations don't pass explicit
4
4
  * literal values.
5
5
  *
6
- * Stage 5 of kb/framework/plans/press-professional-docx.md.
6
+ * Stage 5 of the print/docx work.
7
7
  *
8
8
  * The shape is intentionally small and forgiving: a foundation passes
9
9
  * whatever subset of keys it cares about, and builders fall back to
@@ -534,8 +534,8 @@ function irToInlineChildren(node) {
534
534
  // v1 fallback: emit the LaTeX source as plain text. Better
535
535
  // than the MathML soup that was leaking before this case
536
536
  // existed (`<mo>…</mo>` operator names appearing as letters
537
- // in the document). A faithful OMML emitter is tracked in
538
- // kb/framework/plans/math-print-adapters.md §"Future work".
537
+ // in the document). A faithful OMML emitter is tracked as
538
+ // future work.
539
539
  return [new TextRun({ text: node.latex || '' })]
540
540
  }
541
541
  default:
@@ -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
- const bundle = buildBundle(input, rest)
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)
@@ -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
- const bundle = buildBundle(input, rest)
57
+ const { mode = 'sources', loadAsset, ...rest } = options
58
+
59
+ // Fetch inline body images (`![](foo.png)`) 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)
@@ -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
  }
@@ -2,7 +2,7 @@
2
2
  * BrandLogo — a brand image suitable for page headers, cover blocks,
3
3
  * and watermarks.
4
4
  *
5
- * Stage 6.4 of kb/framework/plans/press-professional-docx.md.
5
+ * Stage 6.4 of the print/docx work.
6
6
  *
7
7
  * Emits a bare `<img data-type="image">` (no Paragraph wrapper) so
8
8
  * the IR walker keeps the image at section level — Press's docx
package/src/docx/Math.jsx CHANGED
@@ -3,7 +3,7 @@
3
3
  * walker turns into a `math` IR node. The docx adapter's v1 fallback
4
4
  * emits the LaTeX source as plain text (better than the mathml soup
5
5
  * it was leaking before this builder existed); a faithful OMML emitter
6
- * is tracked in kb/framework/plans/math-print-adapters.md §"Future work".
6
+ * is tracked as future work.
7
7
  *
8
8
  * Inline and block math share the same builder, distinguished by the
9
9
  * `display` flag. The optional `id` flows through for parity with the
@@ -2,7 +2,7 @@
2
2
  * Page-chrome builders — page header, page footer, and page-number
3
3
  * field codes.
4
4
  *
5
- * Stage 4 of kb/framework/plans/press-professional-docx.md.
5
+ * Stage 4 of the print/docx work.
6
6
  *
7
7
  * The underlying registration plumbing already exists in Press:
8
8
  * `useDocumentOutput(block, 'docx', body, { role: 'header' })` lifts
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @uniweb/press/format — locale-aware text formatters.
3
3
  *
4
- * Stage 6.1 of kb/framework/plans/press-professional-docx.md.
4
+ * Stage 6.1 of the print/docx work.
5
5
  *
6
6
  * The same JSX feeds the React preview and the docx fragment, so
7
7
  * these are React components that emit <span data-type="text"> and
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Synthesize OOXML style definitions from a Press theme.
3
3
  *
4
- * Stage 6.0 of kb/framework/plans/press-professional-docx.md.
4
+ * Stage 6.0 of the print/docx work.
5
5
  *
6
6
  * The theme.typography registry is the foundation-facing API:
7
7
  *