gadenbuie / xaringanExtra

:ferris_wheel: A playground of enhancements and extensions for xaringan slides.
https://pkg.garrickadenbuie.com/xaringanExtra
Other
445 stars 36 forks source link

Automatic markdown citations #167

Open mattwarkentin opened 2 years ago

mattwarkentin commented 2 years ago

Hi @gadenbuie,

I have always wanted citation support for xaringan. I know that the markdown in xaringan is protected from pandocs rendering, so this would require a clever approach, I think. I know citation support has been requested several times in the main xaringan repo (https://github.com/yihui/xaringan/issues/26, https://github.com/yihui/xaringan/issues/74). But I wanted to open an issue here to discuss this with you and assess the feasibility.

Personally, I would love to work toward the type of citation support offered in distill articles, whereby a citation is denoted by standard markdown syntax [@tag] and the citation is rendered such that hovering over the in-text citation reveals the full citation (https://rstudio.github.io/distill/#citations).

Distill citations: https://rstudio.github.io/distill/citations.html

What are your initial thoughts?

gadenbuie commented 2 years ago

I like the style but wonder if it would be a good fit for slides? Have you seen namedropR? I haven't tried it but I saw it in my feed recently and thought it was an interesting approach, esp for citations in slides and live presentations.

mattwarkentin commented 2 years ago

namdropR is new to me. I guess I was thinking about my more academic-focused presentations where the first several slides are background information with citations. It would be nice when presenting or sharing the slides to hover for more information. I am not sure in that space whether a QR code would work, but I'm open to the idea.

Whatever implementation we hypothetically go with would need to be widely appealing, so its worth thinking about format a good deal, assuming the implementation is possible.

How about including the namedropR citation in a hover popup. Is that getting too crazy? It seems like perhaps that would solve both problems of space and still provide ease of access.

mattwarkentin commented 2 years ago

@gadenbuie I think I've got a pretty good start on an implementation. However, I am running into an issue that I think you can probably help with. I've got the implementation working in a standard HTML document (from R Markdown), but when I switch to xaringan, the HTML returned by my R function is not handled the same.

Is there a recommended way for how an R function should safely return HTML and have that HTML be treated as raw HTML in the output/xaringan document? I've tried a few things without success.

Basically my R function returns a htmltools::tags$a(...) object but it is not being rendered in the output document as-is. Thoughts?

gadenbuie commented 2 years ago

Can you show me an example of what you expect and what you actually get?

One thing I've run into frequently with xaringan is that the markdown parser eagerly wraps things in paragraph tags in places where I wasn't expecting them. So maybe that's part of it?

mattwarkentin commented 2 years ago

I can send some code over when I get to work tomorrow morning. But I think what you described is exactly what is happening. My function returns raw HTML, but part of it is getting spliced into random paragraph tags.

My function returns an <a> element with an attribute that is a string of more raw HTML (that gets handled by the relevant JS). Something like: <a data-attr="<table>...</table>">Text</a>. But the attribute string of HTML is getting borked by xaringan.

mattwarkentin commented 2 years ago

Okay, I hacked together a reprex. I think you'll need the dev version of namedropR to ensure it runs as expected. The R Markdown renders as expected for rmarkdown::html_document, but not as xaringan::moon_reader.

---
title: "Citation example"
output:
  html_document: default
  xaringan::moon_reader: default
bibliography: [refs.bib]
---

<script src="https://unpkg.com/@popperjs/core@2/dist/umd/popper.min.js"></script>
<script src="https://unpkg.com/tippy.js@6/dist/tippy-bundle.umd.js"></script>

<style type="text/css" media="all">
.tippy-box[data-theme~='simple'] {
  background-color: white;
  border: 1px solid black;
}
</style>

<script type="text/javascript" charset="utf-8">
  window.addEventListener('DOMContentLoaded', (event) => {
    tippy('[data-tippy-content]', {
      interactive: true,
      allowHTML: true,
      hideOnClick: true,
      trigger: 'click',
      placement: 'auto',
      theme: 'simple',
      maxWidth: 'none'
    });
  });
</script>

```{r echo=FALSE, message=FALSE}
library(namedropR)
library(htmltools)
.keys <- vector('character')

cite <- function(key) {
  bibs <- rmarkdown::metadata$bibliography

  if (length(bibs) > 1) {
    tmp_bib <- tempfile(fileext = '.bib')
    purrr::walk(bibs, function(b) write(readLines(b), tmp_bib, append = TRUE))
    bibs <- tmp_bib
  }

  .keys <<- unique(c(.keys, key))

  tooltip <- htmltools::includeHTML(
    drop_name(
      bibs,
      output_dir = tempdir(),
      cite_key = key,
      export_as = "html",
      use_xaringan = TRUE,
      include_qr = 'embed',
      qr_hyperlink = TRUE,
      qr_size = 150,
      style = 'classic'
    )
  )
  tooltip <- as.character(tooltip)

  id <- which(.keys == key)

  tags$a(
    id,
    href = "javascript:void(0)",
    `data-tippy-content` = tooltip
  )
}

r cite('Eschrich1983')


You will also need `refs.bib`:
```bib
@article{Eschrich1983,
  doi = {10.1007/bf00396886},
  url = {https://doi.org/10.1007/bf00396886},
  year = {1983},
  month = may,
  publisher = {Springer Science and Business Media {LLC}},
  volume = {157},
  number = {6},
  pages = {540--547},
  author = {Walter Eschrich},
  title = {Phloem unloading in aerial roots of Monstera deliciosa},
  journal = {Planta}
}
gadenbuie commented 2 years ago

I'm very certain that there's some kind of issue with whitespace and newlines going on here.

Since it's a little roundabout to get the namedropR html, I'd recommend collecting the name drop htmls into a JSON object that you embed via <script type="application/json">...</script> somehow. Then, you'd make the initial tooltip element pretty concise, e.g. <span data-namedropr="key">citation number/text</span>, and use javascript to inject the tooltip in the right way thereafter.

mattwarkentin commented 2 years ago

Okay, sounds good. I will play around with this implementation idea some more and see what works. Will share when I've got something a little more established. Thanks for the suggestion.

mattwarkentin commented 2 years ago

Okay, I could actually use a little bit more nudging in the right direction. It is relatively straightforward to have the R function return a span or a element with some attribute/key and then use this key to inject the HTML tooltip from the JS side of things. I feel comfortable implementing this.

My issue is with making the JSON object available in a script tag...

I have assumed the usage will be as shown before, using cite() multiple times throughout the document. So far, each invocation will return a span element, and behind the scenes store the name drop HTML in a hidden named list. The last step should be packaging up the named list as a JSON obejct (jsonlite::toJSON) and then wrapping that in the script tag.

But when/how do I do this final step? It should run after all of the cite() calls have been made, so ideally as the last bit of code ran in the document...something like on.exit() but for the whole knitr rendering. Am I over-thinking this?

gadenbuie commented 2 years ago

I think you could probably pair the <a> or <span> tag with a <script> containing the JSON. You'd want to use a consistent pair of attributes, e.g. data-namedropR="citekey" on the a/span and data-namedropR-for="citekey" on the script tag. (And you'd only need to add the script tag once per citekey.)

Then, on load, you'd look for script[data-namedropR-for] and read the data in them and then use that to build the element you really want with JavaScript to augment the placeholder elements.

mattwarkentin commented 2 years ago

Hmm. That was my original strategy but I was unable to have the R function return a tagList object containing both a span and a script element. When I tried this, neither element was to be found in the HTML document.

gadenbuie commented 2 years ago

The two key things that happen in this approach are:

  1. When you turn the html into json it avoids the new line problem by encoding the new lines literally.jsonlite::toJSON() makes minified single-line json by default.
  2. The script tags won't show up in the document, they're just shown as white space in the browser, so they're pretty safe.
gadenbuie commented 2 years ago

When I tried this, neither element was to be found in the HTML document.

Oh weird. Hmmm it should be possible. If you try it again and can make another reprex or PR, I'll be happy to take a look.

mattwarkentin commented 2 years ago

Okay, woof. I tried to push a draft PR of this feature but I forgot I have an open PR already for scribble. So these changes are part of that PR at the moment. Not sure how to proceed. Should've used branches...

gadenbuie commented 2 years ago

Assuming that my remote is called upstream locally (it probably is).

git fetch upstream

# if that errors, then add my repo
git remote add upsream https://github.com/gadenbuie/xaringanExtra.git

Start from your main branch that's really the PR branch

# then from your main branch
git checkout -b citations upstream/main
git cherry-pick 1367261fd83f732ddbe1c3a1396cf78d332b03d5
git push -u origin citations

That'll send the citations branch to https://github.com/mattwarkentin/xaringanExtra where you can create the PR for the citations feature.

Then go back to your main and clean up those extra commits, force pushing back to GitHub to remove the extra commits

git checkout main
git reset --hard 972496718ad8fb5c4cd7c44b5dff9314a37036d1
git push --force

Unfortunately, you can't rename your main branch or move the PR branch without closing the PR. So for now just wait until I merge it and then you'll have to delete your branch and recreate it.