rikhuijzer / PlutoStaticHTML.jl

Convert Pluto notebooks to HTML in automated workflows
https://PlutoStaticHTML.huijzer.xyz
MIT License
83 stars 7 forks source link

`PlutoUI.Scrubbable` doesn't render #154

Open alecloudenback opened 1 year ago

alecloudenback commented 1 year ago

It seems like scrubbable elements don't render in the static version, though there remains a script embedded where there should be something displayed.

Example (should read "With probability equal to 90% that the Observed Rate..." :

image

Served page: https://juliaactuary.org/tutorials/credibility_claims/ Source Franklin site and notebook: https://github.com/JuliaActuary/JuliaActuary.org/blob/54bf2cc5724aa7c7a5fc5f7173fc17c34117f931/tutorials/credibility_claims.jl

Note that I've subsequently pushed a commit to the notebook removing Scrubbable elements, but the repository permalink should still point to the version which had Scrubbables which did not render.

Here's the script contents from the screenshot:

 // weird import to make it faster. The `await import` can still delay execution by one frame if it is already loaded... window.d3format = window.d3format ?? await import("https://cdn.jsdelivr.net/npm/d3-format@2/+esm") const argmin = xs => xs.indexOf(Math.min(...xs)) const closest_index = (xs, y) => argmin(xs.map(x => Math.abs(x-y))) const values = [0.5, 0.51, 0.52, 0.53, 0.54, 0.55, 0.56, 0.57, 0.58, 0.59, 0.6, 0.61, 0.62, 0.63, 0.64, 0.65, 0.66, 0.67, 0.68, 0.69, 0.7, 0.71, 0.72, 0.73, 0.74, 0.75, 0.76, 0.77, 0.78, 0.79, 0.8, 0.81, 0.82, 0.83, 0.84, 0.85, 0.86, 0.87, 0.88, 0.89, 0.9, 0.91, 0.92, 0.93, 0.94, 0.95, 0.96, 0.97, 0.98, 0.99] const el = html` <span title="Click and drag this number left or right!" style="cursor: col-resize; touch-action: none; background: rgb(252, 209, 204); color: black; padding: 0em .2em; border-radius: .3em; font-weight: bold;">0.9</span> ` let old_x = 0 let old_index = 0 const initial_index = closest_index(values, 0.9) let current_index = initial_index const formatter = s => "" + d3format.format(".0%")(s) + "" Object.defineProperty(el, 'value', { get: () => values[current_index], set: x => { current_index = closest_index(values, x) el.innerText = formatter(el.value) }, configurable: true, }); // initial value el.innerText = formatter(0.9) const onScrub = (e) => { const offset = e.clientX - old_x const new_index = Math.min(values.length-1, Math.max(0, Math.round(offset/10) + old_index )) if(new_index !== current_index) { current_index = new_index el.innerText = formatter(el.value) el.dispatchEvent(new CustomEvent("input")) } } const onpointerdown = (e) => { window.getSelection().empty() old_x = e.clientX old_index = current_index window.addEventListener("pointermove", onScrub) } el.addEventListener("pointerdown", onpointerdown) const ondblclick = (e) => { current_index = initial_index el.innerText = formatter(el.value) el.dispatchEvent(new CustomEvent("input")) } el.addEventListener("dblclick", ondblclick) const onpointerup = () => { window.removeEventListener("pointermove", onScrub) } window.addEventListener("pointerup", onpointerup) el.onselectstart = () => false invalidation.then(() => { el.removeEventListener("pointerdown", onpointerdown) el.removeEventListener("dblclick", ondblclick) window.removeEventListener("pointerup", onpointerup) }) return el 

I tried running this manually in the console, and it returned undefined

rikhuijzer commented 1 year ago

Do I understand correctly that PlutoUI.Scrubbable is a dynamic element? It's probably best to declare those out-of-scope for PlutoStaticHTML.jl to avoid littering the logic in this project with workarounds. What do you think?

alecloudenback commented 1 year ago

Pluto itself will just use a default value (e.g. start of range) or use the default argument, which makes me think that a static version would just use the same. Is it tricker because of the javascript on the dynamic elements?

rikhuijzer commented 1 year ago

Is it tricker because of the javascript on the dynamic elements?

Yes.

PlutoStaticHTML lets Pluto handle all the code evaluations, that is, the conversions from code to output. This includes many workarounds and hacks on the Pluto-side. For example, Pluto reformats tables to the well-known table viewer. That is why PlutoStaticHTML always takes Pluto's output and goes from there.

However, when the transformations executed by Pluto are too complex, then that makes it very difficult for PlutoStaticHTML to convert it back to something that can easily be presented. I think this is such a case because

@testset "PlutoUI.Scrubbable" begin
    nb = Notebook([
        Cell("M = [1 2 3; 4 5 6]"),
        Cell("using PlutoUI: Scrubbable"),
        Cell("Scrubbable(M)")
    ])
    html, nb = notebook2html_helper(nb; use_distributed=true)
    print('\n', nb.cells[3].output.body, '\n')
end

gives

<span style='display: contents;'><pluto-display></pluto-display><script id=hxhfzisgeoamsrgz>const body = /* See the documentation for PlutoRunner.publish_to_js */ getPublishedObject(\"95ccc948-6578-11ed-322e-ed714452c0e2/85b917be-6578-11ed-250c-8bde22d87d9f/hxhfzisgeoamsrgz\");const mime = \"application/vnd.pluto.divelement+object\";const create_new = this == null || this._mime !== mime;const display = create_new ? currentScript.previousElementSibling : this;display.persist_js_state = true;display.body = body;if(create_new) {        display.mime = mime;        display._mime = mime;}return display;</script><script id='wztfidhowt'>\nconst div = currentScript.parentElement\nlet key = \"wztfidhowt\"\nconst inputs = div.querySelectorAll(`pl-combined-child[key='\${key}'] > *:first-child`)\n\nconst values = Array(inputs.length)\n\ninputs.forEach(async (el,i) => {\n\tel.oninput = (e) => {\n\t\te.stopPropagation()\n\t}\n\tconst gen = Generators.input(el)\n\twhile(true) {\n\t\tvalues[i] = await gen.next().value\n\t\tdiv.dispatchEvent(new CustomEvent(\"input\", {}))\n\t}\n})\n\n\nlet set_input_value = (() => {\n\tlet result = null\n\ttry {\n\tresult = setBoundElementValueLikePluto\n} catch (e) {\n\tresult = ((input, new_value) => {\n\t/" ⋯ 423 bytes ⋯ "               }\n                return\n            }\n            case \"date\": {\n                if (input.valueAsDate == null || Number(input.valueAsDate) !== Number(new_value)) {\n                    input.valueAsDate = new_value\n                }\n                return\n            }\n            case \"checkbox\": {\n                if (input.checked !== new_value) {\n                    input.checked = new_value\n                }\n                return\n            }\n            case \"file\": {\n                // Can't set files :(\n                return\n            }\n        }\n    } else if (input instanceof HTMLSelectElement && input.multiple) {\n        for (let option of Array.from(input.options)) {\n            option.selected = new_value.includes(option.value)\n        }\n        return\n    }\n    //@ts-ignore\n    if (input.value !== new_value) {\n        //@ts-ignore\n        input.value = new_value\n    }\n})\n}\nreturn result\n})()\n\n\t\nObject.defineProperty(div, 'value', {\n\tget: () => values,\n\tset: (newvals) => {\n\t\tif(!newvals) {\n\t\t\treturn\n\t\t}\n\t\tinputs.forEach((el, i) => {\n\t\t\tvalues[i] = newvals[i]\n\t\t\tset_input_value(el, newvals[i])\n\t\t})\n},\n\tconfigurable: true,\n});\n\t\n</script></span>

The numbers from the matrix are not even in here because Pluto calls them from the back end via the getPublishedObject.

To fix this, there are two ways:

  1. Add a new option to PlutoStaticHTML which calls the backend on the fly.
  2. Modify PlutoUI so that the transformation can be disabled via an override.
  3. Remove all calls to PlutoUI.Scrubbable in the code before evaluating.

All are relatively too much work compared to the benefits, I think. The last one seems the most likely to be implemented if something would be implemented.