From 9b9d76ee9cf4d8b36bc56069150dad1f91a8aa10 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 29 Sep 2023 12:54:59 -0400 Subject: [PATCH 01/13] Implement new API approach that has shape `extension SUB_COMMAND` --- .../quarto-ext/shinylive/shinylive.lua | 89 +++++++++++++++---- 1 file changed, 70 insertions(+), 19 deletions(-) diff --git a/_extensions/quarto-ext/shinylive/shinylive.lua b/_extensions/quarto-ext/shinylive/shinylive.lua index e54493b..9a54ce8 100644 --- a/_extensions/quarto-ext/shinylive/shinylive.lua +++ b/_extensions/quarto-ext/shinylive/shinylive.lua @@ -1,5 +1,8 @@ local hasDoneShinyliveSetup = false +local hasDoneSetup = { init = false, r = false, python = false } +local versions = { r = nil, python = nil } local codeblockScript = nil +local appSpecificDeps = {} -- Try calling `pandoc.pipe('shinylive', ...)` and if it fails, print a message -- about installing shinylive python package. @@ -42,33 +45,65 @@ function callRShinylive(args, input) end function callShinylive(language, args, input) + if input == nil then + input = "" + end + local res -- print("Calling " .. language .. " shinylive with args: " .. table.concat(args, " ")) if language == "python" then - return callPythonShinylive(args, input) + res = callPythonShinylive(args, input) elseif language == "r" then - return callRShinylive(args, input) + res = callRShinylive(args, input) else error("Unknown language: " .. language) end + + -- Decode JSON object + local result + local status, err = pcall( + function() + result = quarto.json.decode(res) + end + ) + if not status then + print("JSON string being parsed:") + print(res) + print("Error:") + print(err) + if language == "python" then + error("Error decoding JSON response from shinylive.") + elseif language == "r" then + error( + "Error decoding JSON response from shinylive." .. + "\nIf the `shinylive` R package has been installed," .. + " please check that no additional output was printed to the console." + ) + end + end + return result end --- Do one-time setup when a Shinylive codeblock is encountered. -function ensureShinyliveSetup(language) - if hasDoneShinyliveSetup then +-- Do one-time setup for language agnostic html dependencies. +-- This should only be called once per document +-- @param language: "python" or "r" +function ensureInitSetup(language) + if hasDoneSetup.init then return end - hasDoneShinyliveSetup = true + hasDoneSetup.init = true -- Find the path to codeblock-to-json.ts and save it for later use. - codeblockScript = callShinylive(language, { "codeblock-to-json-path" }, "") - -- Remove trailing whitespace - codeblockScript = codeblockScript:gsub("%s+$", "") + local infoObj = callShinylive(language, { "extension", "info" }) + -- Store the path to codeblock-to-json.ts for later use + codeblockScript = infoObj.scripts['codeblock-to-json'] + -- Add language-agnostic dependencies local baseDeps = getShinyliveBaseDeps(language) for idx, dep in ipairs(baseDeps) do quarto.doc.add_html_dependency(dep) end + -- Add ext css dependency quarto.doc.add_html_dependency( { name = "shinylive-quarto-css", @@ -77,19 +112,34 @@ function ensureShinyliveSetup(language) ) end +function ensureLanguageSetup(language) + ensureInitSetup(language) + + if hasDoneSetup[language] then + return + end + hasDoneSetup[language] = true + + -- Add language-specific dependencies + local langResources = callShinylive(language, { "extension", "language-resources" }) + for idx, resourceDep in ipairs(langResources) do + -- No need to check for uniqueness. + -- Each resource is only be added once and should already be unique. + quarto.doc.attach_to_dependency("shinylive", resourceDep) + end +end + function getShinyliveBaseDeps(language) -- Relative path from the current page to the root of the site. This is needed -- to find out where shinylive-sw.js is, relative to the current page. if quarto.project.offset == nil then error("The shinylive extension must be used in a Quarto project directory (with a _quarto.yml file).") end - local depJson = callShinylive( + local deps = callShinylive( language, - { "base-deps", "--sw-dir", quarto.project.offset }, + { "extension", "base-htmldeps", "--sw-dir", quarto.project.offset }, "" ) - - local deps = quarto.json.decode(depJson) return deps end @@ -101,6 +151,7 @@ return { return end + local language if el.attr.classes:includes("{shinylive-r}") then language = "r" elseif el.attr.classes:includes("{shinylive-python}") then @@ -109,7 +160,8 @@ return { -- Not a shinylive codeblock, return return end - ensureShinyliveSetup(language) + -- Setup language and language-agnostic dependencies + ensureLanguageSetup(language) -- Convert code block to JSON string in the same format as app.json. local parsedCodeblockJson = pandoc.pipe( @@ -117,19 +169,18 @@ return { { "run", codeblockScript }, el.text ) - -- This contains "files" and "quartoArgs" keys. local parsedCodeblock = quarto.json.decode(parsedCodeblockJson) -- Find Python package dependencies for the current app. - local appDepsJson = callShinylive( + local appDeps = callShinylive( language, - { "package-deps" }, + { "extension", "app-resources" }, + -- Send as piped input to the shinylive command quarto.json.encode(parsedCodeblock["files"]) ) - local appDeps = quarto.json.decode(appDepsJson) - + -- Add app specific dependencies for idx, dep in ipairs(appDeps) do quarto.doc.attach_to_dependency("shinylive", dep) end From ac50f67e8ddb056ffabaeafa0ff1f437c3737507 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 29 Sep 2023 13:07:44 -0400 Subject: [PATCH 02/13] Document --- .../quarto-ext/shinylive/shinylive.lua | 131 +++++++++++++++++- 1 file changed, 126 insertions(+), 5 deletions(-) diff --git a/_extensions/quarto-ext/shinylive/shinylive.lua b/_extensions/quarto-ext/shinylive/shinylive.lua index 9a54ce8..a3db872 100644 --- a/_extensions/quarto-ext/shinylive/shinylive.lua +++ b/_extensions/quarto-ext/shinylive/shinylive.lua @@ -1,12 +1,123 @@ -local hasDoneShinyliveSetup = false +-- # Shinylive package methods +-- Always use `callShinyLive()` to call the shinylive extension. +-- `callPythonShinyLive()` and `callRShinyLive()` should not be used directly. Instead, always use `callShinyLive()`. + +-- # py-shinylive and r-shinylive methods +-- info - Package, version, asset version, and script paths information +-- base-htmldeps - Quarto html dependencies for the base shinylive integration +-- language-resources - R's resource files for the quarto html dependency named `shinylive` +-- app-resources - App-specific resource files for the quarto html dependency named `shinylive` + +-- ### CLI Interface +-- * `extension info` +-- * Prints information about the extension including: +-- * `version`: The version of the R package +-- * `assets_version`: The version of the web assets +-- * `scripts`: A list of paths scripts that are used by the extension, +-- mainly `codeblock-to-json` +-- * Example +-- ``` +-- { +-- "version": "0.1.0", +-- "assets_version": "0.2.0", +-- "scripts": { +-- "codeblock-to-json": "//shinylive-0.2.0/scripts/codeblock-to-json.js" +-- } +-- } +-- ``` +-- * `extension base-htmldeps` +-- * Prints the language agnostic quarto html dependencies as a JSON array. +-- * The first html dependency is the `shinylive` service workers. +-- * The second html dependency is the `shinylive` base dependencies. This +-- dependency will contain the core `shinylive` asset scripts (JS files +-- automatically sourced), stylesheets (CSS files that are automatically +-- included), and resources (additional files that the JS and CSS files can +-- source). +-- * Example +-- ``` +-- [ +-- { +-- "name": "shinylive-serviceworker", +-- "version": "0.2.0", +-- "meta": { "shinylive:serviceworker_dir": "." }, +-- "serviceworkers": [ +-- { +-- "source": "//shinylive-0.2.0/shinylive-sw.js", +-- "destination": "/shinylive-sw.js" +-- } +-- ] +-- }, +-- { +-- "name": "shinylive", +-- "version": "0.2.0", +-- "scripts": [{ +-- "name": "shinylive/load-shinylive-sw.js", +-- "path": "//shinylive-0.2.0/shinylive/load-shinylive-sw.js", +-- "attribs": { "type": "module" } +-- }], +-- "stylesheets": [{ +-- "name": "shinylive/shinylive.css", +-- "path": "//shinylive-0.2.0/shinylive/shinylive.css" +-- }], +-- "resources": [ +-- { +-- "name": "shinylive/shinylive.js", +-- "path": "//shinylive-0.2.0/shinylive/shinylive.js" +-- }, +-- ... # [ truncated ] +-- ] +-- } +-- ] +-- ``` +-- * `extension language-resources` +-- * Prints the language-specific resource files as JSON that should be added to the quarto html dependency. +-- * For r-shinylive, this includes the webr resource files +-- * For py-shinylive, this includes the pyodide and pyright resource files. +-- * Example +-- ``` +-- [ +-- { +-- "name": "shinylive/webr/esbuild.d.ts", +-- "path": "//shinylive-0.2.0/shinylive/webr/esbuild.d.ts" +-- }, +-- { +-- "name": "shinylive/webr/libRblas.so", +-- "path": "//shinylive-0.2.0/shinylive/webr/libRblas.so" +-- }, +-- ... # [ truncated ] +-- ] +-- * `extension app-resources` +-- * Prints app-specific resource files as JSON that should be added to the `"shinylive"` quarto html dependency. +-- * Currently, r-shinylive does not return any resource files. +-- * Example +-- ``` +-- [ +-- { +-- "name": "shinylive/pyodide/anyio-3.7.0-py3-none-any.whl", +-- "path": "//shinylive-0.2.0/shinylive/pyodide/anyio-3.7.0-py3-none-any.whl" +-- }, +-- { +-- "name": "shinylive/pyodide/appdirs-1.4.4-py2.py3-none-any.whl", +-- "path": "//shinylive-0.2.0/shinylive/pyodide/appdirs-1.4.4-py2.py3-none-any.whl" +-- }, +-- ... # [ truncated ] +-- ] +-- ``` + + + + local hasDoneSetup = { init = false, r = false, python = false } local versions = { r = nil, python = nil } local codeblockScript = nil local appSpecificDeps = {} --- Try calling `pandoc.pipe('shinylive', ...)` and if it fails, print a message --- about installing shinylive python package. +-- Python specific method to call py-shinylive +-- @param args: list of string arguments to pass to py-shinylive +-- @param input: string to pipe into to py-shinylive function callPythonShinylive(args, input) + -- Try calling `pandoc.pipe('shinylive', ...)` and if it fails, print a message + -- about installing shinylive python package. local res local status, err = pcall( function() @@ -22,12 +133,16 @@ function callPythonShinylive(args, input) return res end --- Try calling `pandoc.pipe('Rscript', ...)` and if it fails, print a message --- about installing shinylive R package. +-- R specific method to call {r-shinylive} +-- @param args: list of string arguments to pass to r-shinylive +-- @param input: string to pipe into to r-shinylive function callRShinylive(args, input) args = { "-e", "shinylive:::quarto_ext()", table.unpack(args) } + + -- Try calling `pandoc.pipe('Rscript', ...)` and if it fails, print a message + -- about installing shinylive R package. local res local status, err = pcall( function() @@ -44,6 +159,9 @@ function callRShinylive(args, input) return res end +-- Returns decoded object +-- @param language: "python" or "r" +-- @param args, input: see `callPythonShinylive` and `callRShinylive` function callShinylive(language, args, input) if input == nil then input = "" @@ -112,6 +230,9 @@ function ensureInitSetup(language) ) end +-- Do one-time setup for language specific html dependencies. +-- This should only be called once per document +-- @param language: "python" or "r" function ensureLanguageSetup(language) ensureInitSetup(language) From d90f66a21a40f777691dc3496265b878a8fc11be Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 29 Sep 2023 13:08:46 -0400 Subject: [PATCH 03/13] Trim cli output until first `[` or `{` Fixes #23 --- .../quarto-ext/shinylive/shinylive.lua | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/_extensions/quarto-ext/shinylive/shinylive.lua b/_extensions/quarto-ext/shinylive/shinylive.lua index a3db872..49b9447 100644 --- a/_extensions/quarto-ext/shinylive/shinylive.lua +++ b/_extensions/quarto-ext/shinylive/shinylive.lua @@ -176,6 +176,29 @@ function callShinylive(language, args, input) error("Unknown language: " .. language) end + -- Remove any unwanted output before the first curly brace or square bracket. + -- print("res: " .. string.sub(res, 1, math.min(string.len(res), 100)) .. "...") + local curly_start = string.find(res, "{", 0, true) + local brace_start = string.find(res, "[", 0, true) + local min_start + if curly_start == nil then + min_start = brace_start + elseif brace_start == nil then + min_start = curly_start + else + min_start = math.min(curly_start, brace_start) + end + if min_start == nil then + local res_str = res + if string.len(res) > 100 then + res_str = string.sub(res, 1, 100) .. "... [truncated]" + end + error("Could not find start curly brace or start brace in " .. language .. " shinylive response:\n" .. res_str) + end + if min_start > 1 then + res = string.sub(res, min_start) + end + -- Decode JSON object local result local status, err = pcall( From e24c9d183b381c7305cf0a786ed6ded60438180d Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 29 Sep 2023 13:09:45 -0400 Subject: [PATCH 04/13] Only add unique app-resources Fixes #24 --- _extensions/quarto-ext/shinylive/shinylive.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/_extensions/quarto-ext/shinylive/shinylive.lua b/_extensions/quarto-ext/shinylive/shinylive.lua index 49b9447..b71df71 100644 --- a/_extensions/quarto-ext/shinylive/shinylive.lua +++ b/_extensions/quarto-ext/shinylive/shinylive.lua @@ -326,7 +326,10 @@ return { -- Add app specific dependencies for idx, dep in ipairs(appDeps) do - quarto.doc.attach_to_dependency("shinylive", dep) + if not appSpecificDeps[dep.name] then + appSpecificDeps[dep.name] = true + quarto.doc.attach_to_dependency("shinylive", dep) + end end if el.attr.classes:includes("{shinylive-python}") then From dcbf41e83e3484f9c9a66a08db8302da5b82fefc Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 29 Sep 2023 13:28:41 -0400 Subject: [PATCH 05/13] First pass at verifying r-shinylive and py-shinylive support the same assets version --- .../quarto-ext/shinylive/shinylive.lua | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/_extensions/quarto-ext/shinylive/shinylive.lua b/_extensions/quarto-ext/shinylive/shinylive.lua index b71df71..f689b8a 100644 --- a/_extensions/quarto-ext/shinylive/shinylive.lua +++ b/_extensions/quarto-ext/shinylive/shinylive.lua @@ -237,6 +237,8 @@ function ensureInitSetup(language) local infoObj = callShinylive(language, { "extension", "info" }) -- Store the path to codeblock-to-json.ts for later use codeblockScript = infoObj.scripts['codeblock-to-json'] + -- Store the version info for later use + versions[language] = { version = infoObj.version, assets_version = infoObj.assets_version } -- Add language-agnostic dependencies local baseDeps = getShinyliveBaseDeps(language) @@ -264,6 +266,32 @@ function ensureLanguageSetup(language) end hasDoneSetup[language] = true + -- Only get the asset version value if it hasn't been retrieved yet. + if versions[language] == nil then + local infoObj = callShinylive(language, { "extension", "info" }) + versions[language] = { version = infoObj.version, assets_version = infoObj.assets_version } + end + -- Verify that the r-shinylive and py-shinylive versions match + if + (versions.r and versions.python) and + ---@diagnostic disable-next-line: undefined-field + versions.r.assets_version ~= versions.python.assets_version + then + error( + "The shinylive R and Python packages must support the same Shinylive Assets version to be used in the same quarto document." .. + "\nR" .. + ---@diagnostic disable-next-line: undefined-field + "\n\tShinylive package version: " .. versions.r.version .. + ---@diagnostic disable-next-line: undefined-field + "\n\tSupported ssets version: " .. versions.r.assets_version .. + "\nPython" .. + ---@diagnostic disable-next-line: undefined-field + "\n\tShinylive package version: " .. versions.python.version .. + ---@diagnostic disable-next-line: undefined-field + "\n\tSupported ssets version: " .. versions.python.assets_version + ) + end + -- Add language-specific dependencies local langResources = callShinylive(language, { "extension", "language-resources" }) for idx, resourceDep in ipairs(langResources) do From bc8137cb1651b2634070c33d70d4e835b241c277 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Wed, 4 Oct 2023 15:47:44 -0400 Subject: [PATCH 06/13] Move top comments to README.md --- _extensions/quarto-ext/shinylive/README.md | 126 ++++++++++++++++++ .../quarto-ext/shinylive/shinylive.lua | 105 --------------- 2 files changed, 126 insertions(+), 105 deletions(-) create mode 100644 _extensions/quarto-ext/shinylive/README.md diff --git a/_extensions/quarto-ext/shinylive/README.md b/_extensions/quarto-ext/shinylive/README.md new file mode 100644 index 0000000..55bfed2 --- /dev/null +++ b/_extensions/quarto-ext/shinylive/README.md @@ -0,0 +1,126 @@ +# Shinylive package methods + +## Methods + +### R + +Interaction: + +``` +Rscript -e 'shinylive:::quarto_ext()' [methods] [args] +``` + +### Python + +Interaction: + +``` +shinylive [methods] [args] +``` + +## CLI Methods + +* `extension info` + * Package, version, asset version, and script paths information +* `extension base-htmldeps` + * Quarto html dependencies for the base shinylive integration +* `extension language-resources` + * Language specific resource files for the quarto html dependency named `shinylive` +* `extension app-resources` + * App specific resource files for the quarto html dependency named `shinylive` + +### CLI Interface +* `extension info` + * Prints information about the extension including: + * `version`: The version of the R package + * `assets_version`: The version of the web assets + * `scripts`: A list of paths scripts that are used by the extension, + mainly `codeblock-to-json` + * Example + ``` + { + "version": "0.1.0", + "assets_version": "0.2.0", + "scripts": { + "codeblock-to-json": "//shinylive-0.2.0/scripts/codeblock-to-json.js" + } + } + ``` +* `extension base-htmldeps` + * Prints the language agnostic quarto html dependencies as a JSON array. + * The first html dependency is the `shinylive` service workers. + * The second html dependency is the `shinylive` base dependencies. This + dependency will contain the core `shinylive` asset scripts (JS files + automatically sourced), stylesheets (CSS files that are automatically + included), and resources (additional files that the JS and CSS files can + source). + * Example + ``` + [ + { + "name": "shinylive-serviceworker", + "version": "0.2.0", + "meta": { "shinylive:serviceworker_dir": "." }, + "serviceworkers": [ + { + "source": "//shinylive-0.2.0/shinylive-sw.js", + "destination": "/shinylive-sw.js" + } + ] + }, + { + "name": "shinylive", + "version": "0.2.0", + "scripts": [{ + "name": "shinylive/load-shinylive-sw.js", + "path": "//shinylive-0.2.0/shinylive/load-shinylive-sw.js", + "attribs": { "type": "module" } + }], + "stylesheets": [{ + "name": "shinylive/shinylive.css", + "path": "//shinylive-0.2.0/shinylive/shinylive.css" + }], + "resources": [ + { + "name": "shinylive/shinylive.js", + "path": "//shinylive-0.2.0/shinylive/shinylive.js" + }, + ... # [ truncated ] + ] + } + ] + ``` +* `extension language-resources` + * Prints the language-specific resource files as JSON that should be added to the quarto html dependency. + * For r-shinylive, this includes the webr resource files + * For py-shinylive, this includes the pyodide and pyright resource files. + * Example + ``` + [ + { + "name": "shinylive/webr/esbuild.d.ts", + "path": "//shinylive-0.2.0/shinylive/webr/esbuild.d.ts" + }, + { + "name": "shinylive/webr/libRblas.so", + "path": "//shinylive-0.2.0/shinylive/webr/libRblas.so" + }, + ... # [ truncated ] + ] +* `extension app-resources` + * Prints app-specific resource files as JSON that should be added to the `"shinylive"` quarto html dependency. + * Currently, r-shinylive does not return any resource files. + * Example + ``` + [ + { + "name": "shinylive/pyodide/anyio-3.7.0-py3-none-any.whl", + "path": "//shinylive-0.2.0/shinylive/pyodide/anyio-3.7.0-py3-none-any.whl" + }, + { + "name": "shinylive/pyodide/appdirs-1.4.4-py2.py3-none-any.whl", + "path": "//shinylive-0.2.0/shinylive/pyodide/appdirs-1.4.4-py2.py3-none-any.whl" + }, + ... # [ truncated ] + ] + ``` diff --git a/_extensions/quarto-ext/shinylive/shinylive.lua b/_extensions/quarto-ext/shinylive/shinylive.lua index f689b8a..6899082 100644 --- a/_extensions/quarto-ext/shinylive/shinylive.lua +++ b/_extensions/quarto-ext/shinylive/shinylive.lua @@ -1,108 +1,3 @@ --- # Shinylive package methods --- Always use `callShinyLive()` to call the shinylive extension. --- `callPythonShinyLive()` and `callRShinyLive()` should not be used directly. Instead, always use `callShinyLive()`. - --- # py-shinylive and r-shinylive methods --- info - Package, version, asset version, and script paths information --- base-htmldeps - Quarto html dependencies for the base shinylive integration --- language-resources - R's resource files for the quarto html dependency named `shinylive` --- app-resources - App-specific resource files for the quarto html dependency named `shinylive` - --- ### CLI Interface --- * `extension info` --- * Prints information about the extension including: --- * `version`: The version of the R package --- * `assets_version`: The version of the web assets --- * `scripts`: A list of paths scripts that are used by the extension, --- mainly `codeblock-to-json` --- * Example --- ``` --- { --- "version": "0.1.0", --- "assets_version": "0.2.0", --- "scripts": { --- "codeblock-to-json": "//shinylive-0.2.0/scripts/codeblock-to-json.js" --- } --- } --- ``` --- * `extension base-htmldeps` --- * Prints the language agnostic quarto html dependencies as a JSON array. --- * The first html dependency is the `shinylive` service workers. --- * The second html dependency is the `shinylive` base dependencies. This --- dependency will contain the core `shinylive` asset scripts (JS files --- automatically sourced), stylesheets (CSS files that are automatically --- included), and resources (additional files that the JS and CSS files can --- source). --- * Example --- ``` --- [ --- { --- "name": "shinylive-serviceworker", --- "version": "0.2.0", --- "meta": { "shinylive:serviceworker_dir": "." }, --- "serviceworkers": [ --- { --- "source": "//shinylive-0.2.0/shinylive-sw.js", --- "destination": "/shinylive-sw.js" --- } --- ] --- }, --- { --- "name": "shinylive", --- "version": "0.2.0", --- "scripts": [{ --- "name": "shinylive/load-shinylive-sw.js", --- "path": "//shinylive-0.2.0/shinylive/load-shinylive-sw.js", --- "attribs": { "type": "module" } --- }], --- "stylesheets": [{ --- "name": "shinylive/shinylive.css", --- "path": "//shinylive-0.2.0/shinylive/shinylive.css" --- }], --- "resources": [ --- { --- "name": "shinylive/shinylive.js", --- "path": "//shinylive-0.2.0/shinylive/shinylive.js" --- }, --- ... # [ truncated ] --- ] --- } --- ] --- ``` --- * `extension language-resources` --- * Prints the language-specific resource files as JSON that should be added to the quarto html dependency. --- * For r-shinylive, this includes the webr resource files --- * For py-shinylive, this includes the pyodide and pyright resource files. --- * Example --- ``` --- [ --- { --- "name": "shinylive/webr/esbuild.d.ts", --- "path": "//shinylive-0.2.0/shinylive/webr/esbuild.d.ts" --- }, --- { --- "name": "shinylive/webr/libRblas.so", --- "path": "//shinylive-0.2.0/shinylive/webr/libRblas.so" --- }, --- ... # [ truncated ] --- ] --- * `extension app-resources` --- * Prints app-specific resource files as JSON that should be added to the `"shinylive"` quarto html dependency. --- * Currently, r-shinylive does not return any resource files. --- * Example --- ``` --- [ --- { --- "name": "shinylive/pyodide/anyio-3.7.0-py3-none-any.whl", --- "path": "//shinylive-0.2.0/shinylive/pyodide/anyio-3.7.0-py3-none-any.whl" --- }, --- { --- "name": "shinylive/pyodide/appdirs-1.4.4-py2.py3-none-any.whl", --- "path": "//shinylive-0.2.0/shinylive/pyodide/appdirs-1.4.4-py2.py3-none-any.whl" --- }, --- ... # [ truncated ] --- ] --- ``` From 476ffcfc754d98c5b5e386f701a34f4992e139b4 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Wed, 4 Oct 2023 15:49:13 -0400 Subject: [PATCH 07/13] Document global vars; Use `assert(false, foo)`, not `error(foo)` --- .../quarto-ext/shinylive/shinylive.lua | 52 ++++++++++++------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/_extensions/quarto-ext/shinylive/shinylive.lua b/_extensions/quarto-ext/shinylive/shinylive.lua index 6899082..297ece8 100644 --- a/_extensions/quarto-ext/shinylive/shinylive.lua +++ b/_extensions/quarto-ext/shinylive/shinylive.lua @@ -1,10 +1,26 @@ +-- Notes: +-- * 2023/10/04 - Barret: +-- Always use `callShinyLive()` to call a shinylive extension. +-- `callPythonShinyLive()` and `callRShinyLive()` should not be used directly. +-- Instead, always use `callShinyLive()`. +-- * 2023/10/04 - Barret: +-- I could not get `error(msg)` to quit the current function execution and +-- bubble up the stack and stop. Instead, I am using `assert(false, msg)` to +-- achieve the desired behavior. Multi-line error messages should start with a +-- `\n` to keep the message in the same readable area. - -local hasDoneSetup = { init = false, r = false, python = false } +-- `table` to organize flags to have code only run once. +local hasDoneSetup = { base = false, r = false, python = false, python_version = false } +-- `table` to store `{ version, assets_version }` for each language's extension. +-- If both `r` and `python` are used in the same document, then the +-- `assets_version` for each language must be the same. local versions = { r = nil, python = nil } +-- Global variable for the codeblock-to-json.js script file location local codeblockScript = nil +-- Global hash table to store app specific dependencies to avoid calling +-- `quarto.doc.attach_to_dependency()` multiple times for the same dependency. local appSpecificDeps = {} -- Python specific method to call py-shinylive @@ -22,7 +38,7 @@ function callPythonShinylive(args, input) if not status then print(err) - error("Error running 'shinylive' command. Perhaps you need to install the 'shinylive' Python package?") + assert(false, "Error running 'shinylive' command. Perhaps you need to install the 'shinylive' Python package?") end return res @@ -47,7 +63,7 @@ function callRShinylive(args, input) if not status then print(err) - error( + assert(false, "Error running 'Rscript' command. Perhaps you need to install the 'shinylive' R package?") end @@ -68,7 +84,7 @@ function callShinylive(language, args, input) elseif language == "r" then res = callRShinylive(args, input) else - error("Unknown language: " .. language) + assert(false, "Unknown language: " .. language) end -- Remove any unwanted output before the first curly brace or square bracket. @@ -88,7 +104,10 @@ function callShinylive(language, args, input) if string.len(res) > 100 then res_str = string.sub(res, 1, 100) .. "... [truncated]" end - error("Could not find start curly brace or start brace in " .. language .. " shinylive response:\n" .. res_str) + assert(false, + "\nCould not find start curly brace or start brace in " .. language .. " shinylive response:\n" .. + res_str + ) end if min_start > 1 then res = string.sub(res, min_start) @@ -106,15 +125,7 @@ function callShinylive(language, args, input) print(res) print("Error:") print(err) - if language == "python" then - error("Error decoding JSON response from shinylive.") - elseif language == "r" then - error( - "Error decoding JSON response from shinylive." .. - "\nIf the `shinylive` R package has been installed," .. - " please check that no additional output was printed to the console." - ) - end + assert(false, "Error decoding JSON response from `shinylive` " .. language .. " package.") end return result end @@ -122,11 +133,12 @@ end -- Do one-time setup for language agnostic html dependencies. -- This should only be called once per document -- @param language: "python" or "r" -function ensureInitSetup(language) - if hasDoneSetup.init then +function ensureBaseSetup(language) + -- Quit early if already done + if hasDoneSetup.base then return end - hasDoneSetup.init = true + hasDoneSetup.base = true -- Find the path to codeblock-to-json.ts and save it for later use. local infoObj = callShinylive(language, { "extension", "info" }) @@ -166,7 +178,7 @@ function ensureLanguageSetup(language) local infoObj = callShinylive(language, { "extension", "info" }) versions[language] = { version = infoObj.version, assets_version = infoObj.assets_version } end - -- Verify that the r-shinylive and py-shinylive versions match + -- Verify that the r-shinylive and py-shinylive supported assets versions match if (versions.r and versions.python) and ---@diagnostic disable-next-line: undefined-field @@ -200,7 +212,7 @@ function getShinyliveBaseDeps(language) -- Relative path from the current page to the root of the site. This is needed -- to find out where shinylive-sw.js is, relative to the current page. if quarto.project.offset == nil then - error("The shinylive extension must be used in a Quarto project directory (with a _quarto.yml file).") + assert(false, "The shinylive extension must be used in a Quarto project directory (with a _quarto.yml file).") end local deps = callShinylive( language, From e2d4424a4bf15f3ac4c346b33d9c54eb9c014136 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Wed, 4 Oct 2023 15:49:30 -0400 Subject: [PATCH 08/13] Send language to shinylive assets for proper parsing --- _extensions/quarto-ext/shinylive/shinylive.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/_extensions/quarto-ext/shinylive/shinylive.lua b/_extensions/quarto-ext/shinylive/shinylive.lua index 297ece8..7ba33d5 100644 --- a/_extensions/quarto-ext/shinylive/shinylive.lua +++ b/_extensions/quarto-ext/shinylive/shinylive.lua @@ -245,9 +245,10 @@ return { -- Convert code block to JSON string in the same format as app.json. local parsedCodeblockJson = pandoc.pipe( "quarto", - { "run", codeblockScript }, + { "run", codeblockScript, language }, el.text ) + -- This contains "files" and "quartoArgs" keys. local parsedCodeblock = quarto.json.decode(parsedCodeblockJson) From 8b2598e51b8b3e67c0a7a52d525d78adaaef9ed5 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Wed, 4 Oct 2023 15:50:23 -0400 Subject: [PATCH 09/13] Add minimum python shinylive version support. Add matching r/python shinylive assets support --- .../quarto-ext/shinylive/shinylive.lua | 163 ++++++++++++++++-- 1 file changed, 151 insertions(+), 12 deletions(-) diff --git a/_extensions/quarto-ext/shinylive/shinylive.lua b/_extensions/quarto-ext/shinylive/shinylive.lua index 7ba33d5..8038206 100644 --- a/_extensions/quarto-ext/shinylive/shinylive.lua +++ b/_extensions/quarto-ext/shinylive/shinylive.lua @@ -73,10 +73,14 @@ end -- Returns decoded object -- @param language: "python" or "r" -- @param args, input: see `callPythonShinylive` and `callRShinylive` -function callShinylive(language, args, input) +function callShinylive(language, args, input, parseJson) if input == nil then input = "" end + if parseJson == nil then + parseJson = true + end + local res -- print("Calling " .. language .. " shinylive with args: " .. table.concat(args, " ")) if language == "python" then @@ -87,6 +91,10 @@ function callShinylive(language, args, input) assert(false, "Unknown language: " .. language) end + if not parseJson then + return res + end + -- Remove any unwanted output before the first curly brace or square bracket. -- print("res: " .. string.sub(res, 1, math.min(string.len(res), 100)) .. "...") local curly_start = string.find(res, "{", 0, true) @@ -113,6 +121,7 @@ function callShinylive(language, args, input) res = string.sub(res, min_start) end + -- Decode JSON object local result local status, err = pcall( @@ -130,6 +139,109 @@ function callShinylive(language, args, input) return result end +function parseVersion(versionTxt) + local versionParts = {} + for part in string.gmatch(versionTxt, "%d+") do + table.insert(versionParts, tonumber(part)) + end + local ret = { + major = nil, + minor = nil, + patch = nil, + extra = nil, + length = #versionParts, + str = versionTxt + } + + if ret.length >= 1 then + ret.major = versionParts[1] + if ret.length >= 2 then + ret.minor = versionParts[2] + if ret.length >= 3 then + ret.patch = versionParts[3] + if ret.length >= 4 then + ret.extra = versionParts[4] + end + end + end + end + + return ret +end + +-- If verA > verB, return 1 +-- If verA == verB, return 0 +-- If verA < verB, return -1 +function compareVersions(verA, verB) + if verA.major == nil or verB.major == nil then + assert(false, "Invalid version: " .. verA.str .. " or " .. verB.str) + end + + for index, key in ipairs({ "major", "minor", "patch", "extra" }) do + local partDiff = compareVersionPart(verA[key], verB[key]) + if partDiff ~= 0 then + return partDiff + end + end + + -- Equal! + return 0 +end + +function compareVersionPart(aPart, bPart) + if aPart == nil and bPart == nil then + return 0 + end + if aPart == nil then + return -1 + end + if bPart == nil then + return 1 + end + if aPart > bPart then + return 1 + elseif aPart < bPart then + return -1 + end + + -- Equal! + return 0 +end + +function ensurePyshinyliveVersion(language) + -- Quit early if not python + if language ~= "python" then + return + end + -- Quit early if already completed check + if hasDoneSetup.python_version then + return + end + hasDoneSetup.python_version = true + + -- Verify that min python shinylive version is met + pyShinyliveVersion = callShinylive(language, { "--version" }, "", false) + -- Remove trailing whitespace + pyShinyliveVersion = pyShinyliveVersion:gsub("%s+$", "") + -- Parse version into table + parsedVersion = parseVersion(pyShinyliveVersion) + + -- Verify that the version is at least 0.1.0 + if + (parsedVersion.length < 3) or + -- Major and minor values are 0. Ex: 0.0.18 + (parsedVersion.major == 0 and parsedVersion.minor == 0) + then + assert(nil, + "\nThe shinylive Python package must be at least version v0.1.0 to be used in a quarto document." .. + "\n\nInstalled Python Shinylive package version: " .. pyShinyliveVersion .. + "\n\nPlease upgrade the Python Shinylive package by running:" .. + "\n\tpip install --upgrade shinylive" .. + "\n\n(If you are using a virtual environment, please activate it before running the command above.)" + ) + end +end + -- Do one-time setup for language agnostic html dependencies. -- This should only be called once per document -- @param language: "python" or "r" @@ -166,7 +278,11 @@ end -- This should only be called once per document -- @param language: "python" or "r" function ensureLanguageSetup(language) - ensureInitSetup(language) + -- Min version check must be done first + ensurePyshinyliveVersion(language) + + -- Make sure the base setup is done before the langage setup + ensureBaseSetup(language) if hasDoneSetup[language] then return @@ -184,18 +300,41 @@ function ensureLanguageSetup(language) ---@diagnostic disable-next-line: undefined-field versions.r.assets_version ~= versions.python.assets_version then - error( - "The shinylive R and Python packages must support the same Shinylive Assets version to be used in the same quarto document." .. - "\nR" .. - ---@diagnostic disable-next-line: undefined-field - "\n\tShinylive package version: " .. versions.r.version .. - ---@diagnostic disable-next-line: undefined-field - "\n\tSupported ssets version: " .. versions.r.assets_version .. - "\nPython" .. + local parsedRAssetsVersion = parseVersion(versions.r.assets_version) + local parsedPythonAssetsVersion = parseVersion(versions.python.assets_version) + + local verDiff = compareVersions(parsedRAssetsVersion, parsedPythonAssetsVersion) + local verDiffStr = "" + if verDiff == 1 then + -- R shinylive supports higher version of assets. Upgrade python shinylive + verDiffStr = + "The currently installed python shinylive package supports a lower assets version, " .. + "therefore we recommend updating your python shinylive package to the latest version." + elseif verDiff == -1 then + -- Python shinylive supports higher version of assets. Upgrade R shinylive + verDiffStr = + "The currently installed R shinylive package supports a lower assets version, " .. + "therefore we recommend updating your R shinylive package to the latest version." + end + + assert(false, + "\nThe shinylive R and Python packages must support the same Shinylive Assets version to be used in the same quarto document." .. + "\n" .. + "\nPython shinylive package version: " .. ---@diagnostic disable-next-line: undefined-field - "\n\tShinylive package version: " .. versions.python.version .. + versions.python.version .. " ; Supported assets version: " .. versions.python.assets_version .. + "\nR shinylive package version: " .. ---@diagnostic disable-next-line: undefined-field - "\n\tSupported ssets version: " .. versions.python.assets_version + versions.r.version .. " ; Supported assets version: " .. versions.r.assets_version .. + "\n" .. + "\n" .. verDiffStr .. + "\n" .. + "\nTo update your R Shinylive package, run:" .. + "\n\tR -e \"install.packages('shinylive')\"" .. + "\n" .. + "\nTo update your Python Shinylive package, run:" .. + "\n\tpip install --upgrade shinylive" .. + "\n(If you are using a virtual environment, please activate it before running the command above.)" ) end From 0761bb55c7101025f906ad5d7bcbdc4c7631b97f Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 5 Oct 2023 09:30:12 -0400 Subject: [PATCH 10/13] Bump version to 0.0.5 --- _extensions/quarto-ext/shinylive/_extension.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_extensions/quarto-ext/shinylive/_extension.yml b/_extensions/quarto-ext/shinylive/_extension.yml index 453c84d..f26e630 100644 --- a/_extensions/quarto-ext/shinylive/_extension.yml +++ b/_extensions/quarto-ext/shinylive/_extension.yml @@ -1,7 +1,7 @@ name: shinylive title: Embedded Shinylive applications author: Winston Chang -version: 0.0.4 +version: 0.0.5 quarto-required: ">=1.2.198" contributes: filters: From 2cc95fd71854274227d8acf141d01439ea5744ac Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 5 Oct 2023 14:36:06 -0400 Subject: [PATCH 11/13] Better error message layout --- .../quarto-ext/shinylive/shinylive.lua | 101 ++++++++++++------ 1 file changed, 67 insertions(+), 34 deletions(-) diff --git a/_extensions/quarto-ext/shinylive/shinylive.lua b/_extensions/quarto-ext/shinylive/shinylive.lua index 8038206..eb20faa 100644 --- a/_extensions/quarto-ext/shinylive/shinylive.lua +++ b/_extensions/quarto-ext/shinylive/shinylive.lua @@ -23,6 +23,24 @@ local codeblockScript = nil -- `quarto.doc.attach_to_dependency()` multiple times for the same dependency. local appSpecificDeps = {} +-- Display error message and throw error w/ short message +-- @param msg: string Error message to be displayed +-- @param short_msg: string Error message to be thrown +function throw_quarto_error(err_msg, ...) + n = select("#", ...) + if n > 0 then + -- Display any meta information about the error + -- Add blank lines after msg for line separation for better readability + quarto.log.error(...) + else + quarto.log.error(err_msg .. "\n\n") + end + -- Add blank lines after short_msg for line separation for better readability + -- Use assert(false, msg) to quit the current function execution and + -- bubble up the stack and stop. Barret: I could not get this to work with `error(msg)`. + assert(false, err_msg .. "\n") +end + -- Python specific method to call py-shinylive -- @param args: list of string arguments to pass to py-shinylive -- @param input: string to pipe into to py-shinylive @@ -37,8 +55,12 @@ function callPythonShinylive(args, input) ) if not status then - print(err) - assert(false, "Error running 'shinylive' command. Perhaps you need to install the 'shinylive' Python package?") + throw_quarto_error( + "Error running 'shinylive' command. Perhaps you need to install / update the 'shinylive' Python package?", + "Error running 'shinylive' command. Perhaps you need to install / update the 'shinylive' Python package?\n", + "Error:\n", + err + ) end return res @@ -62,9 +84,12 @@ function callRShinylive(args, input) ) if not status then - print(err) - assert(false, - "Error running 'Rscript' command. Perhaps you need to install the 'shinylive' R package?") + throw_quarto_error( + "Error running 'Rscript' command. Perhaps you need to install / update the 'shinylive' R package?", + "Error running 'Rscript' command. Perhaps you need to install / update the 'shinylive' R package?\n", + "Error:\n", + err + ) end return res @@ -82,13 +107,13 @@ function callShinylive(language, args, input, parseJson) end local res - -- print("Calling " .. language .. " shinylive with args: " .. table.concat(args, " ")) + -- print("Calling " .. language .. " shinylive with args: ", args) if language == "python" then res = callPythonShinylive(args, input) elseif language == "r" then res = callRShinylive(args, input) else - assert(false, "Unknown language: " .. language) + throw_quarto_error("internal - Unknown language: " .. language) end if not parseJson then @@ -112,8 +137,11 @@ function callShinylive(language, args, input, parseJson) if string.len(res) > 100 then res_str = string.sub(res, 1, 100) .. "... [truncated]" end - assert(false, - "\nCould not find start curly brace or start brace in " .. language .. " shinylive response:\n" .. + throw_quarto_error( + "Could not find start curly brace or start brace in " .. + language .. " shinylive response. Is JSON being returned from the " .. language .. " `shinylive` package?", + "Could not find start curly brace or start brace in " .. language .. " shinylive response.\n", + "JSON string being parsed:\n", res_str ) end @@ -130,11 +158,14 @@ function callShinylive(language, args, input, parseJson) end ) if not status then - print("JSON string being parsed:") - print(res) - print("Error:") - print(err) - assert(false, "Error decoding JSON response from `shinylive` " .. language .. " package.") + throw_quarto_error( + "Error decoding JSON response from `shinylive` " .. language .. " package.", + "Error decoding JSON response from `shinylive` " .. language .. " package.\n", + "JSON string being parsed:\n", + res, + "Error:\n", + err + ) end return result end @@ -174,7 +205,7 @@ end -- If verA < verB, return -1 function compareVersions(verA, verB) if verA.major == nil or verB.major == nil then - assert(false, "Invalid version: " .. verA.str .. " or " .. verB.str) + throw_quarto_error("Trying to compare an invalid version: " .. verA.str .. " or " .. verB.str) end for index, key in ipairs({ "major", "minor", "patch", "extra" }) do @@ -232,8 +263,8 @@ function ensurePyshinyliveVersion(language) -- Major and minor values are 0. Ex: 0.0.18 (parsedVersion.major == 0 and parsedVersion.minor == 0) then - assert(nil, - "\nThe shinylive Python package must be at least version v0.1.0 to be used in a quarto document." .. + assert(false, + "\nThe shinylive Python package must be at least version v0.1.0 to be used in a Quarto document." .. "\n\nInstalled Python Shinylive package version: " .. pyShinyliveVersion .. "\n\nPlease upgrade the Python Shinylive package by running:" .. "\n\tpip install --upgrade shinylive" .. @@ -317,24 +348,26 @@ function ensureLanguageSetup(language) "therefore we recommend updating your R shinylive package to the latest version." end - assert(false, - "\nThe shinylive R and Python packages must support the same Shinylive Assets version to be used in the same quarto document." .. - "\n" .. - "\nPython shinylive package version: " .. + throw_quarto_error( + "The shinylive R and Python packages must support the same Shinylive Assets version to be used in the same Quarto document.", + "The shinylive R and Python packages must support the same Shinylive Assets version to be used in the same Quarto document.\n", + "\n", + "Python shinylive package version: ", ---@diagnostic disable-next-line: undefined-field - versions.python.version .. " ; Supported assets version: " .. versions.python.assets_version .. - "\nR shinylive package version: " .. + versions.python.version .. " ; Supported assets version: " .. versions.python.assets_version .. "\n", + "R shinylive package version: " .. ---@diagnostic disable-next-line: undefined-field - versions.r.version .. " ; Supported assets version: " .. versions.r.assets_version .. - "\n" .. - "\n" .. verDiffStr .. - "\n" .. - "\nTo update your R Shinylive package, run:" .. - "\n\tR -e \"install.packages('shinylive')\"" .. - "\n" .. - "\nTo update your Python Shinylive package, run:" .. - "\n\tpip install --upgrade shinylive" .. - "\n(If you are using a virtual environment, please activate it before running the command above.)" + versions.r.version .. " ; Supported assets version: " .. versions.r.assets_version .. "\n", + "\n", + verDiffStr .. "\n", + "\n", + "To update your R Shinylive package, run:\n", + "\tR -e \"install.packages('shinylive')\"\n", + "\n", + "To update your Python Shinylive package, run:\n", + "\tpip install --upgrade shinylive\n", + "(If you are using a virtual environment, please activate it before running the command above.)\n", + "\n" ) end @@ -351,7 +384,7 @@ function getShinyliveBaseDeps(language) -- Relative path from the current page to the root of the site. This is needed -- to find out where shinylive-sw.js is, relative to the current page. if quarto.project.offset == nil then - assert(false, "The shinylive extension must be used in a Quarto project directory (with a _quarto.yml file).") + throw_quarto_error("The `shinylive` extension must be used in a Quarto project directory (with a _quarto.yml file).") end local deps = callShinylive( language, From 25d8e6d15ed61644d87112b6cd38a7b2b258e520 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 5 Oct 2023 14:38:26 -0400 Subject: [PATCH 12/13] Bump version to 0.1.0 --- _extensions/quarto-ext/shinylive/_extension.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_extensions/quarto-ext/shinylive/_extension.yml b/_extensions/quarto-ext/shinylive/_extension.yml index f26e630..01b4d68 100644 --- a/_extensions/quarto-ext/shinylive/_extension.yml +++ b/_extensions/quarto-ext/shinylive/_extension.yml @@ -1,7 +1,7 @@ name: shinylive title: Embedded Shinylive applications author: Winston Chang -version: 0.0.5 +version: 0.1.0 quarto-required: ">=1.2.198" contributes: filters: From a1807f034753ad4f22b1eed32b8fe6f759a3e356 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Thu, 5 Oct 2023 14:48:21 -0400 Subject: [PATCH 13/13] Update shinylive.lua --- _extensions/quarto-ext/shinylive/shinylive.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/_extensions/quarto-ext/shinylive/shinylive.lua b/_extensions/quarto-ext/shinylive/shinylive.lua index eb20faa..e2828db 100644 --- a/_extensions/quarto-ext/shinylive/shinylive.lua +++ b/_extensions/quarto-ext/shinylive/shinylive.lua @@ -10,7 +10,6 @@ -- `\n` to keep the message in the same readable area. - -- `table` to organize flags to have code only run once. local hasDoneSetup = { base = false, r = false, python = false, python_version = false } -- `table` to store `{ version, assets_version }` for each language's extension.