Bundle export grievances

I tried using the new bundle feature (Bundle export by laurmaedje · Pull Request #7964 · typst/typst · GitHub), but the loading of assets isn’t very convenient.

I currently use the following code to insert images on my site:

// template.typ
#let image(src, width: none, height: none, ..attrs) = {
  if (width == none and height == none) {
    html.elem("img", attrs: (src: src, ..attrs.named()))
  } else {
    if (width == none and height != none) {
      width = "auto"
    } else if (width != none and height == none) {
      height = "auto"
    }
    html.elem("img", attrs: (src: src, width: str(width), height: str(height),
      ..attrs.named()))
  }
}
// blog/index.typ
#import "template.typ"

#image("./image.png", height: 500, width: 500)

This works fine when compiling the standalone file. I use make to compile all the pages and copy the assets to the output directory.

I wanted to update my code to use the bundle feature, so I created a new file

// site.typ
#document("/blog/index.html", include("/blog/index.typ"))

And tried to modify the image function to automatically export the image.

// template.typ
#let image(src, width: none, height: none, ..attrs) = {
  asset(src, read(src, encoding: none))
  if ...
  // The rest is unchanged
}

However, this fails in multiple ways:

  1. File not found (searched at /home/…/image.png)
    read is trying to load the file with a path relative to template.typ and not blog/index.typ. This is unexpected, I think it would make more sense if the paths were resolved relative to the file being compiled, at least as an option. A better option would be to make paths their own object (being resolved at construction) rather than normal strings. I can ignore this error by using absolute paths everywhere.
  2. Error: assets are only supported in the bundle target
    Even though I invoked typst with the --format bundle option, this isn’t passed down to the individual documents.

Both limitations “make sense”, but they make it more difficult than necessary to export multi-file documents (html pages). I haven’t written it yet, but I think a workaround could be:

  1. Include metadata in the image function, instead of directly calling asset
  2. From the top level site.typ, loop through the metadata
  3. For each asset, determine the “owner” file (is that possible?) to generate the absolute path to copy.
1 Like

I haven’t tried the bundle feature seriously, but I guess the first limitation can be resolved with the new path type.

(This is my personal unofficial build of the dev documentation.)


As for the second limitation, perhaps the test case below is the designed solution. It’s quite similar to your workaround.

1 Like

Thank you, I hadn’t seen that path was available. It isn’t ideal though, since there is no API to get the path back to a string (and no way to get the original relative path back). I could however use the same technique shown in issue 7555 to pass both the relative path (as a string) and the absolute path as a virtual path object.

I have a working workaround now. Is it a bug that asset only accepts a string as its first argument?

// template.typ
#let image(src, width: auto, height: auto, path-cb: none, ..attrs) = {
  if (path-cb != none) { [#metadata((src: src, path: path-cb(src))) <asset-load>] }
  let size-attrs = if (width == auto and height == auto) {
    (:)
  } else {
    (width: width, height: height)
  }
  html.img(src: str(src), ..size-attrs, ..attrs.named())
}
// site.typ
#document("/blog/index.html", include("/blog/index.typ"))
#context {
  query(<asset-load>)
    .dedup(key: a => a.value.path)
    .map(a => asset(
        // parse the path representation to extract it as a string
        repr(a.value.path).slice(6, -2),
        read(a.value.path, encoding: none) 
    ))
    .join()
}
// blog/index.typ
#import "template.typ"

// Requires boilerplate
#let image = image.with(path-cb: x => path(x))

#image("./image.png", height: 500, width: 500)

It looks intentional, but I’m not sure…
is it a bug that asset only accepts a string as its first argument? (but not the path type) - Search | DeepWiki

I don’t know rust, but doesn’t that on the contrary show that BundlePath is constructed from a virtual path? The comment only says it cannot be constructed from a relative path.

1 Like

A VirtualPath is just an absolute-path-like string (closer so str in Typst), while a RootedPath (in Typst called just path) also contains information about a root (project vs a specific package). It does not make sense to specify a root for an output asset (there is just the one output root), hence it’s just a string.

It’s also intentional that adding assets from within documents is not currently supported. It’s something that might potentially be added down the road, but I’m not sure it’s ideal and the initial support is therefore kept minimal and extensible.

I understand that the image use case is not yet super convenient. We want to add more capabilities in this direction and just make the built-in image function also support this, but we’re not quite there yet.

As for turning a path back into a string, this is not yet supported because it’s not yet clear how to expose the root information. If you don’t care about stability, you can use the repr function, but then you have zero guarantees.

2 Likes