quarto-dev / quarto-cli

Open-source scientific and technical publishing system built on Pandoc.
https://quarto.org
Other
3.7k stars 300 forks source link

Document with HTML-interactive dependencies err when `format: typst` #7276

Closed sda030 closed 10 months ago

sda030 commented 10 months ago

Bug description

I know typst is experimental but maybe something to consider: One might want to set up a website for a report, split into interactive HTML-chapters but also providing a download link to a Typst-generated PDF of the entire thing. Using conditional divs, one can e.g. use interactive graphs (ggiraph) for the HTML-part, and some prettier static graphs for the PDF. So far fine. However, some of the JS dependencies (inherited from the HTML-part) are currently being included in the typ-file, resulting in errors. In my case, I get the errors below (Actual Behavior). If I delete the corresponding 8 lines in the 2022H.typ file and run with quarto typst compile 2022H.typ, it compiles beautifully, as expected (Expected Behavior)! Could we avoid placing these dependencies in the typ-file, or at least have an option to exclude them? Due to a more complicated setup with a typst-template, the manual deletion and post-hoc compilation is not a viable workaround for us.

Steps to reproduce

First of all apologies for not having an actually reproducible example, and in Norwegian... If needed I can improve it at a later time.

---
format: 
  typst: default
  html: default
---
# Svarprosent og deltakere

::: {.content-visible when-format="html"}
```{r}
#| label: 'fig-html_Svarprosent_uni_cat_prop_plot_673'
#| fig-cap: '_Svarprosent_  (N = 342) [xlsx](1_Svarprosent_og_deltakere/uni_cat_prop_plot/svarprosent.xlsx)'
Svarprosent_uni_cat_prop_plot <- 
  qs::qread("1_Svarprosent_og_deltakere/uni_cat_prop_plot/svarprosent.rds")
ggiraph::girafe(ggobj = Svarprosent_uni_cat_prop_plot)

:::

::: {.content-visible unless-format="html"}

#| label: 'fig-pdf_Svarprosent_uni_cat_prop_plot_524'
#| fig-cap: '_Svarprosent_  (N &equals; 342) [xlsx](1_Svarprosent_og_deltakere/uni_cat_prop_plot/svarprosent.xlsx)'
Svarprosent_uni_cat_prop_plot <- 
  qs::qread("1_Svarprosent_og_deltakere/uni_cat_prop_plot/svarprosent.rds")
Svarprosent_uni_cat_prop_plot

:::


### Expected behavior

2022H.typ:

````typ
// Some definitions presupposed by pandoc's typst output.
#let blockquote(body) = [
  #set text( size: 0.92em )
  #block(inset: (left: 1.5em, top: 0.2em, bottom: 0.2em))[#body]
]

#let horizontalrule = [
  #line(start: (25%,0%), end: (75%,0%))
]

#let endnote(num, contents) = [
  #stack(dir: ltr, spacing: 3pt, super[#num], contents)
]

#show terms: it => {
  it.children
    .map(child => [
      #strong[#child.term]
      #block(inset: (left: 1.5em, top: -0.4em))[#child.description]
      ])
    .join()
}

// Some quarto-specific definitions.

#show raw: it => {
  if it.block {
    block(fill: luma(230), width: 100%, inset: 8pt, radius: 2pt, it)
  } else {
    it
  }
}

#show ref: it => locate(loc => {
  let target = query(it.target, loc).first()
  if it.at("supplement", default: none) == none {
    it
    return
  }

  let sup = it.supplement.text.matches(regex("^45127368-afa1-446a-820f-fc64c546b2c5%(.*)")).at(0, default: none)
  if sup != none {
    let parent_id = sup.captures.first()
    let parent_figure = query(label(parent_id), loc).first()
    let parent_location = parent_figure.location()

    let counters = numbering(
      parent_figure.at("numbering"), 
      ..parent_figure.at("counter").at(parent_location))

    let subcounter = numbering(
      target.at("numbering"),
      ..target.at("counter").at(target.location()))

    // NOTE there's a nonbreaking space in the block below
    link(target.location(), [#parent_figure.at("supplement") #counters#subcounter])
  } else {
    it
  }
})

#let article(
  title: none,
  authors: none,
  date: none,
  abstract: none,
  cols: 1,
  margin: (x: 1.25in, y: 1.25in),
  paper: "us-letter",
  lang: "en",
  region: "US",
  font: (),
  fontsize: 11pt,
  sectionnumbering: none,
  toc: false,
  doc,
) = {
  set page(
    paper: paper,
    margin: margin,
    numbering: "1",
  )
  set par(justify: true)
  set text(lang: lang,
           region: region,
           font: font,
           size: fontsize)
  set heading(numbering: sectionnumbering)

  if title != none {
    align(center)[#block(inset: 2em)[
      #text(weight: "bold", size: 1.5em)[#title]
    ]]
  }

  if authors != none {
    let count = authors.len()
    let ncols = calc.min(count, 3)
    grid(
      columns: (1fr,) * ncols,
      row-gutter: 1.5em,
      ..authors.map(author =>
          align(center)[
            #author.name \
            #author.affiliation \
            #author.email
          ]
      )
    )
  }

  if date != none {
    align(center)[#block(inset: 1em)[
      #date
    ]]
  }

  if abstract != none {
    block(inset: 2em)[
    #text(weight: "semibold")[Abstract] #h(1em) #abstract
    ]
  }

  if toc {
    block(above: 0em, below: 2em)[
    #outline(
      title: auto,
      depth: none
    );
    ]
  }

  if cols == 1 {
    doc
  } else {
    columns(cols, doc)
  }
}
#show: doc => article(
  title: [2022H],
  date: [22.06.2023],
  lang: "nb",
  sectionnumbering: "1.1.a",
  toc: true,
  cols: 1,
  doc,
)

= Svarprosent og deltakere
#block[
#block[
#figure([
#image("2022H_files/figure-typst/fig-pdf_Svarprosent_uni_cat_prop_plot_524-1.svg")
], caption: figure.caption(
position: bottom, 
[
#emph[Svarprosent] (N \= 342) #link("1_Svarprosent_og_deltakere/uni_cat_prop_plot/svarprosent.xlsx")[xlsx]
]), 
kind: "quarto-float-fig", 
supplement: "Figure", 
numbering: "1", 
)
<fig-pdf_Svarprosent_uni_cat_prop_plot_524>

]
]

Actual behavior

[typst]: Compiling 2022H.typ to 2022H.pdf...error: unclosed label
    ┌─ \\?\C:\Users\py128\AppData\Local\Temp\Saros\21149_DU\55_fullførte_utkast\Rapporter\Barnehageleder\2022H\2022H.typ:149:0
    │
149 │ <script src="../../../site_libs/htmlwidgets-1.6.2/htmlwidgets.js"></script>
    │ ^^^^^^^

error: unclosed label
    ┌─ \\?\C:\Users\py128\AppData\Local\Temp\Saros\21149_DU\55_fullførte_utkast\Rapporter\Barnehageleder\2022H\2022H.typ:150:0
    │
150 │ <script src="../../../site_libs/d3-bundle-5.16.0/d3-bundle.min.js"></script>
    │ ^^^^^^^

error: unclosed label
    ┌─ \\?\C:\Users\py128\AppData\Local\Temp\Saros\21149_DU\55_fullførte_utkast\Rapporter\Barnehageleder\2022H\2022H.typ:151:0
    │
151 │ <script src="../../../site_libs/d3-lasso-0.0.5/d3-lasso.min.js"></script>
    │ ^^^^^^^

error: unclosed label
    ┌─ \\?\C:\Users\py128\AppData\Local\Temp\Saros\21149_DU\55_fullførte_utkast\Rapporter\Barnehageleder\2022H\2022H.typ:152:0
    │
152 │ <script src="../../../site_libs/save-svg-as-png-1.4.17/save-svg-as-png.min.js"></script>
    │ ^^^^^^^

error: unclosed label
    ┌─ \\?\C:\Users\py128\AppData\Local\Temp\Saros\21149_DU\55_fullførte_utkast\Rapporter\Barnehageleder\2022H\2022H.typ:153:0
    │
153 │ <script src="../../../site_libs/flatbush-4.0.0/flatbush.min.js"></script>
    │ ^^^^^^^

error: unclosed label
    ┌─ \\?\C:\Users\py128\AppData\Local\Temp\Saros\21149_DU\55_fullførte_utkast\Rapporter\Barnehageleder\2022H\2022H.typ:154:0
    │
154 │ <link href="../../../site_libs/ggiraphjs-0.4.6/ggiraphjs.min.css" rel="stylesheet" />
    │ ^^^^^

error: unclosed label
    ┌─ \\?\C:\Users\py128\AppData\Local\Temp\Saros\21149_DU\55_fullførte_utkast\Rapporter\Barnehageleder\2022H\2022H.typ:155:0
    │
155 │ <script src="../../../site_libs/ggiraphjs-0.4.6/ggiraphjs.min.js"></script>
    │ ^^^^^^^

error: unclosed label
    ┌─ \\?\C:\Users\py128\AppData\Local\Temp\Saros\21149_DU\55_fullførte_utkast\Rapporter\Barnehageleder\2022H\2022H.typ:156:0
    │
156 │ <script src="../../../site_libs/girafe-binding-0.8.7/girafe.js"></script>
    │ ^^^^^^^

Your environment

Quarto check output

Quarto 1.4.376
[>] Checking versions of quarto binary dependencies...
      Pandoc version 3.1.8: OK
      Dart Sass version 1.55.0: OK
      Deno version 1.33.4: OK
[>] Checking versions of quarto dependencies......OK
[>] Checking Quarto installation......OK
      Version: 1.4.376
      Path: C:\Program Files\Quarto\bin
      CodePage: 1252

[>] Checking tools....................OK
      TinyTeX: v2023.09
      Chromium: (not installed)

[>] Checking LaTeX....................OK
      Using: TinyTex
      Path: C:\Users\py128\AppData\Roaming\TinyTeX\bin\windows\
      Version: 2023

[>] Checking basic markdown render....OK

[>] Checking Python 3 installation....OK
      Version: 3.11.4
      Path: C:/Users/py128/AppData/Local/Programs/Python/Python311/python.exe
      Jupyter: (None)

      Jupyter is not available in this Python installation.
      Install with py -m pip install jupyter

[>] Checking R installation...........OK
      Version: 4.3.1
      Path: C:/PROGRA~1/R/R-43~1.1
      LibPaths:
        - C:/Users/py128/AppData/Local/R/win-library
        - C:/Program Files/R/R-4.3.1/library
      knitr: 1.44
      rmarkdown: 2.25

[>] Checking Knitr engine render......OK
mcanouil commented 10 months ago

We can't use your example as it is not self contained. Please provide a complete reproducible example with low or no dependencies.

You can share a Quarto document using the following syntax, i.e., using more backticks than you have in your document (usually four ````).

````qmd
---
title: "Reproducible Quarto Document"
format: html
---

This is a reproducible Quarto document using `format: html`.
It is written in Markdown and contains embedded R code.
When you run the code, it will produce a plot.

```{r}
plot(cars)

The end.

cscheid commented 10 months ago

I guess that what's happening here is that the dependency declared by ggiraph::girafe(ggobj = Svarprosent_uni_cat_prop_plot) is not format-dependent (it's probably just using knitr to add something to the header, independently of the format).

A good way to confirm this is to try the same thing you're doing but in .pdf format, and see if the latex compilation similarly fails.

Unfortunately, that's not something we can easily fix, because that that block is getting executed before the conditional content evaluation throws it away. I think you should consider using two separate files, one for each format, for the time being.

mcanouil commented 10 months ago

Or use knitr conditional to avoid ggiraph from injecting its dependencies.

::: {.content-visible when-format="html"}
```{r}
if (knitr::is_html_output()) {
  ggiraph::girafe(...)
}

:::


EDIT:

````qmd
---
format: 
  typst: default
  html: default
knitr:
  opts_chunk: 
    dev: png
---

```{r}
#| echo: false
library(ggplot2)
library(ggiraph)
dataset <- mtcars
dataset[["carname"]] <- rownames(mtcars)

gg_base <- ggplot(data = dataset) +
  aes(x = wt, y = qsec, colour = disp) +
  theme_minimal()

::: {.content-visible when-format="html"}

#| label: fig-interactive
#| fig-cap: "Interactive figure"
if (knitr::is_html_output()) {
  ggi <- ggiraph::girafe(
    ggobj = gg_base +
      aes(tooltip = carname, data_id = carname) +
      geom_point_interactive()
  )
  ggi
}

:::

::: {.content-visible unless-format="html"}

#| label: fig-static
#| fig-cap: "Static figure"
gg_base + geom_point()

:::

sda030 commented 10 months ago

Sorry for the poor reprex. Also, I found the real culprit: For other reasons I needed prefer-html: true. Flipping this to false renders without problems. I guess this connects to what you suggested @mcanouil regarding knitr conditional?

---
format: 
  typst: default
  html: default
prefer-html: true
---

# Test

::: {.content-visible when-format="html"}
```{r}
x <- ggplot2::ggplot(data = mtcars, ggplot2::aes(x=hp, y=mpg)) + ggiraph::geom_point_interactive()
ggiraph::girafe(ggobj = x)

:::

::: {.content-visible unless-format="html"}

x <- 
  ggplot2::ggplot(data = mtcars, ggplot2::aes(x=hp, y=mpg)) + ggplot2::geom_point()
x

:::

cderv commented 10 months ago

For other reasons I needed prefer-html: true. Flipping this to false renders without problems.

prefer-html is an option that should be rarely needed. We need to use it internal for some specific format (like Hugo Markdown which supports HTML in markdown). However, most of the time it should not be needed.

When providing HTML only content like HTML widgets, knitr should handle the format requirement by taking a screenshot of the rendered HTML widgets to insert as an image. Using prefer-html: true will deactivate this.

It seems using prefer-html with typst does include the dependencies where it shouldn't really. Maybe this is a bug to handle in our knitr related code...

Anyhow, based on that, with prefer-html: false, using latest Quarto 1.4 and latest knitr with webshot2 or webshot available, this should work without conditional

---
format: 
  typst: 
    keep-typ: true
  html: default
---

# This is the plot 

```{r}
x <- ggplot2::ggplot(data = mtcars, ggplot2::aes(x=hp, y=mpg)) + ggiraph::geom_point_interactive()
x

This is interactive version

It will be a screenshot when format is typst

ggiraph::girafe(ggobj = x)

However, you may not want a screenshot for typst as you can insert image ggplot directly without the interactive. 
In that case, you indeed need to use conditional. IMO best would be 
````markdown
---
format: 
  typst: default
  html: default
---

```{r}
#| include: false
show_graph <- function(x) {
  if (knitr::is_html_output()) return(ggiraph::girafe(ggobj = x))
  x
}

This is the plot

#| echo: false
x <- ggplot2::ggplot(data = mtcars, ggplot2::aes(x=hp, y=mpg)) + ggiraph::geom_point_interactive()
show_graph(x)
mcanouil commented 10 months ago

Personal note/feedback: To note, ggiraph layout is not the native ggplot2 especially regarding font size, caption location (i.e., ggplot2::labs(caption = ...)). I would always prefer to use ggplot2 for static figures rather than "screenshot" of ggiraph.

cderv commented 10 months ago

I would always prefer to use ggplot2 for static figures rather than "screenshot" of ggiraph.

yes obviously ! Thanks for sharing your feedback.

This was illustrative of the behavior because example in OP example is ggiraph.

To be clear, the clear the behavior is the same for a DT table for example. By default, there will be no static version, so if there is a chunk with a DT table, it will be screenshoted for inclusion as image in Typst output.

Same comment here: It will always be better to a have conditional on format to use a table generation tool that is better looking than a DT screenshot.

Issue is the same anyway, prefer-html should have no impact on format: typst. I'll fix that.

cderv commented 10 months ago

From now on, prefer-html will correctly be ignored in format that don't need it, like typst. This will insure no leak for HTML deps in format that does not support it.

Thanks for the report.