scalameta / mdoc

Typechecked markdown documentation for Scala
https://scalameta.org/mdoc/
Apache License 2.0
392 stars 80 forks source link

Docusaurus does not run ScalaJS snippets #588

Open vincenzobaz opened 2 years ago

vincenzobaz commented 2 years ago

ScalaJS snippets

Markdown files containing snippets with the modifier scala mdoc:js are transformed into markdown files which end with two script tags

<script type="text/javascript" src="../assets/somePath/someFile.md.js" defer></script>
<script type="text/javascript" src="../assets/somePath/mdoc.js" defer></script>

where someFile.md.js contains the snippet compiled to Javascript via ScalaJS.

If I run docs/mdoc --watch the web UI allows to open the new file and the JS is executed.

The problem

However when Docusaurus v2 (now default) is used with sbt-mdoc, these scripts are not executed. This is connected to Docusaurus being a React-based SPA.

It is a pity not to use this very nice feature of mdoc!

Some ideas to solve the problem

Docusaurus accepts MDXv1 files as we can read here. This means two things:

  1. We can include JSX code into the markdown files that mdoc generates
  2. We can include JSX code that invokes the Javascript file produced by ScalaJS

In particular we can can modify this section to add JSX code.

My first idea was to inject something like this at the end of the generated markdown:

export const MdocDiv = () => {
  const divRef = React.useRef(null);
  React.useEffect(() => fun(divRef.current));

  return(<div ref={divRef}></div>);
}

<MdocDiv />

were fun is the function exported in the Javascript file created by ScalaJS.

This is the tricky part, where I am stuck, on how to make this function available here:

First approach: proper imports

Change ScalaJS settings to compile an ESModule and import the function here using raw-loader. However this does not seem to be supported

Second approach: dirty dom manipulation

This approach follows the current implementation (it mimics mdoc.js):

  1. Create a script tag pointing to the compiled js script
  2. eval the function name to load the function
  3. Use the function when the node div is ready

Because Docusaurus uses react this is particularly tricky. It could like this:

export const MdocDiv = () => {
  const script = document.createElement('script');
  script.setAttribute('src',"../assets/somePath/someFile.md.js");
  document.head.appendChild(script);
  const divRef = React.useRef(null);
  React.useEffect(() => {
   const fun = eval("mdoc_js_run0");
   fun(divRef.current);
  });

  return(<div ref={divRef}></div>);
}

import BrowserOnly from '@docusaurus/BrowserOnly';

<BrowserOnly>
  {() => {
    return(<MdocDiv />)
  }}
</BrowserOnly>

where BrowserOnly is needed to delay execution to when document will be available.

We then to make sure that someFIle.md.js can be accessed statically (I had to copy to another folder to be able to download it). But this is not working yet maybe because of webpack not including it, but I am not sure. I see that:

You can experiment modifying the generated markdown that is watched by Docusaurus without modifying the source of mdoc

olafurpg commented 2 years ago

Thank you for the detailed report! One workaround for now is to use Docusaurus v1.

I'm happy to merge any PR that adds a custom code path to generate Docusaurus v2 compatible JS. The option can be read around here

https://github.com/scalameta/mdoc/blob/6a5be8cbde534662b27f1b68d2e12384a31216c9/mdoc-js/src/main/scala/mdoc/modifiers/JsModifier.scala#L86

We can also automatically pass the option from the sbt DocusaurusPlugin here https://github.com/scalameta/mdoc/blob/6a5be8cbde534662b27f1b68d2e12384a31216c9/mdoc-sbt/src/main/scala/mdoc/DocusaurusPlugin.scala#L98

vincenzobaz commented 2 years ago

I experimented a bit with alternative site generators and I found that using the new scaladoc with mdoc provides a very lightweight strategy to fix this.

You can see an example here rendered here