├── .Rbuildignore
├── .gitignore
├── DESCRIPTION
├── LICENSE
├── LICENSE.md
├── NAMESPACE
├── R
└── assist.R
├── README.md
├── aidea.Rproj
├── example.R
├── inst
└── app
│ ├── prompt.md
│ ├── quarto
│ ├── _extensions
│ │ └── r-wasm
│ │ │ └── live
│ │ │ ├── _extension.yml
│ │ │ ├── _gradethis.qmd
│ │ │ ├── _knitr.qmd
│ │ │ ├── live.lua
│ │ │ ├── resources
│ │ │ ├── live-runtime.css
│ │ │ ├── live-runtime.js
│ │ │ ├── pyodide-worker.js
│ │ │ └── tinyyaml.lua
│ │ │ └── templates
│ │ │ ├── interpolate.ojs
│ │ │ ├── pyodide-editor.ojs
│ │ │ ├── pyodide-evaluate.ojs
│ │ │ ├── pyodide-exercise.ojs
│ │ │ ├── pyodide-setup.ojs
│ │ │ ├── webr-editor.ojs
│ │ │ ├── webr-evaluate.ojs
│ │ │ ├── webr-exercise.ojs
│ │ │ ├── webr-setup.ojs
│ │ │ └── webr-widget.ojs
│ └── quarto-live-template.qmd
│ └── www
│ └── helpers.js
└── man
└── assist.Rd
/.Rbuildignore:
--------------------------------------------------------------------------------
1 | ^LICENSE\.md$
2 | ^.*\.Rproj$
3 | ^\.Rproj\.user$
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .Rproj.user
2 |
--------------------------------------------------------------------------------
/DESCRIPTION:
--------------------------------------------------------------------------------
1 | Package: aidea
2 | Title: What the Package Does (One Line, Title Case)
3 | Version: 0.0.0.9000
4 | Authors@R:
5 | person("Carson", "Sievert", , "carson@posit.co", role = c("aut", "cre"))
6 | Description: What the package does (one paragraph).
7 | License: MIT + file LICENSE
8 | Encoding: UTF-8
9 | Roxygen: list(markdown = TRUE)
10 | RoxygenNote: 7.3.2
11 | Imports:
12 | bslib,
13 | glue,
14 | shiny,
15 | skimr,
16 | quarto,
17 | jsonlite,
18 | withr,
19 | ellmer,
20 | shinychat
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | YEAR: 2024
2 | COPYRIGHT HOLDER: aidea authors
3 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright (c) 2024 aidea authors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/NAMESPACE:
--------------------------------------------------------------------------------
1 | # Generated by roxygen2: do not edit by hand
2 |
3 | export(assist)
4 | import(bslib)
5 | import(ellmer)
6 | import(glue)
7 | import(jsonlite)
8 | import(quarto)
9 | import(shiny)
10 | import(shinychat)
11 | import(withr)
12 |
--------------------------------------------------------------------------------
/R/assist.R:
--------------------------------------------------------------------------------
1 | #' @import shiny
2 | #' @import bslib
3 | #' @import shinychat
4 | #' @import ellmer
5 | #' @import quarto
6 | #' @import glue
7 | #' @import jsonlite
8 | #' @import withr
9 | NULL
10 |
11 | #' Launch the AI-powered EDA assistant
12 | #'
13 | #' @param data A data frame.
14 | #' @param chat A [ellmer::Chat] instance (e.g., `ellmer::chat_ollama()`).
15 | #' Be aware that any `system_prompt` will be overwritten.
16 | #'
17 | #' @export
18 | #' @examples
19 | #'
20 | #' data(diamonds, package = "ggplot2")
21 | #' assist(diamonds)
22 | assist <- function(data, chat = NULL) {
23 | data_name <- as.character(substitute(data))
24 |
25 | # Directory holding assets for the app
26 | app_dir <- system.file("app", package = "aidea")
27 |
28 | # Get the prompt and start the chat
29 | prompt_template <- paste(
30 | readLines(file.path(app_dir, "prompt.md")),
31 | collapse = "\n"
32 | )
33 | prompt <- glue::glue(prompt_template)
34 |
35 | if (is.null(chat)) {
36 | chat <- ellmer::chat_openai(
37 | system_prompt = prompt,
38 | api_args = list(temperature = 0),
39 | )
40 | } else {
41 | chat$set_system_prompt(prompt)
42 | }
43 |
44 | # Make JS/CSS assets available
45 | shiny::addResourcePath("www", file.path(app_dir, "www"))
46 |
47 | # For the Quarto portion, set up a tempdir where the
48 | # doc will render and supporting files will live
49 | user_dir <- tempfile()
50 | dir.create(user_dir)
51 | on.exit(unlink(user_dir), add = TRUE)
52 |
53 | # Make the data available to the Quarto doc
54 | saveRDS(data, file.path(user_dir, "data.rds"))
55 |
56 | # Copy quarto extensions over to the user dir (for rendering)
57 | quarto_dir <- file.path(app_dir, "quarto")
58 | file.copy(
59 | file.path(quarto_dir, "_extensions"),
60 | user_dir,
61 | recursive = TRUE,
62 | overwrite = TRUE
63 | )
64 | # Grab the quarto template (which will be filled in with code suggestions)
65 | quarto_template <- paste(
66 | readLines(file.path(quarto_dir, "quarto-live-template.qmd")),
67 | collapse = "\n"
68 | )
69 | # Need to make the Quarto assets available for the iframe
70 | shiny::addResourcePath("quarto-assets", user_dir)
71 |
72 | ui <- page_sidebar(
73 | shinychat::chat_ui("chat"), # TODO: shinychat doesn't work with dynamic UI
74 | tags$script(src = "www/helpers.js"),
75 | div(
76 | class = "offcanvas offcanvas-start",
77 | tabindex = "-1",
78 | id = "offcanvas-interpret",
79 | "data-bs-scroll" = "true",
80 | div(
81 | class = "offcanvas-header",
82 | uiOutput("interpret_title"),
83 | tags$button(
84 | type = "button",
85 | class = "btn-close",
86 | `data-bs-dismiss` = "offcanvas",
87 | `aria-label` = "Close"
88 | )
89 | ),
90 | div(
91 | class = "offcanvas-body",
92 | tags$style("#offcanvas-interpret shiny-chat-input {display: none;}"),
93 | shinychat::chat_ui("chat_interpret")
94 | )
95 | ),
96 | title = "EDA with R assistant 🤖",
97 | sidebar = sidebar(
98 | open = FALSE,
99 | position = "right",
100 | width = "35%",
101 | style = "height:100%;",
102 | gap = 0,
103 | id = "sidebar-repl",
104 | uiOutput("editor", fill = TRUE),
105 | actionButton(
106 | "interpret_editor_results",
107 | "Interpret results",
108 | icon = icon("wand-sparkles"),
109 | disabled = TRUE,
110 | "data-bs-toggle" = "offcanvas",
111 | "data-bs-target" = "#offcanvas-interpret",
112 | "aria-controls" = "offcanvas-interpret"
113 | )
114 | ),
115 | theme = bslib::bs_theme(
116 | "offcanvas-horizontal-width" = "600px",
117 | "offcanvas-backdrop-opacity" = 0
118 | )
119 | )
120 |
121 | server <- function(input, output, session) {
122 | # Welcome message for the chat
123 | init_response <- chat$stream_async(
124 | paste("Tell me about the", data_name, "dataset")
125 | )
126 | shinychat::chat_append("chat", init_response)
127 |
128 | # When input is submitted, append to chat, and create artifact buttons
129 | observeEvent(input$chat_user_input, {
130 | stream <- chat$stream_async(
131 | input$chat_user_input,
132 | !!!lapply(input$file, ellmer::content_image_file)
133 | )
134 | shinychat::chat_append("chat", stream)
135 | })
136 |
137 | editor_code <- reactive({
138 | input$editor_code
139 | })
140 |
141 | interpret_title <- reactiveVal(NULL)
142 |
143 | observeEvent(editor_code(), {
144 | bslib::sidebar_toggle("sidebar-repl", open = TRUE)
145 |
146 | res <- chat$chat(
147 | "I've selected the following code to run in an R console.",
148 | paste("```r", editor_code(), "```"),
149 | "Provide to me a short summary title capturing the main idea of this code does. ",
150 | "It'll get used in the UI so that the user can refer back to it later.",
151 | "Don't bother with putting a Title: prefix or markdown formatting, just the title."
152 | )
153 |
154 | interpret_title(as.character(res))
155 | })
156 |
157 | output$interpret_title <- renderUI({
158 | req(interpret_title())
159 | tags$h5(interpret_title())
160 | })
161 |
162 | output$editor <- renderUI({
163 | code <- paste(editor_code(), collapse = "\n")
164 | validate(
165 | need(
166 | nzchar(code),
167 | "No code suggestions made yet. Try asking a question that produces a code suggestion and click the 'Run this code' button."
168 | )
169 | )
170 |
171 | quarto_src <- glue::glue(
172 | quarto_template,
173 | .open = "$$$",
174 | .close = "$$$",
175 | )
176 |
177 | withr::with_dir(user_dir, {
178 | writeLines(quarto_src, "quarto-live.qmd")
179 | quarto::quarto_render("quarto-live.qmd")
180 | })
181 |
182 | tags$iframe(
183 | src = "quarto-assets/quarto-live.html",
184 | width = "100%",
185 | height = "400px",
186 | frameborder = "0",
187 | class = "html-fill-item"
188 | )
189 | })
190 |
191 | results_have_interpretation <- reactiveVal(FALSE)
192 |
193 | observeEvent(editor_results(), {
194 | updateActionButton(
195 | inputId = "interpret_editor_results",
196 | disabled = FALSE
197 | )
198 | results_have_interpretation(FALSE)
199 | })
200 |
201 | editor_results <- reactive({
202 | req(input$editor_results)
203 | results <- jsonlite::fromJSON(input$editor_results, simplifyDataFrame = FALSE)
204 | lapply(results, function(x) {
205 | if (x$type == "image") {
206 | ellmer::content_image_url(x$content)
207 | } else if (x$type == "text") {
208 | x$content
209 | } else {
210 | x
211 | }
212 | })
213 | })
214 |
215 | observeEvent(input$interpret_editor_results, {
216 |
217 | if (results_have_interpretation()) {
218 | return()
219 | }
220 |
221 | results_have_interpretation(TRUE)
222 |
223 | # TODO: shinychat needs a way to clear the chat
224 | shiny::removeUI(
225 | selector = "#chat_interpret shiny-chat-message",
226 | multiple = TRUE,
227 | )
228 |
229 | chat_input <- rlang::list2(
230 | "The following code:",
231 | "```r",
232 | editor_code(),
233 | "```",
234 | "Has created the following results:",
235 | !!!editor_results(),
236 | "Interpret these results, and offer suggestions for next steps."
237 | )
238 |
239 | stream <- chat$stream_async(!!!chat_input)
240 |
241 | shinychat::chat_append(
242 | "chat_interpret",
243 | stream
244 | )
245 | })
246 | }
247 |
248 | shinyApp(ui, server)
249 | }
250 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # aidea
2 |
3 | Combine the power of LLMs and R to help guide exploration of a dataset.
4 |
5 | DISCLAIMER: This package is a proof of concept and was created for a 2-day hackathon. It's currently just a fun side project. Don't use it for anything serious.
6 |
7 | ## Installation
8 |
9 | You can install the development version of aidea from GitHub with:
10 |
11 | ```r
12 | remotes::install_github("cpsievert/aidea")
13 | ```
14 |
15 | ## Prerequisites
16 |
17 | To use this package, you'll also need credentials for the LLM that powers `assist()`.
18 |
19 | By default, `assist()` uses OpenAI, so you'll need to set an environment variable named `OPENAI_API_KEY` using the key from https://platform.openai.com/account/api-keys
20 |
21 | We recommend setting that variable via `usethis::edit_r_environ()`. See [`{ellmer}`](https://github.com/hadley/ellmer/?tab=readme-ov-file#prerequisites)'s prerequisites if you plan on using a different model.
22 |
23 | ## Usage
24 |
25 | This package currently contains just one function, `assist()`, which takes a data frame as input, and provides a chat bot experience tailored for that dataset:
26 |
27 | ```r
28 | # Load a dataset
29 | data(diamonds, package = "ggplot2")
30 | # Start the aidea app assistant
31 | aidea::assist(diamonds)
32 | ```
33 |
34 | You'll be welcomed with overview of what's in the data (e.g., interesting summary stats, variable types, etc) as well as some questions to ask about the data.
35 |
36 |
37 |
38 |
39 |
40 | When you ask a question about the data, it'll offer R code to assist in answering that question.
41 |
42 | That R code will include an option to run the code in browser:
43 |
44 |
45 |
46 |
47 |
48 | When clicked, the code is run in a sidebar, and results displayed below the interactive code editor.
49 |
50 |
51 |
52 | When you're unsure of how to interpret the results, press the interpret button. This will open an additional sidebar with an interpretation of the current results:
53 |
54 |
55 |
56 |
57 | ## How does it work?
58 |
59 | This package uses a combination of [`{ellmer}`](https://github.com/hadley/ellmer) and [`{shinychat}`](https://github.com/jcheng5/shinychat/) to provide the LLM assisted chatbot experience.
60 | It **does not** send all of your data to the LLM, just basic summary stats (e.g., number of rows/columns) and data characteristics (e.g., variable types).
61 | It will, however, send any results you choose to interpret to the LLM.
62 | If you are worried about privacy, consider using a local model (i.e., `assist(chat = ellmer::chat_ollama())`) instead of OpenAI
63 |
--------------------------------------------------------------------------------
/aidea.Rproj:
--------------------------------------------------------------------------------
1 | Version: 1.0
2 | ProjectId: 04e61bf8-4d8d-42c5-aca6-5c36dd86e19a
3 |
4 | RestoreWorkspace: No
5 | SaveWorkspace: No
6 | AlwaysSaveHistory: Default
7 |
8 | EnableCodeIndexing: Yes
9 | UseSpacesForTab: Yes
10 | NumSpacesForTab: 4
11 | Encoding: UTF-8
12 |
13 | RnwWeave: Sweave
14 | LaTeX: pdfLaTeX
15 |
16 | AutoAppendNewline: Yes
17 | StripTrailingWhitespace: Yes
18 | LineEndingConversion: Posix
19 |
20 | BuildType: Package
21 | PackageUseDevtools: Yes
22 | PackageInstallArgs: --no-multiarch --with-keep.source
23 |
--------------------------------------------------------------------------------
/example.R:
--------------------------------------------------------------------------------
1 | df <- data.frame(
2 | x = rnorm(100),
3 | y = rnorm(100)
4 | )
5 |
6 | aidea::assist(df)
7 |
--------------------------------------------------------------------------------
/inst/app/prompt.md:
--------------------------------------------------------------------------------
1 | You are assisting the user with exploration of a dataset loaded into the R programming language. Assume the user is relatively new to R, and you are helping them learn about R while also learning about the dataset.
2 |
3 | The name of the dataset is: { data_name }. Whenever referring to the dataset in an answer, wrap the name in backticks (e.g., `name`).
4 |
5 | The name of the columns are: { paste(names(data), collapse = ",") }
6 |
7 | The corresponding data types of those columns are: { paste(vapply(data, function(x) { paste(class(x), collapse = "-") }, character(1)), collapse = ", ") }
8 |
9 | Some summary statistics include:
10 |
11 | ```
12 | { paste(capture.output(skimr::skim(data)), collapse = "\n") }
13 | ```
14 |
15 | Your first response is actually the first message the user sees when they start exploring the dataset (i.e., the 1st user message you receive isn't actually from the user), so it's important to provide a welcoming and informative response that isn't too overwhelming.
16 | Avoid detailed descriptions of variables in the dataset (since the user likely has that context, but you don't), but also highlight key numerical summaries and aspects of the dataset that may help guide further analysis.
17 | Also, for your information, it's not interesting to say the dataset "has summary statistics" since that's a given. Instead, focus on the most interesting aspects of the dataset that will help guide the user's exploration.
18 | Finish this initial response by providing some example questions that will help the user get started with exploring the dataset.
19 | Also, if you don't much about the dataset information provided, it's okay to say that and ask the user to provide more context before offering further help.
20 |
21 | When you do receive questions about the data, include R code that can be executed on the dataset provided (i.e., { data_name }), and don't pretend to know more than you do since you likely will only have access to summary statistics about the dataset.
22 | The user will likely copy/paste your answer to produce the result, and return back to you with those results to ask further questions.
23 | It is VERY IMPORTANT that every single code block includes all the necessary library imports, even if it becomes repetitive. This is because users will have the ability to easily copy/paste/run each code snippet independently in a fresh R session.
24 | You may assume, however, that the dataset is already loaded in the user's R environment.
25 |
26 | Your R code solutions should prefer use of tidyverse functions (e.g., dplyr, ggplot2) and other packages that are commonly used in the R community. If you are not sure about the best way to solve a problem, feel free to ask for help from the community.
27 |
--------------------------------------------------------------------------------
/inst/app/quarto/_extensions/r-wasm/live/_extension.yml:
--------------------------------------------------------------------------------
1 | title: Quarto Live
2 | author: George Stagg
3 | version: 0.1.2-dev
4 | quarto-required: ">=1.4.0"
5 | contributes:
6 | filters:
7 | - live.lua
8 | formats:
9 | common:
10 | ojs-engine: true
11 | filters:
12 | - live.lua
13 | html: default
14 | revealjs: default
15 | dashboard: default
16 |
--------------------------------------------------------------------------------
/inst/app/quarto/_extensions/r-wasm/live/_gradethis.qmd:
--------------------------------------------------------------------------------
1 | ```{webr}
2 | #| edit: false
3 | #| output: false
4 | webr::install("gradethis", quiet = TRUE)
5 | library(gradethis)
6 | options(webr.exercise.checker = function(
7 | label, user_code, solution_code, check_code, envir_result, evaluate_result,
8 | envir_prep, last_value, engine, stage, ...
9 | ) {
10 | if (is.null(check_code)) {
11 | # No grading code, so just skip grading
12 | invisible(NULL)
13 | } else if (is.null(label)) {
14 | list(
15 | correct = FALSE,
16 | type = "warning",
17 | message = "All exercises must have a label."
18 | )
19 | } else if (is.null(solution_code)) {
20 | list(
21 | correct = FALSE,
22 | type = "warning",
23 | message = htmltools::tags$div(
24 | htmltools::tags$p("A problem occurred grading this exercise."),
25 | htmltools::tags$p(
26 | "No solution code was found. Note that grading exercises using the ",
27 | htmltools::tags$code("gradethis"),
28 | "package requires a model solution to be included in the document."
29 | )
30 | )
31 | )
32 | } else {
33 | gradethis::gradethis_exercise_checker(
34 | label = label, solution_code = solution_code, user_code = user_code,
35 | check_code = check_code, envir_result = envir_result,
36 | evaluate_result = evaluate_result, envir_prep = envir_prep,
37 | last_value = last_value, stage = stage, engine = engine)
38 | }
39 | })
40 | ```
41 |
--------------------------------------------------------------------------------
/inst/app/quarto/_extensions/r-wasm/live/_knitr.qmd:
--------------------------------------------------------------------------------
1 | ```{r echo=FALSE}
2 | # Setup knitr for handling {webr} and {pyodide} blocks
3 | # TODO: With quarto-dev/quarto-cli#10169, we can implement this in a filter
4 |
5 | # We'll handle `include: false` in Lua, always include cell in knitr output
6 | knitr::opts_hooks$set(include = function(options) {
7 | if (options$engine == "webr" || options$engine == "pyodide" ) {
8 | options$include <- TRUE
9 | }
10 | options
11 | })
12 |
13 | # Passthrough engine for webr
14 | knitr::knit_engines$set(webr = function(options) {
15 | knitr:::one_string(c(
16 | "```{webr}",
17 | options$yaml.code,
18 | options$code,
19 | "```"
20 | ))
21 | })
22 |
23 | # Passthrough engine for pyodide
24 | knitr::knit_engines$set(pyodide = function(options) {
25 | knitr:::one_string(c(
26 | "```{pyodide}",
27 | options$yaml.code,
28 | options$code,
29 | "```"
30 | ))
31 | })
32 | ```
33 |
--------------------------------------------------------------------------------
/inst/app/quarto/_extensions/r-wasm/live/live.lua:
--------------------------------------------------------------------------------
1 | local tinyyaml = require "resources/tinyyaml"
2 |
3 | local cell_options = {
4 | webr = { eval = true },
5 | pyodide = { eval = true },
6 | }
7 |
8 | local live_options = {
9 | ["show-solutions"] = true,
10 | ["show-hints"] = true,
11 | ["grading"] = true,
12 | }
13 |
14 | local ojs_definitions = {
15 | contents = {},
16 | }
17 | local block_id = 0
18 |
19 | local include_webr = false
20 | local include_pyodide = false
21 |
22 | local function json_as_b64(obj)
23 | local json_string = quarto.json.encode(obj)
24 | return quarto.base64.encode(json_string)
25 | end
26 |
27 | local function tree(root)
28 | function isdir(path)
29 | -- Is there a better OS agnostic way to do this?
30 | local ok, err, code = os.rename(path .. "/", path .. "/")
31 | if not ok then
32 | if code == 13 then
33 | -- Permission denied, but it exists
34 | return true
35 | end
36 | end
37 | return ok, err
38 | end
39 |
40 | function gather(path, list)
41 | if (isdir(path)) then
42 | -- For each item in this dir, recurse for subdir content
43 | local items = pandoc.system.list_directory(path)
44 | for _, item in pairs(items) do
45 | gather(path .. "/" .. item, list)
46 | end
47 | else
48 | -- This is a file, add it to the table directly
49 | table.insert(list, path)
50 | end
51 | return list
52 | end
53 |
54 | return gather(root, {})
55 | end
56 |
57 | function ParseBlock(block, engine)
58 | local attr = {}
59 | local param_lines = {}
60 | local code_lines = {}
61 | for line in block.text:gmatch("([^\r\n]*)[\r\n]?") do
62 | local param_line = string.find(line, "^#|")
63 | if (param_line ~= nil) then
64 | table.insert(param_lines, string.sub(line, 4))
65 | else
66 | table.insert(code_lines, line)
67 | end
68 | end
69 | local code = table.concat(code_lines, "\n")
70 |
71 | -- Include cell-options defaults
72 | for k, v in pairs(cell_options[engine]) do
73 | attr[k] = v
74 | end
75 |
76 | -- Parse quarto-style yaml attributes
77 | local param_yaml = table.concat(param_lines, "\n")
78 | if (param_yaml ~= "") then
79 | param_attr = tinyyaml.parse(param_yaml)
80 | for k, v in pairs(param_attr) do
81 | attr[k] = v
82 | end
83 | end
84 |
85 | -- Parse traditional knitr-style attributes
86 | for k, v in pairs(block.attributes) do
87 | local function toboolean(v)
88 | return string.lower(v) == "true"
89 | end
90 |
91 | local convert = {
92 | autorun = toboolean,
93 | runbutton = toboolean,
94 | echo = toboolean,
95 | edit = toboolean,
96 | error = toboolean,
97 | eval = toboolean,
98 | include = toboolean,
99 | output = toboolean,
100 | startover = toboolean,
101 | solution = toboolean,
102 | warning = toboolean,
103 | timelimit = tonumber,
104 | ["fig-width"] = tonumber,
105 | ["fig-height"] = tonumber,
106 | }
107 |
108 | if (convert[k]) then
109 | attr[k] = convert[k](v)
110 | else
111 | attr[k] = v
112 | end
113 | end
114 |
115 | -- When echo: false: disable the editor
116 | if (attr.echo == false) then
117 | attr.edit = false
118 | end
119 |
120 | -- When `include: false`: disable the editor, source block echo, and output
121 | if (attr.include == false) then
122 | attr.edit = false
123 | attr.echo = false
124 | attr.output = false
125 | end
126 |
127 | -- If we're not executing anything, there's no point showing an editor
128 | if (attr.edit == nil) then
129 | attr.edit = attr.eval
130 | end
131 |
132 | return {
133 | code = code,
134 | attr = attr
135 | }
136 | end
137 |
138 | local exercise_keys = {}
139 | function assertUniqueExercise(key)
140 | if (exercise_keys[key]) then
141 | error("Document contains multiple exercises with key `" .. tostring(key) ..
142 | "`." .. "Exercise keys must be unique.")
143 | end
144 | exercise_keys[key] = true
145 | end
146 |
147 | function assertBlockExercise(type, engine, block)
148 | if (not block.attr.exercise) then
149 | error("Can't create `" .. engine .. "` " .. type ..
150 | " block, `exercise` not defined in cell options.")
151 | end
152 | end
153 |
154 | function ExerciseDataBlocks(btype, block)
155 | local ex = block.attr.exercise
156 | if (type(ex) ~= "table") then
157 | ex = { ex }
158 | end
159 |
160 | local blocks = {}
161 | for idx, ex_id in pairs(ex) do
162 | blocks[idx] = pandoc.RawBlock(
163 | "html",
164 | ""
166 | )
167 | end
168 | return blocks
169 | end
170 |
171 | function PyodideCodeBlock(code)
172 | block_id = block_id + 1
173 |
174 | function append_ojs_template(template, template_vars)
175 | local file = io.open(quarto.utils.resolve_path("templates/" .. template), "r")
176 | assert(file)
177 | local content = file:read("*a")
178 | for k, v in pairs(template_vars) do
179 | content = string.gsub(content, "{{" .. k .. "}}", v)
180 | end
181 |
182 | table.insert(ojs_definitions.contents, 1, {
183 | methodName = "interpret",
184 | cellName = "pyodide-" .. block_id,
185 | inline = false,
186 | source = content,
187 | })
188 | end
189 |
190 | -- Parse codeblock contents for YAML header and Python code body
191 | local block = ParseBlock(code, "pyodide")
192 |
193 | if (block.attr.output == "asis") then
194 | quarto.log.warning(
195 | "For `pyodide` code blocks, using `output: asis` renders Python output as HTML.",
196 | "Markdown rendering is not currently supported."
197 | )
198 | end
199 |
200 | -- Supplementary execise blocks: setup, check, hint, solution
201 | if (block.attr.setup) then
202 | assertBlockExercise("setup", "pyodide", block)
203 | return ExerciseDataBlocks("setup", block)
204 | end
205 |
206 | if (block.attr.check) then
207 | assertBlockExercise("check", "pyodide", block)
208 | if live_options["grading"] then
209 | return ExerciseDataBlocks("check", block)
210 | else
211 | return {}
212 | end
213 | end
214 |
215 | if (block.attr.hint) then
216 | assertBlockExercise("hint", "pyodide", block)
217 | if live_options["show-hints"] then
218 | return pandoc.Div(
219 | InterpolatedBlock(
220 | pandoc.CodeBlock(block.code, pandoc.Attr('', { 'python', 'cell-code' })),
221 | "python"
222 | ),
223 | pandoc.Attr('',
224 | { 'pyodide-ojs-exercise', 'exercise-hint', 'd-none' },
225 | { exercise = block.attr.exercise }
226 | )
227 | )
228 | end
229 | return {}
230 | end
231 |
232 | if (block.attr.solution) then
233 | assertBlockExercise("solution", "pyodide", block)
234 | if live_options["show-solutions"] then
235 | local plaincode = pandoc.Code(block.code, pandoc.Attr('', { 'solution-code', 'd-none' }))
236 | local codeblock = pandoc.CodeBlock(block.code, pandoc.Attr('', { 'python', 'cell-code' }))
237 | return pandoc.Div(
238 | {
239 | InterpolatedBlock(plaincode, "none"),
240 | InterpolatedBlock(codeblock, "python"),
241 | },
242 | pandoc.Attr('',
243 | { 'pyodide-ojs-exercise', 'exercise-solution', 'd-none' },
244 | { exercise = block.attr.exercise }
245 | )
246 | )
247 | end
248 | return {}
249 | end
250 |
251 | -- Prepare OJS attributes
252 | local input = "{" .. table.concat(block.attr.input or {}, ", ") .. "}"
253 | local ojs_vars = {
254 | block_id = block_id,
255 | block_input = input,
256 | }
257 |
258 | -- Render appropriate OJS for the type of client-side block we're working with
259 | local ojs_source = nil
260 | if (block.attr.exercise) then
261 | -- Primary interactive exercise block
262 | assertUniqueExercise(block.attr.exercise)
263 | ojs_source = "pyodide-exercise.ojs"
264 | elseif (block.attr.edit) then
265 | -- Editable non-exercise sandbox block
266 | ojs_source = "pyodide-editor.ojs"
267 | else
268 | -- Non-interactive evaluation block
269 | ojs_source = "pyodide-evaluate.ojs"
270 | end
271 |
272 | append_ojs_template(ojs_source, ojs_vars)
273 |
274 | return pandoc.Div({
275 | pandoc.Div({}, pandoc.Attr("pyodide-" .. block_id, { 'exercise-cell' })),
276 | pandoc.RawBlock(
277 | "html",
278 | ""
280 | )
281 | })
282 | end
283 |
284 | function WebRCodeBlock(code)
285 | block_id = block_id + 1
286 |
287 | function append_ojs_template(template, template_vars)
288 | local file = io.open(quarto.utils.resolve_path("templates/" .. template), "r")
289 | assert(file)
290 | local content = file:read("*a")
291 | for k, v in pairs(template_vars) do
292 | content = string.gsub(content, "{{" .. k .. "}}", v)
293 | end
294 |
295 | table.insert(ojs_definitions.contents, 1, {
296 | methodName = "interpret",
297 | cellName = "webr-" .. block_id,
298 | inline = false,
299 | source = content,
300 | })
301 | end
302 |
303 | -- Parse codeblock contents for YAML header and R code body
304 | local block = ParseBlock(code, "webr")
305 |
306 | if (block.attr.output == "asis") then
307 | quarto.log.warning(
308 | "For `webr` code blocks, using `output: asis` renders R output as HTML.",
309 | "Markdown rendering is not currently supported."
310 | )
311 | end
312 |
313 | -- Supplementary execise blocks: setup, check, hint, solution
314 | if (block.attr.setup) then
315 | assertBlockExercise("setup", "webr", block)
316 | return ExerciseDataBlocks("setup", block)
317 | end
318 |
319 | if (block.attr.check) then
320 | assertBlockExercise("check", "webr", block)
321 | if live_options["grading"] then
322 | return ExerciseDataBlocks("check", block)
323 | else
324 | return {}
325 | end
326 | end
327 |
328 | if (block.attr.hint) then
329 | assertBlockExercise("hint", "webr", block)
330 | if live_options["show-hints"] then
331 | return pandoc.Div(
332 | InterpolatedBlock(
333 | pandoc.CodeBlock(block.code, pandoc.Attr('', { 'r', 'cell-code' })),
334 | "r"
335 | ),
336 | pandoc.Attr('',
337 | { 'webr-ojs-exercise', 'exercise-hint', 'd-none' },
338 | { exercise = block.attr.exercise }
339 | )
340 | )
341 | end
342 | return {}
343 | end
344 |
345 | if (block.attr.solution) then
346 | assertBlockExercise("solution", "webr", block)
347 | if live_options["show-solutions"] then
348 | local plaincode = pandoc.Code(block.code, pandoc.Attr('', { 'solution-code', 'd-none' }))
349 | local codeblock = pandoc.CodeBlock(block.code, pandoc.Attr('', { 'r', 'cell-code' }))
350 | return pandoc.Div(
351 | {
352 | InterpolatedBlock(plaincode, "none"),
353 | InterpolatedBlock(codeblock, "r"),
354 | },
355 | pandoc.Attr('',
356 | { 'webr-ojs-exercise', 'exercise-solution', 'd-none' },
357 | { exercise = block.attr.exercise }
358 | )
359 | )
360 | end
361 | return {}
362 | end
363 |
364 | -- Prepare OJS attributes
365 | local input = "{" .. table.concat(block.attr.input or {}, ", ") .. "}"
366 | local ojs_vars = {
367 | block_id = block_id,
368 | block_input = input,
369 | }
370 |
371 | -- Render appropriate OJS for the type of client-side block we're working with
372 | local ojs_source = nil
373 | if (block.attr.exercise) then
374 | -- Primary interactive exercise block
375 | assertUniqueExercise(block.attr.exercise)
376 | ojs_source = "webr-exercise.ojs"
377 | elseif (block.attr.edit) then
378 | -- Editable non-exercise sandbox block
379 | ojs_source = "webr-editor.ojs"
380 | else
381 | -- Non-interactive evaluation block
382 | ojs_source = "webr-evaluate.ojs"
383 | end
384 |
385 | append_ojs_template(ojs_source, ojs_vars)
386 |
387 | -- Render any HTMLWidgets after HTML output has been added to the DOM
388 | HTMLWidget(block_id)
389 |
390 | return pandoc.Div({
391 | pandoc.Div({}, pandoc.Attr("webr-" .. block_id, { 'exercise-cell' })),
392 | pandoc.RawBlock(
393 | "html",
394 | ""
396 | )
397 | })
398 | end
399 |
400 | function InterpolatedBlock(block, language)
401 | block_id = block_id + 1
402 |
403 | -- Reactively render OJS variables in codeblocks
404 | file = io.open(quarto.utils.resolve_path("templates/interpolate.ojs"), "r")
405 | assert(file)
406 | content = file:read("*a")
407 |
408 | -- Build map of OJS variable names to JS template literals
409 | local map = "{\n"
410 | for var in block.text:gmatch("${([a-zA-Z_$][%w_$]+)}") do
411 | map = map .. var .. ",\n"
412 | end
413 | map = map .. "}"
414 |
415 | -- We add this OJS block for its side effect of updating the HTML element
416 | content = string.gsub(content, "{{block_id}}", block_id)
417 | content = string.gsub(content, "{{def_map}}", map)
418 | content = string.gsub(content, "{{language}}", language)
419 | table.insert(ojs_definitions.contents, {
420 | methodName = "interpretQuiet",
421 | cellName = "interpolate-" .. block_id,
422 | inline = false,
423 | source = content,
424 | })
425 |
426 | block.identifier = "interpolate-" .. block_id
427 | return block
428 | end
429 |
430 | function CodeBlock(code)
431 | if (
432 | code.classes:includes("{webr}") or
433 | code.classes:includes("webr") or
434 | code.classes:includes("{webr-r}")
435 | ) then
436 | -- Client side R code block
437 | include_webr = true
438 | return WebRCodeBlock(code)
439 | end
440 |
441 | if (
442 | code.classes:includes("{pyodide}") or
443 | code.classes:includes("pyodide") or
444 | code.classes:includes("{pyodide-python}")
445 | ) then
446 | -- Client side Python code block
447 | include_pyodide = true
448 | return PyodideCodeBlock(code)
449 | end
450 |
451 | -- Non-interactive code block containing OJS variables
452 | if (string.match(code.text, "${[a-zA-Z_$][%w_$]+}")) then
453 | if (code.classes:includes("r")) then
454 | include_webr = true
455 | return InterpolatedBlock(code, "r")
456 | elseif (code.classes:includes("python")) then
457 | include_pyodide = true
458 | return InterpolatedBlock(code, "python")
459 | end
460 | end
461 | end
462 |
463 | function HTMLWidget(block_id)
464 | local file = io.open(quarto.utils.resolve_path("templates/webr-widget.ojs"), "r")
465 | assert(file)
466 | content = file:read("*a")
467 |
468 | table.insert(ojs_definitions.contents, 1, {
469 | methodName = "interpretQuiet",
470 | cellName = "webr-widget-" .. block_id,
471 | inline = false,
472 | source = string.gsub(content, "{{block_id}}", block_id),
473 | })
474 | end
475 |
476 | function Div(block)
477 | -- Render exercise hints with display:none
478 | if (block.classes:includes("hint") and block.attributes["exercise"] ~= nil) then
479 | if live_options["show-hints"] then
480 | block.classes:insert("webr-ojs-exercise")
481 | block.classes:insert("exercise-hint")
482 | block.classes:insert("d-none")
483 | return block
484 | else
485 | return {}
486 | end
487 | end
488 | end
489 |
490 | function Proof(block)
491 | -- Quarto wraps solution blocks in a Proof structure
492 | -- Dig into the expected shape and look for our own exercise solutions
493 | if (block["type"] == "Solution") then
494 | local content = block["__quarto_custom_node"]
495 | local container = content.c[1]
496 | if (container) then
497 | local solution = container.c[1]
498 | if (solution) then
499 | if (solution.attributes["exercise"] ~= nil) then
500 | if live_options["show-solutions"] then
501 | solution.classes:insert("webr-ojs-exercise")
502 | solution.classes:insert("exercise-solution")
503 | solution.classes:insert("d-none")
504 | return solution
505 | else
506 | return {}
507 | end
508 | end
509 | end
510 | end
511 | end
512 | end
513 |
514 | function setupPyodide(doc)
515 | local pyodide = doc.meta.pyodide or {}
516 | local packages = pyodide.packages or {}
517 |
518 | local file = io.open(quarto.utils.resolve_path("templates/pyodide-setup.ojs"), "r")
519 | assert(file)
520 | local content = file:read("*a")
521 |
522 | local pyodide_packages = {
523 | pkgs = { "pyodide_http", "micropip", "ipython" },
524 | }
525 | for _, pkg in pairs(packages) do
526 | table.insert(pyodide_packages.pkgs, pandoc.utils.stringify(pkg))
527 | end
528 |
529 | -- Initial Pyodide startup options
530 | local pyodide_options = {
531 | indexURL = "https://cdn.jsdelivr.net/pyodide/v0.26.1/full/",
532 | }
533 | if (pyodide["engine-url"]) then
534 | pyodide_options["indexURL"] = pandoc.utils.stringify(pyodide["engine-url"])
535 | end
536 |
537 | local data = {
538 | packages = pyodide_packages,
539 | options = pyodide_options,
540 | }
541 |
542 | table.insert(ojs_definitions.contents, {
543 | methodName = "interpretQuiet",
544 | cellName = "pyodide-prelude",
545 | inline = false,
546 | source = content,
547 | })
548 |
549 | doc.blocks:insert(pandoc.RawBlock(
550 | "html",
551 | ""
552 | ))
553 |
554 | return pyodide
555 | end
556 |
557 | function setupWebR(doc)
558 | local webr = doc.meta.webr or {}
559 | local packages = webr.packages or {}
560 | local repos = webr.repos or {}
561 |
562 | local file = io.open(quarto.utils.resolve_path("templates/webr-setup.ojs"), "r")
563 | assert(file)
564 | local content = file:read("*a")
565 |
566 | -- List of webR R packages and repositories to install
567 | local webr_packages = {
568 | pkgs = { "evaluate", "knitr", "htmltools" },
569 | repos = {}
570 | }
571 | for _, pkg in pairs(packages) do
572 | table.insert(webr_packages.pkgs, pandoc.utils.stringify(pkg))
573 | end
574 | for _, repo in pairs(repos) do
575 | table.insert(webr_packages.repos, pandoc.utils.stringify(repo))
576 | end
577 |
578 | -- Data frame rendering
579 | local webr_render_df = "default"
580 | if (webr["render-df"]) then
581 | webr_render_df = pandoc.utils.stringify(webr["render-df"])
582 | local pkg = {
583 | ["paged-table"] = "rmarkdown",
584 | ["gt"] = "gt",
585 | ["gt-interactive"] = "gt",
586 | ["dt"] = "DT",
587 | ["reactable"] = "reactable",
588 | }
589 | if (pkg[webr_render_df]) then
590 | table.insert(webr_packages.pkgs, pkg[webr_render_df])
591 | end
592 | end
593 |
594 | -- Initial webR startup options
595 | local webr_options = {
596 | baseUrl = "https://webr.r-wasm.org/v0.4.1/"
597 | }
598 | if (webr["engine-url"]) then
599 | webr_options["baseUrl"] = pandoc.utils.stringify(webr["engine-url"])
600 | end
601 |
602 | local data = {
603 | packages = webr_packages,
604 | options = webr_options,
605 | render_df = webr_render_df,
606 | }
607 |
608 | table.insert(ojs_definitions.contents, {
609 | methodName = "interpretQuiet",
610 | cellName = "webr-prelude",
611 | inline = false,
612 | source = content,
613 | })
614 |
615 | doc.blocks:insert(pandoc.RawBlock(
616 | "html",
617 | ""
618 | ))
619 |
620 | return webr
621 | end
622 |
623 | function Pandoc(doc)
624 | local webr = nil
625 | local pyodide = nil
626 | if (include_webr) then
627 | webr = setupWebR(doc)
628 | end
629 | if (include_pyodide) then
630 | pyodide = setupPyodide(doc)
631 | end
632 |
633 | -- OJS block definitions
634 | doc.blocks:insert(pandoc.RawBlock(
635 | "html",
636 | ""
637 | ))
638 |
639 | -- Loading indicator
640 | doc.blocks:insert(
641 | pandoc.Div({
642 | pandoc.Div({}, pandoc.Attr("exercise-loading-status", { "d-flex", "gap-2" })),
643 | pandoc.Div({}, pandoc.Attr("", { "spinner-grow", "spinner-grow-sm" })),
644 | }, pandoc.Attr(
645 | "exercise-loading-indicator",
646 | { "exercise-loading-indicator", "d-none", "d-flex", "align-items-center", "gap-2" }
647 | ))
648 | )
649 |
650 | -- Exercise runtime dependencies
651 | quarto.doc.add_html_dependency({
652 | name = 'live-runtime',
653 | scripts = {
654 | { path = "resources/live-runtime.js", attribs = { type = "module" } },
655 | },
656 | resources = { "resources/pyodide-worker.js" },
657 | stylesheets = { "resources/live-runtime.css" },
658 | })
659 |
660 | -- Copy resources for upload to VFS at runtime
661 | local vfs_files = {}
662 | if (webr and webr.resources) then
663 | resource_list = webr.resources
664 | elseif (pyodide and pyodide.resources) then
665 | resource_list = pyodide.resources
666 | else
667 | resource_list = doc.meta.resources
668 | end
669 |
670 | if (type(resource_list) ~= "table") then
671 | resource_list = { resource_list }
672 | end
673 |
674 | if (resource_list) then
675 | for _, files in pairs(resource_list) do
676 | if (type(files) ~= "table") then
677 | files = { files }
678 | end
679 | for _, file in pairs(files) do
680 | local filetree = tree(pandoc.utils.stringify(file))
681 | for _, path in pairs(filetree) do
682 | table.insert(vfs_files, path)
683 | end
684 | end
685 | end
686 | end
687 | doc.blocks:insert(pandoc.RawBlock(
688 | "html",
689 | ""
690 | ))
691 | return doc
692 | end
693 |
694 | function Meta(meta)
695 | local webr = meta.webr or {}
696 |
697 | for k, v in pairs(webr["cell-options"] or {}) do
698 | if (type(v) == "table") then
699 | cell_options.webr[k] = pandoc.utils.stringify(v)
700 | else
701 | cell_options.webr[k] = v
702 | end
703 | end
704 |
705 | local pyodide = meta.pyodide or {}
706 |
707 | for k, v in pairs(pyodide["cell-options"] or {}) do
708 | if (type(v) == "table") then
709 | cell_options.pyodide[k] = pandoc.utils.stringify(v)
710 | else
711 | cell_options.pyodide[k] = v
712 | end
713 | end
714 |
715 | local live = meta.live or {}
716 | if (type(live) == "table") then
717 | for k, v in pairs(live) do
718 | live_options[k] = v
719 | end
720 | else
721 | quarto.log.error("Invalid value for document yaml key: `live`.")
722 | end
723 | end
724 |
725 | return {
726 | { Meta = Meta },
727 | {
728 | Div = Div,
729 | Proof = Proof,
730 | CodeBlock = CodeBlock,
731 | Pandoc = Pandoc,
732 | },
733 | }
734 |
--------------------------------------------------------------------------------
/inst/app/quarto/_extensions/r-wasm/live/resources/live-runtime.css:
--------------------------------------------------------------------------------
1 | .quarto-light{--exercise-main-color: var(--bs-body-color, var(--r-main-color, #212529));--exercise-main-bg: var(--bs-body-bg, var(--r-background-color, #ffffff));--exercise-primary-rgb: var(--bs-primary-rgb, 13, 110, 253);--exercise-gray: var(--bs-gray-300, #dee2e6);--exercise-cap-bg: var(--bs-light-bg-subtle, #f8f8f8);--exercise-line-bg: rgba(var(--exercise-primary-rgb), .05);--exercise-line-gutter-bg: rgba(var(--exercise-primary-rgb), .1)}.quarto-dark{--exercise-main-color: var(--bs-body-color, var(--r-main-color, #ffffff));--exercise-main-bg: var(--bs-body-bg, var(--r-background-color, #222222));--exercise-primary-rgb: var(--bs-primary-rgb, 55, 90, 127);--exercise-gray: var(--bs-gray-700, #434343);--exercise-cap-bg: var(--bs-card-cap-bg, #505050);--exercise-line-bg: rgba(var(--exercise-primary-rgb), .2);--exercise-line-gutter-bg: rgba(var(--exercise-primary-rgb), .4)}.webr-ojs-exercise.exercise-solution,.webr-ojs-exercise.exercise-hint{border:var(--exercise-gray) 1px solid;border-radius:5px;padding:1rem}.exercise-hint .exercise-hint,.exercise-solution .exercise-solution{border:none;padding:0}.webr-ojs-exercise.exercise-solution>.callout,.webr-ojs-exercise.exercise-hint>.callout{margin:-1rem;border:0}#exercise-loading-indicator{position:fixed;bottom:0;right:0;font-size:1.2rem;padding:.2rem .75rem;border:1px solid var(--exercise-gray);background-color:var(--exercise-cap-bg);border-top-left-radius:5px}#exercise-loading-indicator>.spinner-grow{min-width:1rem}.exercise-loading-details+.exercise-loading-details:before{content:"/ "}@media only screen and (max-width: 576px){#exercise-loading-indicator{font-size:.8rem;padding:.1rem .5rem}#exercise-loading-indicator>.spinner-grow{min-width:.66rem}#exercise-loading-indicator .gap-2{gap:.2rem!important}#exercise-loading-indicator .spinner-grow{--bs-spinner-width: .66rem;--bs-spinner-height: .66rem}}.btn.btn-exercise-editor:disabled,.btn.btn-exercise-editor.disabled,.btn-exercise-editor fieldset:disabled .btn{transition:opacity .5s}.card.exercise-editor .card-header a.btn{--bs-btn-padding-x: .5rem;--bs-btn-padding-y: .15rem;--bs-btn-font-size: .75rem}.quarto-dark .card.exercise-editor .card-header .btn.btn-outline-dark{--bs-btn-color: #f8f8f8;--bs-btn-border-color: #f8f8f8;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #f8f8f8;--bs-btn-hover-border-color: #f8f8f8;--bs-btn-focus-shadow-rgb: 248, 248, 248;--bs-btn-active-color: #000;--bs-btn-active-bg: #f8f8f8;--bs-btn-active-border-color: #f8f8f8;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);--bs-btn-disabled-color: #f8f8f8;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #f8f8f8;--bs-btn-bg: transparent;--bs-gradient: none}.card.exercise-editor{--exercise-min-lines: 0;--exercise-max-lines: infinity;--exercise-font-size: var(--bs-body-font-size, 1rem)}.card.exercise-editor .card-header{padding:.5rem 1rem;background-color:var(--exercise-cap-bg);border-bottom:1px solid rgba(0,0,0,.175)}.card.exercise-editor .cm-editor{color:var(--exercise-main-color);background-color:var(--exercise-main-bg);max-height:calc(var(--exercise-max-lines) * 1.4 * var(--exercise-font-size) + 8px)}.card.exercise-editor .cm-content{caret-color:var(--exercise-main-color)}.card.exercise-editor .cm-cursor,.card.exercise-editor .cm-dropCursor{border-left-color:var(--exercise-main-color)}.card.exercise-editor .cm-focused .cm-selectionBackgroundm .cm-selectionBackground,.card.exercise-editor .cm-content ::selection{background-color:rgba(var(--exercise-primary-rgb),.1)}.card.exercise-editor .cm-activeLine{background-color:var(--exercise-line-bg)}.card.exercise-editor .cm-activeLineGutter{background-color:var(--exercise-line-gutter-bg)}.card.exercise-editor .cm-gutters{background-color:var(--exercise-cap-bg);color:var(--exercise-main-color);border-right:1px solid var(--exercise-gray)}.card.exercise-editor .cm-content,.card.exercise-editor .cm-gutter{min-height:calc(var(--exercise-min-lines) * 1.4 * var(--exercise-font-size) + 8px)}.card.exercise-editor .cm-scroller{line-height:1.4;overflow:auto}:root{--exercise-editor-hl-al: var(--quarto-hl-al-color, #AD0000);--exercise-editor-hl-an: var(--quarto-hl-an-color, #5E5E5E);--exercise-editor-hl-at: var(--quarto-hl-at-color, #657422);--exercise-editor-hl-bn: var(--quarto-hl-bn-color, #AD0000);--exercise-editor-hl-ch: var(--quarto-hl-ch-color, #20794D);--exercise-editor-hl-co: var(--quarto-hl-co-color, #5E5E5E);--exercise-editor-hl-cv: var(--quarto-hl-cv-color, #5E5E5E);--exercise-editor-hl-cn: var(--quarto-hl-cn-color, #8f5902);--exercise-editor-hl-cf: var(--quarto-hl-cf-color, #003B4F);--exercise-editor-hl-dt: var(--quarto-hl-dt-color, #AD0000);--exercise-editor-hl-dv: var(--quarto-hl-dv-color, #AD0000);--exercise-editor-hl-do: var(--quarto-hl-do-color, #5E5E5E);--exercise-editor-hl-er: var(--quarto-hl-er-color, #AD0000);--exercise-editor-hl-fl: var(--quarto-hl-fl-color, #AD0000);--exercise-editor-hl-fu: var(--quarto-hl-fu-color, #4758AB);--exercise-editor-hl-im: var(--quarto-hl-im-color, #00769E);--exercise-editor-hl-in: var(--quarto-hl-in-color, #5E5E5E);--exercise-editor-hl-kw: var(--quarto-hl-kw-color, #003B4F);--exercise-editor-hl-op: var(--quarto-hl-op-color, #5E5E5E);--exercise-editor-hl-ot: var(--quarto-hl-ot-color, #003B4F);--exercise-editor-hl-pp: var(--quarto-hl-pp-color, #AD0000);--exercise-editor-hl-sc: var(--quarto-hl-sc-color, #5E5E5E);--exercise-editor-hl-ss: var(--quarto-hl-ss-color, #20794D);--exercise-editor-hl-st: var(--quarto-hl-st-color, #20794D);--exercise-editor-hl-va: var(--quarto-hl-va-color, #111111);--exercise-editor-hl-vs: var(--quarto-hl-vs-color, #20794D);--exercise-editor-hl-wa: var(--quarto-hl-wa-color, #5E5E5E)}*[data-bs-theme=dark]{--exercise-editor-hl-al: var(--quarto-hl-al-color, #f07178);--exercise-editor-hl-an: var(--quarto-hl-an-color, #d4d0ab);--exercise-editor-hl-at: var(--quarto-hl-at-color, #00e0e0);--exercise-editor-hl-bn: var(--quarto-hl-bn-color, #d4d0ab);--exercise-editor-hl-bu: var(--quarto-hl-bu-color, #abe338);--exercise-editor-hl-ch: var(--quarto-hl-ch-color, #abe338);--exercise-editor-hl-co: var(--quarto-hl-co-color, #f8f8f2);--exercise-editor-hl-cv: var(--quarto-hl-cv-color, #ffd700);--exercise-editor-hl-cn: var(--quarto-hl-cn-color, #ffd700);--exercise-editor-hl-cf: var(--quarto-hl-cf-color, #ffa07a);--exercise-editor-hl-dt: var(--quarto-hl-dt-color, #ffa07a);--exercise-editor-hl-dv: var(--quarto-hl-dv-color, #d4d0ab);--exercise-editor-hl-do: var(--quarto-hl-do-color, #f8f8f2);--exercise-editor-hl-er: var(--quarto-hl-er-color, #f07178);--exercise-editor-hl-ex: var(--quarto-hl-ex-color, #00e0e0);--exercise-editor-hl-fl: var(--quarto-hl-fl-color, #d4d0ab);--exercise-editor-hl-fu: var(--quarto-hl-fu-color, #ffa07a);--exercise-editor-hl-im: var(--quarto-hl-im-color, #abe338);--exercise-editor-hl-in: var(--quarto-hl-in-color, #d4d0ab);--exercise-editor-hl-kw: var(--quarto-hl-kw-color, #ffa07a);--exercise-editor-hl-op: var(--quarto-hl-op-color, #ffa07a);--exercise-editor-hl-ot: var(--quarto-hl-ot-color, #00e0e0);--exercise-editor-hl-pp: var(--quarto-hl-pp-color, #dcc6e0);--exercise-editor-hl-re: var(--quarto-hl-re-color, #00e0e0);--exercise-editor-hl-sc: var(--quarto-hl-sc-color, #abe338);--exercise-editor-hl-ss: var(--quarto-hl-ss-color, #abe338);--exercise-editor-hl-st: var(--quarto-hl-st-color, #abe338);--exercise-editor-hl-va: var(--quarto-hl-va-color, #00e0e0);--exercise-editor-hl-vs: var(--quarto-hl-vs-color, #abe338);--exercise-editor-hl-wa: var(--quarto-hl-wa-color, #dcc6e0)}pre>code.sourceCode span.tok-keyword,.exercise-editor-body>.cm-editor span.tok-keyword{color:var(--exercise-editor-hl-kw)}pre>code.sourceCode span.tok-operator,.exercise-editor-body>.cm-editor span.tok-operator{color:var(--exercise-editor-hl-op)}pre>code.sourceCode span.tok-definitionOperator,.exercise-editor-body>.cm-editor span.tok-definitionOperator{color:var(--exercise-editor-hl-ot)}pre>code.sourceCode span.tok-compareOperator,.exercise-editor-body>.cm-editor span.tok-compareOperator{color:var(--exercise-editor-hl-ot)}pre>code.sourceCode span.tok-attributeName,.exercise-editor-body>.cm-editor span.tok-attributeName{color:var(--exercise-editor-hl-at)}pre>code.sourceCode span.tok-controlKeyword,.exercise-editor-body>.cm-editor span.tok-controlKeyword{color:var(--exercise-editor-hl-cf)}pre>code.sourceCode span.tok-comment,.exercise-editor-body>.cm-editor span.tok-comment{color:var(--exercise-editor-hl-co)}pre>code.sourceCode span.tok-string,.exercise-editor-body>.cm-editor span.tok-string{color:var(--exercise-editor-hl-st)}pre>code.sourceCode span.tok-string2,.exercise-editor-body>.cm-editor span.tok-string2{color:var(--exercise-editor-hl-ss)}pre>code.sourceCode span.tok-variableName,.exercise-editor-body>.cm-editor span.tok-variableName{color:var(--exercise-editor-hl-va)}pre>code.sourceCode span.tok-bool,pre>code.sourceCode span.tok-literal,pre>code.sourceCode span.tok-separator,.exercise-editor-body>.cm-editor span.tok-bool,.exercise-editor-body>.cm-editor span.tok-literal,.exercise-editor-body>.cm-editor span.tok-separator{color:var(--exercise-editor-hl-cn)}pre>code.sourceCode span.tok-bool,pre>code.sourceCode span.tok-literal,.exercise-editor-body>.cm-editor span.tok-bool,.exercise-editor-body>.cm-editor span.tok-literal{color:var(--exercise-editor-hl-cn)}pre>code.sourceCode span.tok-number,pre>code.sourceCode span.tok-integer,.exercise-editor-body>.cm-editor span.tok-number,.exercise-editor-body>.cm-editor span.tok-integer{color:var(--exercise-editor-hl-dv)}pre>code.sourceCode span.tok-function-variableName,.exercise-editor-body>.cm-editor span.tok-function-variableName{color:var(--exercise-editor-hl-fu)}pre>code.sourceCode span.tok-function-attributeName,.exercise-editor-body>.cm-editor span.tok-function-attributeName{color:var(--exercise-editor-hl-at)}div.exercise-cell-output.cell-output-stdout pre code,div.exercise-cell-output.cell-output-stderr pre code{white-space:pre-wrap;word-wrap:break-word}div.exercise-cell-output.cell-output-stderr pre code{color:var(--exercise-editor-hl-er, #AD0000)}div.cell-output-pyodide table{border:none;margin:0 auto 1em}div.cell-output-pyodide thead{border-bottom:1px solid var(--exercise-main-color)}div.cell-output-pyodide td,div.cell-output-pyodide th,div.cell-output-pyodide tr{padding:.5em;line-height:normal}div.cell-output-pyodide th{font-weight:700}div.cell-output-display canvas{background-color:#fff}.tab-pane>.exercise-tab-pane-header+div.webr-ojs-exercise{margin-top:1em}.alert .exercise-feedback p:last-child{margin-bottom:0}.alert.exercise-grade{animation-duration:.25s;animation-name:exercise-grade-slidein}@keyframes exercise-grade-slidein{0%{transform:translateY(10px);opacity:0}to{transform:translateY(0);opacity:1}}.alert.exercise-grade p:last-child{margin-bottom:0}.alert.exercise-grade pre{white-space:pre-wrap;color:inherit}.observablehq pre>code.sourceCode{white-space:pre;position:relative}.observablehq div.sourceCode{margin:1em 0!important}.observablehq pre.sourceCode{margin:0!important}@media screen{.observablehq div.sourceCode{overflow:auto}}@media print{.observablehq pre>code.sourceCode{white-space:pre-wrap}.observablehq pre>code.sourceCode>span{text-indent:-5em;padding-left:5em}}.reveal .d-none{display:none!important}.reveal .d-flex{display:flex!important}.reveal .card.exercise-editor .justify-content-between{justify-content:space-between!important}.reveal .card.exercise-editor .align-items-center{align-items:center!important}.reveal .card.exercise-editor .gap-1{gap:.25rem!important}.reveal .card.exercise-editor .gap-2{gap:.5rem!important}.reveal .card.exercise-editor .gap-3{gap:.75rem!important}.reveal .card.exercise-editor{--exercise-font-size: 1.3rem;margin:1rem 0;border:1px solid rgba(0,0,0,.175);border-radius:.375rem;font-size:var(--exercise-font-size);overflow:hidden}.reveal .card.exercise-editor .card-header{padding:.5rem 1rem;background-color:var(--exercise-cap-bg);border-bottom:1px solid rgba(0,0,0,.175)}.reveal .cell-output-webr.cell-output-display,.reveal .cell-output-pyodide.cell-output-display{text-align:center}.quarto-light .reveal .btn.btn-exercise-editor.btn-primary{--exercise-btn-bg: var(--bs-btn-bg, #0d6efd);--exercise-btn-color: var(--bs-btn-color, #ffffff);--exercise-btn-border-color: var(--bs-btn-border-color, #0d6efd);--exercise-btn-hover-border-color: var(--bs-btn-hover-border-color, #0b5ed7);--exercise-btn-hover-bg: var(--bs-btn-hover-bg, #0b5ed7);--exercise-btn-hover-color: var(--bs-btn-hover-color, #ffffff)}.quarto-dark .reveal .btn.btn-exercise-editor.btn-primary{--exercise-btn-bg: var(--bs-btn-bg, #375a7f);--exercise-btn-color: var(--bs-btn-color, #ffffff);--exercise-btn-border-color: var(--bs-btn-border-color, #375a7f);--exercise-btn-hover-border-color: var(--bs-btn-hover-border-color, #2c4866);--exercise-btn-hover-bg: var(--bs-btn-hover-bg, #2c4866);--exercise-btn-hover-color: var(--bs-btn-hover-color, #ffffff)}.quarto-light .reveal .btn.btn-exercise-editor.btn-outline-dark{--exercise-btn-bg: var(--bs-btn-bg, transparent);--exercise-btn-color: var(--bs-btn-color, #333);--exercise-btn-border-color: var(--bs-btn-border-color, #333);--exercise-btn-hover-border-color: var(--bs-btn-hover-border-color, #333);--exercise-btn-hover-bg: var(--bs-btn-hover-bg, #333);--exercise-btn-hover-color: var(--bs-btn-hover-color, #ffffff)}.quarto-dark .reveal .btn.btn-exercise-editor.btn-outline-dark{--exercise-btn-bg: var(--bs-btn-bg, transparent);--exercise-btn-color: var(--bs-btn-color, #f8f8f8);--exercise-btn-border-color: var(--bs-btn-border-color, #f8f8f8);--exercise-btn-hover-border-color: var(--bs-btn-hover-border-color, #f8f8f8);--exercise-btn-hover-bg: var(--bs-btn-hover-bg, #f8f8f8);--exercise-btn-hover-color: var(--bs-btn-hover-color, #000000)}@media only screen and (max-width: 576px){:not(.reveal) .card-header .btn-exercise-editor>.btn-label-exercise-editor{max-width:0px;margin-left:-4px;overflow:hidden;transition:max-width .2s ease-in,margin-left .05s ease-out .2s}:not(.reveal) .card-header .btn-exercise-editor:hover>.btn-label-exercise-editor{position:inherit;max-width:80px;margin-left:0;transition:max-width .2s ease-out .05s,margin-left .05s ease-in}}.reveal .card.exercise-editor .btn-group{border-radius:.375rem;position:relative;display:inline-flex;vertical-align:middle}.reveal .card.exercise-editor .btn-group>.btn{position:relative;flex:1 1 auto}.reveal .card.exercise-editor .btn-group>:not(.btn-check:first-child)+.btn,.reveal .card.exercise-editor .btn-group>.btn-group:not(:first-child){margin-left:-1px}.reveal .card.exercise-editor .btn-group>.btn:not(:last-child):not(.dropdown-toggle),.reveal .card.exercise-editor .btn-group>.btn.dropdown-toggle-split:first-child,.reveal .card.exercise-editor .btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.reveal .card.exercise-editor .btn-group>.btn:nth-child(n+3),.reveal .card.exercise-editor .btn-group>:not(.btn-check)+.btn,.reveal .card.exercise-editor .btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.reveal .btn.btn-exercise-editor{display:inline-block;padding:.25rem .5rem;font-size:1rem;color:var(--exercise-btn-color);background-color:var(--exercise-btn-bg);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;user-select:none;border:1px solid var(--exercise-btn-border-color);border-radius:.375rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.reveal .btn.btn-exercise-editor:hover{color:var(--exercise-btn-hover-color);background-color:var(--exercise-btn-hover-bg);border-color:var(--exercise-btn-hover-border-color)}.reveal .btn.btn-exercise-editor:disabled,.reveal .btn.btn-exercise-editor.disabled,.reveal .btn-exercise-editor fieldset:disabled .btn{pointer-events:none;opacity:.65}.reveal .card.exercise-editor .spinner-grow{background-color:currentcolor;opacity:0;display:inline-block;width:1.5rem;height:1.5rem;vertical-align:-.125em;border-radius:50%;animation:.75s linear infinite spinner-grow}.reveal .cell-output-container pre code{overflow:auto;max-height:initial}.reveal .alert.exercise-grade{font-size:.55em;position:relative;padding:1rem;margin:1rem 0;border-radius:.25rem;color:var(--exercise-alert-color);background-color:var(--exercise-alert-bg);border:1px solid var(--exercise-alert-border-color)}.reveal .alert.exercise-grade .alert-link{font-weight:700;color:var(--exercise-alert-link-color)}.quarto-light .reveal .exercise-grade.alert-info{--exercise-alert-color: #055160;--exercise-alert-bg: #cff4fc;--exercise-alert-border-color: #9eeaf9;--exercise-alert-link-color: #055160}.quarto-light .reveal .exercise-grade.alert-success{--exercise-alert-color: #0a3622;--exercise-alert-bg: #d1e7dd;--exercise-alert-border-color: #a3cfbb;--exercise-alert-link-color: #0a3622}.quarto-light .reveal .exercise-grade.alert-warning{--exercise-alert-color: #664d03;--exercise-alert-bg: #fff3cd;--exercise-alert-border-color: #ffe69c;--exercise-alert-link-color: #664d03}.quarto-light .reveal .exercise-grade.alert-danger{--exercise-alert-color: #58151c;--exercise-alert-bg: #f8d7da;--exercise-alert-border-color: #f1aeb5;--exercise-alert-link-color: #58151c}.quarto-dark .reveal .exercise-grade.alert-info{--exercise-alert-color: #ffffff;--exercise-alert-bg: #3498db;--exercise-alert-border-color: #3498db;--exercise-alert-link-color: #ffffff}.quarto-dark .reveal .exercise-grade.alert-success{--exercise-alert-color: #ffffff;--exercise-alert-bg: #00bc8c;--exercise-alert-border-color: #00bc8c;--exercise-alert-link-color: #ffffff}.quarto-dark .reveal .exercise-grade.alert-warning{--exercise-alert-color: #ffffff;--exercise-alert-bg: #f39c12;--exercise-alert-border-color: #f39c12;--exercise-alert-link-color: #ffffff}.quarto-dark .reveal .exercise-grade.alert-danger{--exercise-alert-color: #ffffff;--exercise-alert-bg: #e74c3c;--exercise-alert-border-color: #e74c3c;--exercise-alert-link-color: #ffffff}
2 |
--------------------------------------------------------------------------------
/inst/app/quarto/_extensions/r-wasm/live/resources/pyodide-worker.js:
--------------------------------------------------------------------------------
1 | var je=Object.create;var U=Object.defineProperty;var Be=Object.getOwnPropertyDescriptor;var ze=Object.getOwnPropertyNames;var We=Object.getPrototypeOf,Ve=Object.prototype.hasOwnProperty;var x=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,r)=>(typeof require<"u"?require:t)[r]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var qe=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),Ye=(e,t)=>{for(var r in t)U(e,r,{get:t[r],enumerable:!0})},Ge=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of ze(t))!Ve.call(e,a)&&a!==r&&U(e,a,{get:()=>t[a],enumerable:!(o=Be(t,a))||o.enumerable});return e};var Je=(e,t,r)=>(r=e!=null?je(We(e)):{},Ge(t||!e||!e.__esModule?U(r,"default",{value:e,enumerable:!0}):r,e));var ae=qe(()=>{});var M={};Ye(M,{createEndpoint:()=>R,expose:()=>k,finalizer:()=>T,proxy:()=>I,proxyMarker:()=>B,releaseProxy:()=>ee,transfer:()=>oe,transferHandlers:()=>v,windowEndpoint:()=>nt,wrap:()=>A});var B=Symbol("Comlink.proxy"),R=Symbol("Comlink.endpoint"),ee=Symbol("Comlink.releaseProxy"),T=Symbol("Comlink.finalizer"),C=Symbol("Comlink.thrown"),te=e=>typeof e=="object"&&e!==null||typeof e=="function",Xe={canHandle:e=>te(e)&&e[B],serialize(e){let{port1:t,port2:r}=new MessageChannel;return k(e,t),[r,[r]]},deserialize(e){return e.start(),A(e)}},Ke={canHandle:e=>te(e)&&C in e,serialize({value:e}){let t;return e instanceof Error?t={isError:!0,value:{message:e.message,name:e.name,stack:e.stack}}:t={isError:!1,value:e},[t,[]]},deserialize(e){throw e.isError?Object.assign(new Error(e.value.message),e.value):e.value}},v=new Map([["proxy",Xe],["throw",Ke]]);function Qe(e,t){for(let r of e)if(t===r||r==="*"||r instanceof RegExp&&r.test(t))return!0;return!1}function k(e,t=globalThis,r=["*"]){t.addEventListener("message",function o(a){if(!a||!a.data)return;if(!Qe(r,a.origin)){console.warn(`Invalid origin '${a.origin}' for comlink proxy`);return}let{id:i,type:n,path:l}=Object.assign({path:[]},a.data),s=(a.data.argumentList||[]).map(E),u;try{let c=l.slice(0,-1).reduce((p,y)=>p[y],e),f=l.reduce((p,y)=>p[y],e);switch(n){case"GET":u=f;break;case"SET":c[l.slice(-1)[0]]=E(a.data.value),u=!0;break;case"APPLY":u=f.apply(c,s);break;case"CONSTRUCT":{let p=new f(...s);u=I(p)}break;case"ENDPOINT":{let{port1:p,port2:y}=new MessageChannel;k(e,y),u=oe(p,[p])}break;case"RELEASE":u=void 0;break;default:return}}catch(c){u={value:c,[C]:0}}Promise.resolve(u).catch(c=>({value:c,[C]:0})).then(c=>{let[f,p]=N(c);t.postMessage(Object.assign(Object.assign({},f),{id:i}),p),n==="RELEASE"&&(t.removeEventListener("message",o),re(t),T in e&&typeof e[T]=="function"&&e[T]())}).catch(c=>{let[f,p]=N({value:new TypeError("Unserializable return value"),[C]:0});t.postMessage(Object.assign(Object.assign({},f),{id:i}),p)})}),t.start&&t.start()}function Ze(e){return e.constructor.name==="MessagePort"}function re(e){Ze(e)&&e.close()}function A(e,t){return j(e,[],t)}function F(e){if(e)throw new Error("Proxy has been released and is not useable")}function ne(e){return P(e,{type:"RELEASE"}).then(()=>{re(e)})}var L=new WeakMap,_="FinalizationRegistry"in globalThis&&new FinalizationRegistry(e=>{let t=(L.get(e)||0)-1;L.set(e,t),t===0&&ne(e)});function et(e,t){let r=(L.get(t)||0)+1;L.set(t,r),_&&_.register(e,t,e)}function tt(e){_&&_.unregister(e)}function j(e,t=[],r=function(){}){let o=!1,a=new Proxy(r,{get(i,n){if(F(o),n===ee)return()=>{tt(a),ne(e),o=!0};if(n==="then"){if(t.length===0)return{then:()=>a};let l=P(e,{type:"GET",path:t.map(s=>s.toString())}).then(E);return l.then.bind(l)}return j(e,[...t,n])},set(i,n,l){F(o);let[s,u]=N(l);return P(e,{type:"SET",path:[...t,n].map(c=>c.toString()),value:s},u).then(E)},apply(i,n,l){F(o);let s=t[t.length-1];if(s===R)return P(e,{type:"ENDPOINT"}).then(E);if(s==="bind")return j(e,t.slice(0,-1));let[u,c]=Z(l);return P(e,{type:"APPLY",path:t.map(f=>f.toString()),argumentList:u},c).then(E)},construct(i,n){F(o);let[l,s]=Z(n);return P(e,{type:"CONSTRUCT",path:t.map(u=>u.toString()),argumentList:l},s).then(E)}});return et(a,e),a}function rt(e){return Array.prototype.concat.apply([],e)}function Z(e){let t=e.map(N);return[t.map(r=>r[0]),rt(t.map(r=>r[1]))]}var ie=new WeakMap;function oe(e,t){return ie.set(e,t),e}function I(e){return Object.assign(e,{[B]:!0})}function nt(e,t=globalThis,r="*"){return{postMessage:(o,a)=>e.postMessage(o,r,a),addEventListener:t.addEventListener.bind(t),removeEventListener:t.removeEventListener.bind(t)}}function N(e){for(let[t,r]of v)if(r.canHandle(e)){let[o,a]=r.serialize(e);return[{type:"HANDLER",name:t,value:o},a]}return[{type:"RAW",value:e},ie.get(e)||[]]}function E(e){switch(e.type){case"HANDLER":return v.get(e.name).deserialize(e.value);case"RAW":return e.value}}function P(e,t,r){return new Promise(o=>{let a=it();e.addEventListener("message",function i(n){!n.data||!n.data.id||n.data.id!==a||(e.removeEventListener("message",i),o(n.data))}),e.start&&e.start(),e.postMessage(Object.assign({id:a},t),r)})}function it(){return new Array(4).fill(0).map(()=>Math.floor(Math.random()*Number.MAX_SAFE_INTEGER).toString(16)).join("-")}var ot=Object.create,V=Object.defineProperty,at=Object.getOwnPropertyDescriptor,st=Object.getOwnPropertyNames,ct=Object.getPrototypeOf,lt=Object.prototype.hasOwnProperty,d=(e,t)=>V(e,"name",{value:t,configurable:!0}),le=(e=>typeof x<"u"?x:typeof Proxy<"u"?new Proxy(e,{get:(t,r)=>(typeof x<"u"?x:t)[r]}):e)(function(e){if(typeof x<"u")return x.apply(this,arguments);throw new Error('Dynamic require of "'+e+'" is not supported')}),ue=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),ut=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of st(t))!lt.call(e,a)&&a!==r&&V(e,a,{get:()=>t[a],enumerable:!(o=at(t,a))||o.enumerable});return e},ft=(e,t,r)=>(r=e!=null?ot(ct(e)):{},ut(t||!e||!e.__esModule?V(r,"default",{value:e,enumerable:!0}):r,e)),pt=ue((e,t)=>{(function(r,o){"use strict";typeof define=="function"&&define.amd?define("stackframe",[],o):typeof e=="object"?t.exports=o():r.StackFrame=o()})(e,function(){"use strict";function r(m){return!isNaN(parseFloat(m))&&isFinite(m)}d(r,"_isNumber");function o(m){return m.charAt(0).toUpperCase()+m.substring(1)}d(o,"_capitalize");function a(m){return function(){return this[m]}}d(a,"_getter");var i=["isConstructor","isEval","isNative","isToplevel"],n=["columnNumber","lineNumber"],l=["fileName","functionName","source"],s=["args"],u=["evalOrigin"],c=i.concat(n,l,s,u);function f(m){if(m)for(var g=0;g{(function(r,o){"use strict";typeof define=="function"&&define.amd?define("error-stack-parser",["stackframe"],o):typeof e=="object"?t.exports=o(pt()):r.ErrorStackParser=o(r.StackFrame)})(e,d(function(r){"use strict";var o=/(^|@)\S+:\d+/,a=/^\s*at .*(\S+:\d+|\(native\))/m,i=/^(eval@)?(\[native code])?$/;return{parse:d(function(n){if(typeof n.stacktrace<"u"||typeof n["opera#sourceloc"]<"u")return this.parseOpera(n);if(n.stack&&n.stack.match(a))return this.parseV8OrIE(n);if(n.stack)return this.parseFFOrSafari(n);throw new Error("Cannot parse given Error object")},"ErrorStackParser$$parse"),extractLocation:d(function(n){if(n.indexOf(":")===-1)return[n];var l=/(.+?)(?::(\d+))?(?::(\d+))?$/,s=l.exec(n.replace(/[()]/g,""));return[s[1],s[2]||void 0,s[3]||void 0]},"ErrorStackParser$$extractLocation"),parseV8OrIE:d(function(n){var l=n.stack.split(`
2 | `).filter(function(s){return!!s.match(a)},this);return l.map(function(s){s.indexOf("(eval ")>-1&&(s=s.replace(/eval code/g,"eval").replace(/(\(eval at [^()]*)|(,.*$)/g,""));var u=s.replace(/^\s+/,"").replace(/\(eval code/g,"(").replace(/^.*?\s+/,""),c=u.match(/ (\(.+\)$)/);u=c?u.replace(c[0],""):u;var f=this.extractLocation(c?c[1]:u),p=c&&u||void 0,y=["eval",""].indexOf(f[0])>-1?void 0:f[0];return new r({functionName:p,fileName:y,lineNumber:f[1],columnNumber:f[2],source:s})},this)},"ErrorStackParser$$parseV8OrIE"),parseFFOrSafari:d(function(n){var l=n.stack.split(`
3 | `).filter(function(s){return!s.match(i)},this);return l.map(function(s){if(s.indexOf(" > eval")>-1&&(s=s.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g,":$1")),s.indexOf("@")===-1&&s.indexOf(":")===-1)return new r({functionName:s});var u=/((.*".+"[^@]*)?[^@]*)(?:@)/,c=s.match(u),f=c&&c[1]?c[1]:void 0,p=this.extractLocation(s.replace(u,""));return new r({functionName:f,fileName:p[0],lineNumber:p[1],columnNumber:p[2],source:s})},this)},"ErrorStackParser$$parseFFOrSafari"),parseOpera:d(function(n){return!n.stacktrace||n.message.indexOf(`
4 | `)>-1&&n.message.split(`
5 | `).length>n.stacktrace.split(`
6 | `).length?this.parseOpera9(n):n.stack?this.parseOpera11(n):this.parseOpera10(n)},"ErrorStackParser$$parseOpera"),parseOpera9:d(function(n){for(var l=/Line (\d+).*script (?:in )?(\S+)/i,s=n.message.split(`
7 | `),u=[],c=2,f=s.length;c/,"$2").replace(/\([^)]*\)/g,"")||void 0,y;f.match(/\(([^)]*)\)/)&&(y=f.replace(/^[^(]+\(([^)]*)\)$/,"$1"));var h=y===void 0||y==="[arguments not available]"?void 0:y.split(",");return new r({functionName:p,args:h,fileName:c[0],lineNumber:c[1],columnNumber:c[2],source:s})},this)},"ErrorStackParser$$parseOpera11")}},"ErrorStackParser"))}),dt=ft(mt()),w=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&typeof process.browser>"u",fe=w&&typeof module<"u"&&typeof module.exports<"u"&&typeof le<"u"&&typeof __dirname<"u",yt=w&&!fe,gt=typeof Deno<"u",pe=!w&&!gt,ht=pe&&typeof window=="object"&&typeof document=="object"&&typeof document.createElement=="function"&&typeof sessionStorage=="object"&&typeof importScripts!="function",wt=pe&&typeof importScripts=="function"&&typeof self=="object",Tt=typeof navigator=="object"&&typeof navigator.userAgent=="string"&&navigator.userAgent.indexOf("Chrome")==-1&&navigator.userAgent.indexOf("Safari")>-1,me,z,de,se,q;async function Y(){if(!w||(me=(await import("node:url")).default,se=await import("node:fs"),q=await import("node:fs/promises"),de=(await import("node:vm")).default,z=await import("node:path"),G=z.sep,typeof le<"u"))return;let e=se,t=await import("node:crypto"),r=await Promise.resolve().then(()=>Je(ae(),1)),o=await import("node:child_process"),a={fs:e,crypto:t,ws:r,child_process:o};globalThis.require=function(i){return a[i]}}d(Y,"initNodeModules");function ye(e,t){return z.resolve(t||".",e)}d(ye,"node_resolvePath");function ge(e,t){return t===void 0&&(t=location),new URL(e,t).toString()}d(ge,"browser_resolvePath");var W;w?W=ye:W=ge;var G;w||(G="/");function he(e,t){return e.startsWith("file://")&&(e=e.slice(7)),e.includes("://")?{response:fetch(e)}:{binary:q.readFile(e).then(r=>new Uint8Array(r.buffer,r.byteOffset,r.byteLength))}}d(he,"node_getBinaryResponse");function we(e,t){let r=new URL(e,location);return{response:fetch(r,t?{integrity:t}:{})}}d(we,"browser_getBinaryResponse");var D;w?D=he:D=we;async function ve(e,t){let{response:r,binary:o}=D(e,t);if(o)return o;let a=await r;if(!a.ok)throw new Error(`Failed to load '${e}': request failed.`);return new Uint8Array(await a.arrayBuffer())}d(ve,"loadBinaryFile");var $;if(ht)$=d(async e=>await import(e),"loadScript");else if(wt)$=d(async e=>{try{globalThis.importScripts(e)}catch(t){if(t instanceof TypeError)await import(e);else throw t}},"loadScript");else if(w)$=be;else throw new Error("Cannot determine runtime environment");async function be(e){e.startsWith("file://")&&(e=e.slice(7)),e.includes("://")?de.runInThisContext(await(await fetch(e)).text()):await import(me.pathToFileURL(e).href)}d(be,"nodeLoadScript");async function xe(e){if(w){await Y();let t=await q.readFile(e,{encoding:"utf8"});return JSON.parse(t)}else return await(await fetch(e)).json()}d(xe,"loadLockFile");async function Ee(){if(fe)return __dirname;let e;try{throw new Error}catch(o){e=o}let t=dt.default.parse(e)[0].fileName;if(yt){let o=await import("node:path");return(await import("node:url")).fileURLToPath(o.dirname(t))}let r=t.lastIndexOf(G);if(r===-1)throw new Error("Could not extract indexURL path from pyodide module location");return t.slice(0,r)}d(Ee,"calculateDirname");function ke(e){let t=e.FS,r=e.FS.filesystems.MEMFS,o=e.PATH,a={DIR_MODE:16895,FILE_MODE:33279,mount:function(i){if(!i.opts.fileSystemHandle)throw new Error("opts.fileSystemHandle is required");return r.mount.apply(null,arguments)},syncfs:async(i,n,l)=>{try{let s=a.getLocalSet(i),u=await a.getRemoteSet(i),c=n?u:s,f=n?s:u;await a.reconcile(i,c,f),l(null)}catch(s){l(s)}},getLocalSet:i=>{let n=Object.create(null);function l(c){return c!=="."&&c!==".."}d(l,"isRealDir");function s(c){return f=>o.join2(c,f)}d(s,"toAbsolute");let u=t.readdir(i.mountpoint).filter(l).map(s(i.mountpoint));for(;u.length;){let c=u.pop(),f=t.stat(c);t.isDir(f.mode)&&u.push.apply(u,t.readdir(c).filter(l).map(s(c))),n[c]={timestamp:f.mtime,mode:f.mode}}return{type:"local",entries:n}},getRemoteSet:async i=>{let n=Object.create(null),l=await vt(i.opts.fileSystemHandle);for(let[s,u]of l)s!=="."&&(n[o.join2(i.mountpoint,s)]={timestamp:u.kind==="file"?(await u.getFile()).lastModifiedDate:new Date,mode:u.kind==="file"?a.FILE_MODE:a.DIR_MODE});return{type:"remote",entries:n,handles:l}},loadLocalEntry:i=>{let n=t.lookupPath(i).node,l=t.stat(i);if(t.isDir(l.mode))return{timestamp:l.mtime,mode:l.mode};if(t.isFile(l.mode))return n.contents=r.getFileDataAsTypedArray(n),{timestamp:l.mtime,mode:l.mode,contents:n.contents};throw new Error("node type not supported")},storeLocalEntry:(i,n)=>{if(t.isDir(n.mode))t.mkdirTree(i,n.mode);else if(t.isFile(n.mode))t.writeFile(i,n.contents,{canOwn:!0});else throw new Error("node type not supported");t.chmod(i,n.mode),t.utime(i,n.timestamp,n.timestamp)},removeLocalEntry:i=>{var n=t.stat(i);t.isDir(n.mode)?t.rmdir(i):t.isFile(n.mode)&&t.unlink(i)},loadRemoteEntry:async i=>{if(i.kind==="file"){let n=await i.getFile();return{contents:new Uint8Array(await n.arrayBuffer()),mode:a.FILE_MODE,timestamp:n.lastModifiedDate}}else{if(i.kind==="directory")return{mode:a.DIR_MODE,timestamp:new Date};throw new Error("unknown kind: "+i.kind)}},storeRemoteEntry:async(i,n,l)=>{let s=i.get(o.dirname(n)),u=t.isFile(l.mode)?await s.getFileHandle(o.basename(n),{create:!0}):await s.getDirectoryHandle(o.basename(n),{create:!0});if(u.kind==="file"){let c=await u.createWritable();await c.write(l.contents),await c.close()}i.set(n,u)},removeRemoteEntry:async(i,n)=>{await i.get(o.dirname(n)).removeEntry(o.basename(n)),i.delete(n)},reconcile:async(i,n,l)=>{let s=0,u=[];Object.keys(n.entries).forEach(function(p){let y=n.entries[p],h=l.entries[p];(!h||t.isFile(y.mode)&&y.timestamp.getTime()>h.timestamp.getTime())&&(u.push(p),s++)}),u.sort();let c=[];if(Object.keys(l.entries).forEach(function(p){n.entries[p]||(c.push(p),s++)}),c.sort().reverse(),!s)return;let f=n.type==="remote"?n.handles:l.handles;for(let p of u){let y=o.normalize(p.replace(i.mountpoint,"/")).substring(1);if(l.type==="local"){let h=f.get(y),m=await a.loadRemoteEntry(h);a.storeLocalEntry(p,m)}else{let h=a.loadLocalEntry(p);await a.storeRemoteEntry(f,y,h)}}for(let p of c)if(l.type==="local")a.removeLocalEntry(p);else{let y=o.normalize(p.replace(i.mountpoint,"/")).substring(1);await a.removeRemoteEntry(f,y)}}};e.FS.filesystems.NATIVEFS_ASYNC=a}d(ke,"initializeNativeFS");var vt=d(async e=>{let t=[];async function r(a){for await(let i of a.values())t.push(i),i.kind==="directory"&&await r(i)}d(r,"collect"),await r(e);let o=new Map;o.set(".",e);for(let a of t){let i=(await e.resolve(a)).join("/");o.set(i,a)}return o},"getFsHandles");function Pe(e){let t={noImageDecoding:!0,noAudioDecoding:!0,noWasmDecoding:!1,preRun:Ce(e),quit(r,o){throw t.exited={status:r,toThrow:o},o},print:e.stdout,printErr:e.stderr,arguments:e.args,API:{config:e},locateFile:r=>e.indexURL+r,instantiateWasm:Le(e.indexURL)};return t}d(Pe,"createSettings");function Se(e){return function(t){let r="/";try{t.FS.mkdirTree(e)}catch(o){console.error(`Error occurred while making a home directory '${e}':`),console.error(o),console.error(`Using '${r}' for a home directory instead`),e=r}t.FS.chdir(e)}}d(Se,"createHomeDirectory");function Oe(e){return function(t){Object.assign(t.ENV,e)}}d(Oe,"setEnvironment");function Fe(e){return t=>{for(let r of e)t.FS.mkdirTree(r),t.FS.mount(t.FS.filesystems.NODEFS,{root:r},r)}}d(Fe,"mountLocalDirectories");function Te(e){let t=ve(e);return r=>{let o=r._py_version_major(),a=r._py_version_minor();r.FS.mkdirTree("/lib"),r.FS.mkdirTree(`/lib/python${o}.${a}/site-packages`),r.addRunDependency("install-stdlib"),t.then(i=>{r.FS.writeFile(`/lib/python${o}${a}.zip`,i)}).catch(i=>{console.error("Error occurred while installing the standard library:"),console.error(i)}).finally(()=>{r.removeRunDependency("install-stdlib")})}}d(Te,"installStdlib");function Ce(e){let t;return e.stdLibURL!=null?t=e.stdLibURL:t=e.indexURL+"python_stdlib.zip",[Te(t),Se(e.env.HOME),Oe(e.env),Fe(e._node_mounts),ke]}d(Ce,"getFileSystemInitializationFuncs");function Le(e){let{binary:t,response:r}=D(e+"pyodide.asm.wasm");return function(o,a){return async function(){try{let i;r?i=await WebAssembly.instantiateStreaming(r,o):i=await WebAssembly.instantiate(await t,o);let{instance:n,module:l}=i;typeof WasmOffsetConverter<"u"&&(wasmOffsetConverter=new WasmOffsetConverter(wasmBinary,l)),a(n,l)}catch(i){console.warn("wasm instantiation failed!"),console.warn(i)}}(),{}}}d(Le,"getInstantiateWasmFunc");var ce="0.26.1";async function J(e={}){await Y();let t=e.indexURL||await Ee();t=W(t),t.endsWith("/")||(t+="/"),e.indexURL=t;let r={fullStdLib:!1,jsglobals:globalThis,stdin:globalThis.prompt?globalThis.prompt:void 0,lockFileURL:t+"pyodide-lock.json",args:[],_node_mounts:[],env:{},packageCacheDir:t,packages:[],enableRunUntilComplete:!1},o=Object.assign(r,e);o.env.HOME||(o.env.HOME="/home/pyodide");let a=Pe(o),i=a.API;if(i.lockFilePromise=xe(o.lockFileURL),typeof _createPyodideModule!="function"){let c=`${o.indexURL}pyodide.asm.js`;await $(c)}let n;if(e._loadSnapshot){let c=await e._loadSnapshot;ArrayBuffer.isView(c)?n=c:n=new Uint8Array(c),a.noInitialRun=!0,a.INITIAL_MEMORY=n.length}let l=await _createPyodideModule(a);if(a.exited)throw a.exited.toThrow;if(e.pyproxyToStringRepr&&i.setPyProxyToStringMethod(!0),i.version!==ce)throw new Error(`Pyodide version does not match: '${ce}' <==> '${i.version}'. If you updated the Pyodide version, make sure you also updated the 'indexURL' parameter passed to loadPyodide.`);l.locateFile=c=>{throw new Error("Didn't expect to load any more file_packager files!")};let s;n&&(s=i.restoreSnapshot(n));let u=i.finalizeBootstrap(s);return i.sys.path.insert(0,i.config.env.HOME),u.version.includes("dev")||i.setCdnUrl(`https://cdn.jsdelivr.net/pyodide/v${u.version}/full/`),i._pyodide.set_excepthook(),await i.packageIndexReady,i.initializeStreams(o.stdin,o.stdout,o.stderr),u}d(J,"loadPyodide");function X(e){return typeof ImageBitmap<"u"&&e instanceof ImageBitmap}function S(e,t,r,...o){return e==null||X(e)||e instanceof ArrayBuffer||ArrayBuffer.isView(e)?e:t(e)?r(e,...o):Array.isArray(e)?e.map(a=>S(a,t,r,...o)):typeof e=="object"?Object.fromEntries(Object.entries(e).map(([a,i])=>[a,S(i,t,r,...o)])):e}function bt(e){return e&&e[Symbol.toStringTag]=="PyProxy"}function _e(e){return e&&!!e[R]}function xt(e){return e&&typeof e=="object"&&"_comlinkProxy"in e&&"ptr"in e}function Et(e){return e&&e[Symbol.toStringTag]=="Map"}function K(e){if(_e(e))return!0;if(e==null||e instanceof ArrayBuffer||ArrayBuffer.isView(e))return!1;if(e instanceof Array)return e.some(t=>K(t));if(typeof e=="object")return Object.entries(e).some(([t,r])=>K(r))}var Ne={},Re={canHandle:bt,serialize(e){let t=self.pyodide._module.PyProxy_getPtr(e);Ne[t]=e;let{port1:r,port2:o}=new MessageChannel;return k(e,r),[[o,t],[o]]},deserialize([e,t]){e.start();let r=A(e);return new Proxy(r,{get:(a,i)=>i==="_ptr"?t:a[i]})}},Ae={canHandle:K,serialize(e){return[S(e,_e,t=>({_comlinkProxy:!0,ptr:t._ptr})),[]]},deserialize(e){return S(e,xt,t=>Ne[t.ptr])}},Ie={canHandle:X,serialize(e){if(e.width==0&&e.height==0){let t=new OffscreenCanvas(1,1);t.getContext("2d"),e=t.transferToImageBitmap()}return[e,[e]]},deserialize(e){return e}},Me={canHandle:Et,serialize(e){return[Object.fromEntries(e.entries()),[]]},deserialize(e){return e}};var kt={mkdir(e){self.pyodide._FS.mkdir(e)},writeFile(e,t){self.pyodide._FS.writeFile(e,t)}};async function Pt(e){return self.pyodide=await J(e),self.pyodide.registerComlink(M),self.pyodide._FS=self.pyodide.FS,self.pyodide.FS=kt,v.set("PyProxy",Re),v.set("Comlink",Ae),v.set("ImageBitmap",Ie),v.set("Map",Me),I(self.pyodide)}k({init:Pt});
10 | /*! Bundled license information:
11 |
12 | comlink/dist/esm/comlink.mjs:
13 | (**
14 | * @license
15 | * Copyright 2019 Google LLC
16 | * SPDX-License-Identifier: Apache-2.0
17 | *)
18 | */
19 |
--------------------------------------------------------------------------------
/inst/app/quarto/_extensions/r-wasm/live/resources/tinyyaml.lua:
--------------------------------------------------------------------------------
1 | -------------------------------------------------------------------------------
2 | -- tinyyaml - YAML subset parser
3 | -- https://github.com/api7/lua-tinyyaml
4 | --
5 | -- MIT License
6 | --
7 | -- Copyright (c) 2017 peposso
8 | --
9 | -- Permission is hereby granted, free of charge, to any person obtaining a copy
10 | -- of this software and associated documentation files (the "Software"), to deal
11 | -- in the Software without restriction, including without limitation the rights
12 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | -- copies of the Software, and to permit persons to whom the Software is
14 | -- furnished to do so, subject to the following conditions:
15 | --
16 | -- The above copyright notice and this permission notice shall be included in all
17 | -- copies or substantial portions of the Software.
18 | --
19 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | -- SOFTWARE.
26 | -------------------------------------------------------------------------------
27 |
28 | local table = table
29 | local string = string
30 | local schar = string.char
31 | local ssub, gsub = string.sub, string.gsub
32 | local sfind, smatch = string.find, string.match
33 | local tinsert, tconcat, tremove = table.insert, table.concat, table.remove
34 | local setmetatable = setmetatable
35 | local pairs = pairs
36 | local rawget = rawget
37 | local type = type
38 | local tonumber = tonumber
39 | local math = math
40 | local getmetatable = getmetatable
41 | local error = error
42 | local end_symbol = "..."
43 | local end_break_symbol = "...\n"
44 |
45 | local UNESCAPES = {
46 | ['0'] = "\x00", z = "\x00", N = "\x85",
47 | a = "\x07", b = "\x08", t = "\x09",
48 | n = "\x0a", v = "\x0b", f = "\x0c",
49 | r = "\x0d", e = "\x1b", ['\\'] = '\\',
50 | };
51 |
52 | -------------------------------------------------------------------------------
53 | -- utils
54 | local function select(list, pred)
55 | local selected = {}
56 | for i = 0, #list do
57 | local v = list[i]
58 | if v and pred(v, i) then
59 | tinsert(selected, v)
60 | end
61 | end
62 | return selected
63 | end
64 |
65 | local function startswith(haystack, needle)
66 | return ssub(haystack, 1, #needle) == needle
67 | end
68 |
69 | local function ltrim(str)
70 | return smatch(str, "^%s*(.-)$")
71 | end
72 |
73 | local function rtrim(str)
74 | return smatch(str, "^(.-)%s*$")
75 | end
76 |
77 | local function trim(str)
78 | return smatch(str, "^%s*(.-)%s*$")
79 | end
80 |
81 | -------------------------------------------------------------------------------
82 | -- Implementation.
83 | --
84 | local class = {__meta={}}
85 | function class.__meta.__call(cls, ...)
86 | local self = setmetatable({}, cls)
87 | if cls.__init then
88 | cls.__init(self, ...)
89 | end
90 | return self
91 | end
92 |
93 | function class.def(base, typ, cls)
94 | base = base or class
95 | local mt = {__metatable=base, __index=base}
96 | for k, v in pairs(base.__meta) do mt[k] = v end
97 | cls = setmetatable(cls or {}, mt)
98 | cls.__index = cls
99 | cls.__metatable = cls
100 | cls.__type = typ
101 | cls.__meta = mt
102 | return cls
103 | end
104 |
105 |
106 | local types = {
107 | null = class:def('null'),
108 | map = class:def('map'),
109 | omap = class:def('omap'),
110 | pairs = class:def('pairs'),
111 | set = class:def('set'),
112 | seq = class:def('seq'),
113 | timestamp = class:def('timestamp'),
114 | }
115 |
116 | local Null = types.null
117 | function Null.__tostring() return 'yaml.null' end
118 | function Null.isnull(v)
119 | if v == nil then return true end
120 | if type(v) == 'table' and getmetatable(v) == Null then return true end
121 | return false
122 | end
123 | local null = Null()
124 |
125 | function types.timestamp:__init(y, m, d, h, i, s, f, z)
126 | self.year = tonumber(y)
127 | self.month = tonumber(m)
128 | self.day = tonumber(d)
129 | self.hour = tonumber(h or 0)
130 | self.minute = tonumber(i or 0)
131 | self.second = tonumber(s or 0)
132 | if type(f) == 'string' and sfind(f, '^%d+$') then
133 | self.fraction = tonumber(f) * 10 ^ (3 - #f)
134 | elseif f then
135 | self.fraction = f
136 | else
137 | self.fraction = 0
138 | end
139 | self.timezone = z
140 | end
141 |
142 | function types.timestamp:__tostring()
143 | return string.format(
144 | '%04d-%02d-%02dT%02d:%02d:%02d.%03d%s',
145 | self.year, self.month, self.day,
146 | self.hour, self.minute, self.second, self.fraction,
147 | self:gettz())
148 | end
149 |
150 | function types.timestamp:gettz()
151 | if not self.timezone then
152 | return ''
153 | end
154 | if self.timezone == 0 then
155 | return 'Z'
156 | end
157 | local sign = self.timezone > 0
158 | local z = sign and self.timezone or -self.timezone
159 | local zh = math.floor(z)
160 | local zi = (z - zh) * 60
161 | return string.format(
162 | '%s%02d:%02d', sign and '+' or '-', zh, zi)
163 | end
164 |
165 |
166 | local function countindent(line)
167 | local _, j = sfind(line, '^%s+')
168 | if not j then
169 | return 0, line
170 | end
171 | return j, ssub(line, j+1)
172 | end
173 |
174 | local Parser = {
175 | timestamps=true,-- parse timestamps as objects instead of strings
176 | }
177 |
178 | function Parser:parsestring(line, stopper)
179 | stopper = stopper or ''
180 | local q = ssub(line, 1, 1)
181 | if q == ' ' or q == '\t' then
182 | return self:parsestring(ssub(line, 2))
183 | end
184 | if q == "'" then
185 | local i = sfind(line, "'", 2, true)
186 | if not i then
187 | return nil, line
188 | end
189 | -- Unescape repeated single quotes.
190 | while i < #line and ssub(line, i+1, i+1) == "'" do
191 | i = sfind(line, "'", i + 2, true)
192 | if not i then
193 | return nil, line
194 | end
195 | end
196 | return ssub(line, 2, i-1):gsub("''", "'"), ssub(line, i+1)
197 | end
198 | if q == '"' then
199 | local i, buf = 2, ''
200 | while i < #line do
201 | local c = ssub(line, i, i)
202 | if c == '\\' then
203 | local n = ssub(line, i+1, i+1)
204 | if UNESCAPES[n] ~= nil then
205 | buf = buf..UNESCAPES[n]
206 | elseif n == 'x' then
207 | local h = ssub(i+2,i+3)
208 | if sfind(h, '^[0-9a-fA-F]$') then
209 | buf = buf..schar(tonumber(h, 16))
210 | i = i + 2
211 | else
212 | buf = buf..'x'
213 | end
214 | else
215 | buf = buf..n
216 | end
217 | i = i + 1
218 | elseif c == q then
219 | break
220 | else
221 | buf = buf..c
222 | end
223 | i = i + 1
224 | end
225 | return buf, ssub(line, i+1)
226 | end
227 | if q == '{' or q == '[' then -- flow style
228 | return nil, line
229 | end
230 | if q == '|' or q == '>' then -- block
231 | return nil, line
232 | end
233 | if q == '-' or q == ':' then
234 | if ssub(line, 2, 2) == ' ' or ssub(line, 2, 2) == '\n' or #line == 1 then
235 | return nil, line
236 | end
237 | end
238 |
239 | if line == "*" then
240 | error("did not find expected alphabetic or numeric character")
241 | end
242 |
243 | local buf = ''
244 | while #line > 0 do
245 | local c = ssub(line, 1, 1)
246 | if sfind(stopper, c, 1, true) then
247 | break
248 | elseif c == ':' and (ssub(line, 2, 2) == ' ' or ssub(line, 2, 2) == '\n' or #line == 1) then
249 | break
250 | elseif c == '#' and (ssub(buf, #buf, #buf) == ' ') then
251 | break
252 | else
253 | buf = buf..c
254 | end
255 | line = ssub(line, 2)
256 | end
257 | buf = rtrim(buf)
258 | local val = tonumber(buf) or buf
259 | return val, line
260 | end
261 |
262 | local function isemptyline(line)
263 | return line == '' or sfind(line, '^%s*$') or sfind(line, '^%s*#')
264 | end
265 |
266 | local function equalsline(line, needle)
267 | return startswith(line, needle) and isemptyline(ssub(line, #needle+1))
268 | end
269 |
270 | local function compactifyemptylines(lines)
271 | -- Appends empty lines as "\n" to the end of the nearest preceding non-empty line
272 | local compactified = {}
273 | local lastline = {}
274 | for i = 1, #lines do
275 | local line = lines[i]
276 | if isemptyline(line) then
277 | if #compactified > 0 and i < #lines then
278 | tinsert(lastline, "\n")
279 | end
280 | else
281 | if #lastline > 0 then
282 | tinsert(compactified, tconcat(lastline, ""))
283 | end
284 | lastline = {line}
285 | end
286 | end
287 | if #lastline > 0 then
288 | tinsert(compactified, tconcat(lastline, ""))
289 | end
290 | return compactified
291 | end
292 |
293 | local function checkdupekey(map, key)
294 | if rawget(map, key) ~= nil then
295 | -- print("found a duplicate key '"..key.."' in line: "..line)
296 | local suffix = 1
297 | while rawget(map, key..'_'..suffix) do
298 | suffix = suffix + 1
299 | end
300 | key = key ..'_'..suffix
301 | end
302 | return key
303 | end
304 |
305 |
306 | function Parser:parseflowstyle(line, lines)
307 | local stack = {}
308 | while true do
309 | if #line == 0 then
310 | if #lines == 0 then
311 | break
312 | else
313 | line = tremove(lines, 1)
314 | end
315 | end
316 | local c = ssub(line, 1, 1)
317 | if c == '#' then
318 | line = ''
319 | elseif c == ' ' or c == '\t' or c == '\r' or c == '\n' then
320 | line = ssub(line, 2)
321 | elseif c == '{' or c == '[' then
322 | tinsert(stack, {v={},t=c})
323 | line = ssub(line, 2)
324 | elseif c == ':' then
325 | local s = tremove(stack)
326 | tinsert(stack, {v=s.v, t=':'})
327 | line = ssub(line, 2)
328 | elseif c == ',' then
329 | local value = tremove(stack)
330 | if value.t == ':' or value.t == '{' or value.t == '[' then error() end
331 | if stack[#stack].t == ':' then
332 | -- map
333 | local key = tremove(stack)
334 | key.v = checkdupekey(stack[#stack].v, key.v)
335 | stack[#stack].v[key.v] = value.v
336 | elseif stack[#stack].t == '{' then
337 | -- set
338 | stack[#stack].v[value.v] = true
339 | elseif stack[#stack].t == '[' then
340 | -- seq
341 | tinsert(stack[#stack].v, value.v)
342 | end
343 | line = ssub(line, 2)
344 | elseif c == '}' then
345 | if stack[#stack].t == '{' then
346 | if #stack == 1 then break end
347 | stack[#stack].t = '}'
348 | line = ssub(line, 2)
349 | else
350 | line = ','..line
351 | end
352 | elseif c == ']' then
353 | if stack[#stack].t == '[' then
354 | if #stack == 1 then break end
355 | stack[#stack].t = ']'
356 | line = ssub(line, 2)
357 | else
358 | line = ','..line
359 | end
360 | else
361 | local s, rest = self:parsestring(line, ',{}[]')
362 | if not s then
363 | error('invalid flowstyle line: '..line)
364 | end
365 | tinsert(stack, {v=s, t='s'})
366 | line = rest
367 | end
368 | end
369 | return stack[1].v, line
370 | end
371 |
372 | function Parser:parseblockstylestring(line, lines, indent)
373 | if #lines == 0 then
374 | error("failed to find multi-line scalar content")
375 | end
376 | local s = {}
377 | local firstindent = -1
378 | local endline = -1
379 | for i = 1, #lines do
380 | local ln = lines[i]
381 | local idt = countindent(ln)
382 | if idt <= indent then
383 | break
384 | end
385 | if ln == '' then
386 | tinsert(s, '')
387 | else
388 | if firstindent == -1 then
389 | firstindent = idt
390 | elseif idt < firstindent then
391 | break
392 | end
393 | tinsert(s, ssub(ln, firstindent + 1))
394 | end
395 | endline = i
396 | end
397 |
398 | local striptrailing = true
399 | local sep = '\n'
400 | local newlineatend = true
401 | if line == '|' then
402 | striptrailing = true
403 | sep = '\n'
404 | newlineatend = true
405 | elseif line == '|+' then
406 | striptrailing = false
407 | sep = '\n'
408 | newlineatend = true
409 | elseif line == '|-' then
410 | striptrailing = true
411 | sep = '\n'
412 | newlineatend = false
413 | elseif line == '>' then
414 | striptrailing = true
415 | sep = ' '
416 | newlineatend = true
417 | elseif line == '>+' then
418 | striptrailing = false
419 | sep = ' '
420 | newlineatend = true
421 | elseif line == '>-' then
422 | striptrailing = true
423 | sep = ' '
424 | newlineatend = false
425 | else
426 | error('invalid blockstyle string:'..line)
427 | end
428 |
429 | if #s == 0 then
430 | return ""
431 | end
432 |
433 | local _, eonl = s[#s]:gsub('\n', '\n')
434 | s[#s] = rtrim(s[#s])
435 | if striptrailing then
436 | eonl = 0
437 | end
438 | if newlineatend then
439 | eonl = eonl + 1
440 | end
441 | for i = endline, 1, -1 do
442 | tremove(lines, i)
443 | end
444 | return tconcat(s, sep)..string.rep('\n', eonl)
445 | end
446 |
447 | function Parser:parsetimestamp(line)
448 | local _, p1, y, m, d = sfind(line, '^(%d%d%d%d)%-(%d%d)%-(%d%d)')
449 | if not p1 then
450 | return nil, line
451 | end
452 | if p1 == #line then
453 | return types.timestamp(y, m, d), ''
454 | end
455 | local _, p2, h, i, s = sfind(line, '^[Tt ](%d+):(%d+):(%d+)', p1+1)
456 | if not p2 then
457 | return types.timestamp(y, m, d), ssub(line, p1+1)
458 | end
459 | if p2 == #line then
460 | return types.timestamp(y, m, d, h, i, s), ''
461 | end
462 | local _, p3, f = sfind(line, '^%.(%d+)', p2+1)
463 | if not p3 then
464 | p3 = p2
465 | f = 0
466 | end
467 | local zc = ssub(line, p3+1, p3+1)
468 | local _, p4, zs, z = sfind(line, '^ ?([%+%-])(%d+)', p3+1)
469 | if p4 then
470 | z = tonumber(z)
471 | local _, p5, zi = sfind(line, '^:(%d+)', p4+1)
472 | if p5 then
473 | z = z + tonumber(zi) / 60
474 | end
475 | z = zs == '-' and -tonumber(z) or tonumber(z)
476 | elseif zc == 'Z' then
477 | p4 = p3 + 1
478 | z = 0
479 | else
480 | p4 = p3
481 | z = false
482 | end
483 | return types.timestamp(y, m, d, h, i, s, f, z), ssub(line, p4+1)
484 | end
485 |
486 | function Parser:parsescalar(line, lines, indent)
487 | line = trim(line)
488 | line = gsub(line, '^%s*#.*$', '') -- comment only -> ''
489 | line = gsub(line, '^%s*', '') -- trim head spaces
490 |
491 | if line == '' or line == '~' then
492 | return null
493 | end
494 |
495 | if self.timestamps then
496 | local ts, _ = self:parsetimestamp(line)
497 | if ts then
498 | return ts
499 | end
500 | end
501 |
502 | local s, _ = self:parsestring(line)
503 | -- startswith quote ... string
504 | -- not startswith quote ... maybe string
505 | if s and (startswith(line, '"') or startswith(line, "'")) then
506 | return s
507 | end
508 |
509 | if startswith('!', line) then -- unexpected tagchar
510 | error('unsupported line: '..line)
511 | end
512 |
513 | if equalsline(line, '{}') then
514 | return {}
515 | end
516 | if equalsline(line, '[]') then
517 | return {}
518 | end
519 |
520 | if startswith(line, '{') or startswith(line, '[') then
521 | return self:parseflowstyle(line, lines)
522 | end
523 |
524 | if startswith(line, '|') or startswith(line, '>') then
525 | return self:parseblockstylestring(line, lines, indent)
526 | end
527 |
528 | -- Regular unquoted string
529 | line = gsub(line, '%s*#.*$', '') -- trim tail comment
530 | local v = line
531 | if v == 'null' or v == 'Null' or v == 'NULL'then
532 | return null
533 | elseif v == 'true' or v == 'True' or v == 'TRUE' then
534 | return true
535 | elseif v == 'false' or v == 'False' or v == 'FALSE' then
536 | return false
537 | elseif v == '.inf' or v == '.Inf' or v == '.INF' then
538 | return math.huge
539 | elseif v == '+.inf' or v == '+.Inf' or v == '+.INF' then
540 | return math.huge
541 | elseif v == '-.inf' or v == '-.Inf' or v == '-.INF' then
542 | return -math.huge
543 | elseif v == '.nan' or v == '.NaN' or v == '.NAN' then
544 | return 0 / 0
545 | elseif sfind(v, '^[%+%-]?[0-9]+$') or sfind(v, '^[%+%-]?[0-9]+%.$')then
546 | return tonumber(v) -- : int
547 | elseif sfind(v, '^[%+%-]?[0-9]+%.[0-9]+$') then
548 | return tonumber(v)
549 | end
550 | return s or v
551 | end
552 |
553 | function Parser:parseseq(line, lines, indent)
554 | local seq = setmetatable({}, types.seq)
555 | if line ~= '' then
556 | error()
557 | end
558 | while #lines > 0 do
559 | -- Check for a new document
560 | line = lines[1]
561 | if startswith(line, '---') then
562 | while #lines > 0 and not startswith(lines, '---') do
563 | tremove(lines, 1)
564 | end
565 | return seq
566 | end
567 |
568 | -- Check the indent level
569 | local level = countindent(line)
570 | if level < indent then
571 | return seq
572 | elseif level > indent then
573 | error("found bad indenting in line: ".. line)
574 | end
575 |
576 | local i, j = sfind(line, '%-%s+')
577 | if not i then
578 | i, j = sfind(line, '%-$')
579 | if not i then
580 | return seq
581 | end
582 | end
583 | local rest = ssub(line, j+1)
584 |
585 | if sfind(rest, '^[^\'\"%s]*:%s*$') or sfind(rest, '^[^\'\"%s]*:%s+.') then
586 | -- Inline nested hash
587 | -- There are two patterns need to match as inline nested hash
588 | -- first one should have no other characters except whitespace after `:`
589 | -- and the second one should have characters besides whitespace after `:`
590 | --
591 | -- value:
592 | -- - foo:
593 | -- bar: 1
594 | --
595 | -- and
596 | --
597 | -- value:
598 | -- - foo: bar
599 | --
600 | -- And there is one pattern should not be matched, where there is no space after `:`
601 | -- in below, `foo:bar` should be parsed into a single string
602 | --
603 | -- value:
604 | -- - foo:bar
605 | local indent2 = j or 0
606 | lines[1] = string.rep(' ', indent2)..rest
607 | tinsert(seq, self:parsemap('', lines, indent2))
608 | elseif sfind(rest, '^%-%s+') then
609 | -- Inline nested seq
610 | local indent2 = j or 0
611 | lines[1] = string.rep(' ', indent2)..rest
612 | tinsert(seq, self:parseseq('', lines, indent2))
613 | elseif isemptyline(rest) then
614 | tremove(lines, 1)
615 | if #lines == 0 then
616 | tinsert(seq, null)
617 | return seq
618 | end
619 | if sfind(lines[1], '^%s*%-') then
620 | local nextline = lines[1]
621 | local indent2 = countindent(nextline)
622 | if indent2 == indent then
623 | -- Null seqay entry
624 | tinsert(seq, null)
625 | else
626 | tinsert(seq, self:parseseq('', lines, indent2))
627 | end
628 | else
629 | -- - # comment
630 | -- key: value
631 | local nextline = lines[1]
632 | local indent2 = countindent(nextline)
633 | tinsert(seq, self:parsemap('', lines, indent2))
634 | end
635 | elseif line == "*" then
636 | error("did not find expected alphabetic or numeric character")
637 | elseif rest then
638 | -- Array entry with a value
639 | local nextline = lines[1]
640 | local indent2 = countindent(nextline)
641 | tremove(lines, 1)
642 | tinsert(seq, self:parsescalar(rest, lines, indent2))
643 | end
644 | end
645 | return seq
646 | end
647 |
648 | function Parser:parseset(line, lines, indent)
649 | if not isemptyline(line) then
650 | error('not seq line: '..line)
651 | end
652 | local set = setmetatable({}, types.set)
653 | while #lines > 0 do
654 | -- Check for a new document
655 | line = lines[1]
656 | if startswith(line, '---') then
657 | while #lines > 0 and not startswith(lines, '---') do
658 | tremove(lines, 1)
659 | end
660 | return set
661 | end
662 |
663 | -- Check the indent level
664 | local level = countindent(line)
665 | if level < indent then
666 | return set
667 | elseif level > indent then
668 | error("found bad indenting in line: ".. line)
669 | end
670 |
671 | local i, j = sfind(line, '%?%s+')
672 | if not i then
673 | i, j = sfind(line, '%?$')
674 | if not i then
675 | return set
676 | end
677 | end
678 | local rest = ssub(line, j+1)
679 |
680 | if sfind(rest, '^[^\'\"%s]*:') then
681 | -- Inline nested hash
682 | local indent2 = j or 0
683 | lines[1] = string.rep(' ', indent2)..rest
684 | set[self:parsemap('', lines, indent2)] = true
685 | elseif sfind(rest, '^%s+$') then
686 | tremove(lines, 1)
687 | if #lines == 0 then
688 | tinsert(set, null)
689 | return set
690 | end
691 | if sfind(lines[1], '^%s*%?') then
692 | local indent2 = countindent(lines[1])
693 | if indent2 == indent then
694 | -- Null array entry
695 | set[null] = true
696 | else
697 | set[self:parseseq('', lines, indent2)] = true
698 | end
699 | end
700 |
701 | elseif rest then
702 | tremove(lines, 1)
703 | set[self:parsescalar(rest, lines)] = true
704 | else
705 | error("failed to classify line: "..line)
706 | end
707 | end
708 | return set
709 | end
710 |
711 | function Parser:parsemap(line, lines, indent)
712 | if not isemptyline(line) then
713 | error('not map line: '..line)
714 | end
715 | local map = setmetatable({}, types.map)
716 | while #lines > 0 do
717 | -- Check for a new document
718 | line = lines[1]
719 | if line == end_symbol or line == end_break_symbol then
720 | for i, _ in ipairs(lines) do
721 | lines[i] = nil
722 | end
723 | return map
724 | end
725 |
726 | if startswith(line, '---') then
727 | while #lines > 0 and not startswith(lines, '---') do
728 | tremove(lines, 1)
729 | end
730 | return map
731 | end
732 |
733 | -- Check the indent level
734 | local level, _ = countindent(line)
735 | if level < indent then
736 | return map
737 | elseif level > indent then
738 | error("found bad indenting in line: ".. line)
739 | end
740 |
741 | -- Find the key
742 | local key
743 | local s, rest = self:parsestring(line)
744 |
745 | -- Quoted keys
746 | if s and startswith(rest, ':') then
747 | local sc = self:parsescalar(s, {}, 0)
748 | if sc and type(sc) ~= 'string' then
749 | key = sc
750 | else
751 | key = s
752 | end
753 | line = ssub(rest, 2)
754 | else
755 | error("failed to classify line: "..line)
756 | end
757 |
758 | key = checkdupekey(map, key)
759 | line = ltrim(line)
760 |
761 | if ssub(line, 1, 1) == '!' then
762 | -- ignore type
763 | local rh = ltrim(ssub(line, 3))
764 | local typename = smatch(rh, '^!?[^%s]+')
765 | line = ltrim(ssub(rh, #typename+1))
766 | end
767 |
768 | if not isemptyline(line) then
769 | tremove(lines, 1)
770 | line = ltrim(line)
771 | map[key] = self:parsescalar(line, lines, indent)
772 | else
773 | -- An indent
774 | tremove(lines, 1)
775 | if #lines == 0 then
776 | map[key] = null
777 | return map;
778 | end
779 | if sfind(lines[1], '^%s*%-') then
780 | local indent2 = countindent(lines[1])
781 | map[key] = self:parseseq('', lines, indent2)
782 | elseif sfind(lines[1], '^%s*%?') then
783 | local indent2 = countindent(lines[1])
784 | map[key] = self:parseset('', lines, indent2)
785 | else
786 | local indent2 = countindent(lines[1])
787 | if indent >= indent2 then
788 | -- Null hash entry
789 | map[key] = null
790 | else
791 | map[key] = self:parsemap('', lines, indent2)
792 | end
793 | end
794 | end
795 | end
796 | return map
797 | end
798 |
799 |
800 | -- : (list)->dict
801 | function Parser:parsedocuments(lines)
802 | lines = compactifyemptylines(lines)
803 |
804 | if sfind(lines[1], '^%%YAML') then tremove(lines, 1) end
805 |
806 | local root = {}
807 | local in_document = false
808 | while #lines > 0 do
809 | local line = lines[1]
810 | -- Do we have a document header?
811 | local docright;
812 | if sfind(line, '^%-%-%-') then
813 | -- Handle scalar documents
814 | docright = ssub(line, 4)
815 | tremove(lines, 1)
816 | in_document = true
817 | end
818 | if docright then
819 | if (not sfind(docright, '^%s+$') and
820 | not sfind(docright, '^%s+#')) then
821 | tinsert(root, self:parsescalar(docright, lines))
822 | end
823 | elseif #lines == 0 or startswith(line, '---') then
824 | -- A naked document
825 | tinsert(root, null)
826 | while #lines > 0 and not sfind(lines[1], '---') do
827 | tremove(lines, 1)
828 | end
829 | in_document = false
830 | -- XXX The final '-+$' is to look for -- which ends up being an
831 | -- error later.
832 | elseif not in_document and #root > 0 then
833 | -- only the first document can be explicit
834 | error('parse error: '..line)
835 | elseif sfind(line, '^%s*%-') then
836 | -- An array at the root
837 | tinsert(root, self:parseseq('', lines, 0))
838 | elseif sfind(line, '^%s*[^%s]') then
839 | -- A hash at the root
840 | local level = countindent(line)
841 | tinsert(root, self:parsemap('', lines, level))
842 | else
843 | -- Shouldn't get here. @lines have whitespace-only lines
844 | -- stripped, and previous match is a line with any
845 | -- non-whitespace. So this clause should only be reachable via
846 | -- a perlbug where \s is not symmetric with \S
847 |
848 | -- uncoverable statement
849 | error('parse error: '..line)
850 | end
851 | end
852 | if #root > 1 and Null.isnull(root[1]) then
853 | tremove(root, 1)
854 | return root
855 | end
856 | return root
857 | end
858 |
859 | --- Parse yaml string into table.
860 | function Parser:parse(source)
861 | local lines = {}
862 | for line in string.gmatch(source .. '\n', '(.-)\r?\n') do
863 | tinsert(lines, line)
864 | end
865 |
866 | local docs = self:parsedocuments(lines)
867 | if #docs == 1 then
868 | return docs[1]
869 | end
870 |
871 | return docs
872 | end
873 |
874 | local function parse(source, options)
875 | local options = options or {}
876 | local parser = setmetatable (options, {__index=Parser})
877 | return parser:parse(source)
878 | end
879 |
880 | return {
881 | version = 0.1,
882 | parse = parse,
883 | }
884 |
--------------------------------------------------------------------------------
/inst/app/quarto/_extensions/r-wasm/live/templates/interpolate.ojs:
--------------------------------------------------------------------------------
1 | {
2 | const { interpolate } = window._exercise_ojs_runtime;
3 | const block_id = "{{block_id}}";
4 | const language = "{{language}}";
5 | const def_map = {{def_map}};
6 | const elem = document.getElementById(`interpolate-${block_id}`);
7 |
8 | // Store original templated HTML for reference in future reactive updates
9 | if (!elem.origHTML) elem.origHTML = elem.innerHTML;
10 |
11 | // Interpolate reactive OJS variables into established HTML element
12 | elem.innerHTML = elem.origHTML;
13 | Object.keys(def_map).forEach((def) =>
14 | interpolate(elem, "${" + def + "}", def_map[def], language)
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/inst/app/quarto/_extensions/r-wasm/live/templates/pyodide-editor.ojs:
--------------------------------------------------------------------------------
1 | viewof _pyodide_editor_{{block_id}} = {
2 | const { PyodideExerciseEditor, b64Decode } = window._exercise_ojs_runtime;
3 |
4 | const scriptContent = document.querySelector(`script[type=\"pyodide-{{block_id}}-contents\"]`).textContent;
5 | const block = JSON.parse(b64Decode(scriptContent));
6 |
7 | const options = Object.assign({ id: `pyodide-{{block_id}}-contents` }, block.attr);
8 | const editor = new PyodideExerciseEditor(
9 | pyodideOjs.pyodidePromise,
10 | block.code,
11 | options
12 | );
13 |
14 | return editor.container;
15 | }
16 | _pyodide_value_{{block_id}} = pyodideOjs.process(_pyodide_editor_{{block_id}}, {{block_input}});
17 |
--------------------------------------------------------------------------------
/inst/app/quarto/_extensions/r-wasm/live/templates/pyodide-evaluate.ojs:
--------------------------------------------------------------------------------
1 | _pyodide_value_{{block_id}} = {
2 | const { highlightPython, b64Decode} = window._exercise_ojs_runtime;
3 |
4 | const scriptContent = document.querySelector(`script[type=\"pyodide-{{block_id}}-contents\"]`).textContent;
5 | const block = JSON.parse(b64Decode(scriptContent));
6 |
7 | // Default evaluation configuration
8 | const options = Object.assign({
9 | id: "pyodide-{{block_id}}-contents",
10 | echo: true,
11 | output: true
12 | }, block.attr);
13 |
14 | // Evaluate the provided Python code
15 | const result = pyodideOjs.process({code: block.code, options}, {{block_input}});
16 |
17 | // Early yield while we wait for the first evaluation and render
18 | if (options.output && !("{{block_id}}" in pyodideOjs.renderedOjs)) {
19 | const container = document.createElement("div");
20 | const spinner = document.createElement("div");
21 |
22 | if (options.echo) {
23 | // Show output as highlighted source
24 | const preElem = document.createElement("pre");
25 | container.className = "sourceCode";
26 | preElem.className = "sourceCode python";
27 | preElem.appendChild(highlightPython(block.code));
28 | spinner.className = "spinner-grow spinner-grow-sm m-2 position-absolute top-0 end-0";
29 | preElem.appendChild(spinner);
30 | container.appendChild(preElem);
31 | } else {
32 | spinner.className = "spinner-border spinner-border-sm";
33 | container.appendChild(spinner);
34 | }
35 |
36 | yield container;
37 | }
38 |
39 | pyodideOjs.renderedOjs["{{block_id}}"] = true;
40 | yield await result;
41 | }
42 |
--------------------------------------------------------------------------------
/inst/app/quarto/_extensions/r-wasm/live/templates/pyodide-exercise.ojs:
--------------------------------------------------------------------------------
1 | viewof _pyodide_editor_{{block_id}} = {
2 | const { PyodideExerciseEditor, b64Decode } = window._exercise_ojs_runtime;
3 |
4 | const scriptContent = document.querySelector(`script[type=\"pyodide-{{block_id}}-contents\"]`).textContent;
5 | const block = JSON.parse(b64Decode(scriptContent));
6 |
7 | // Default exercise configuration
8 | const options = Object.assign(
9 | {
10 | id: "pyodide-{{block_id}}-contents",
11 | envir: `exercise-env-${block.attr.exercise}`,
12 | error: false,
13 | caption: 'Exercise',
14 | },
15 | block.attr
16 | );
17 |
18 | const editor = new PyodideExerciseEditor(pyodideOjs.pyodidePromise, block.code, options);
19 | return editor.container;
20 | }
21 | viewof _pyodide_value_{{block_id}} = pyodideOjs.process(_pyodide_editor_{{block_id}}, {{block_input}});
22 | _pyodide_feedback_{{block_id}} = {
23 | const { PyodideGrader } = window._exercise_ojs_runtime;
24 | const emptyFeedback = document.createElement('div');
25 |
26 | const grader = new PyodideGrader(_pyodide_value_{{block_id}}.evaluator);
27 | const feedback = await grader.gradeExercise();
28 | if (!feedback) return emptyFeedback;
29 | return feedback;
30 | }
31 |
--------------------------------------------------------------------------------
/inst/app/quarto/_extensions/r-wasm/live/templates/pyodide-setup.ojs:
--------------------------------------------------------------------------------
1 | pyodideOjs = {
2 | const {
3 | PyodideEvaluator,
4 | PyodideEnvironmentManager,
5 | setupPython,
6 | startPyodideWorker,
7 | b64Decode,
8 | collapsePath,
9 | } = window._exercise_ojs_runtime;
10 |
11 | const statusContainer = document.getElementById("exercise-loading-status");
12 | const indicatorContainer = document.getElementById("exercise-loading-indicator");
13 | indicatorContainer.classList.remove("d-none");
14 |
15 | let statusText = document.createElement("div")
16 | statusText.classList = "exercise-loading-details";
17 | statusText = statusContainer.appendChild(statusText);
18 | statusText.textContent = `Initialise`;
19 |
20 | // Hoist indicator out from final slide when running under reveal
21 | const revealStatus = document.querySelector(".reveal .exercise-loading-indicator");
22 | if (revealStatus) {
23 | revealStatus.remove();
24 | document.querySelector(".reveal > .slides").appendChild(revealStatus);
25 | }
26 |
27 | // Make any reveal slides with live cells scrollable
28 | document.querySelectorAll(".reveal .exercise-cell").forEach((el) => {
29 | el.closest('section.slide').classList.add("scrollable");
30 | })
31 |
32 | // Pyodide supplemental data and options
33 | const dataContent = document.querySelector(`script[type=\"pyodide-data\"]`).textContent;
34 | const data = JSON.parse(b64Decode(dataContent));
35 |
36 | // Grab list of resources to be downloaded
37 | const filesContent = document.querySelector(`script[type=\"vfs-file\"]`).textContent;
38 | const files = JSON.parse(b64Decode(filesContent));
39 |
40 | let pyodidePromise = (async () => {
41 | statusText.textContent = `Downloading Pyodide`;
42 | const pyodide = await startPyodideWorker(data.options);
43 |
44 | statusText.textContent = `Downloading package: micropip`;
45 | await pyodide.loadPackage("micropip");
46 | const micropip = await pyodide.pyimport("micropip");
47 | await data.packages.pkgs.map((pkg) => () => {
48 | statusText.textContent = `Downloading package: ${pkg}`;
49 | return micropip.install(pkg);
50 | }).reduce((cur, next) => cur.then(next), Promise.resolve());
51 | await micropip.destroy();
52 |
53 | // Download and install resources
54 | await files.map((file) => async () => {
55 | const name = file.substring(file.lastIndexOf('/') + 1);
56 | statusText.textContent = `Downloading resource: ${name}`;
57 | const response = await fetch(file);
58 | if (!response.ok) {
59 | throw new Error(`Can't download \`${file}\`. Error ${response.status}: "${response.statusText}".`);
60 | }
61 | const data = await response.arrayBuffer();
62 |
63 | // Store URLs in the cwd without any subdirectory structure
64 | if (file.includes("://")) {
65 | file = name;
66 | }
67 |
68 | // Collapse higher directory structure
69 | file = collapsePath(file);
70 |
71 | // Create directory tree, ignoring "directory exists" VFS errors
72 | const parts = file.split('/').slice(0, -1);
73 | let path = '';
74 | while (parts.length > 0) {
75 | path += parts.shift() + '/';
76 | try {
77 | await pyodide.FS.mkdir(path);
78 | } catch (e) {
79 | if (e.name !== "ErrnoError") throw e;
80 | if (e.errno !== 20) {
81 | const errorTextPtr = await pyodide._module._strerror(e.errno);
82 | const errorText = await pyodide._module.UTF8ToString(errorTextPtr);
83 | throw new Error(`Filesystem Error ${e.errno} "${errorText}".`);
84 | }
85 | }
86 | }
87 |
88 | // Write this file to the VFS
89 | try {
90 | return await pyodide.FS.writeFile(file, new Uint8Array(data));
91 | } catch (e) {
92 | if (e.name !== "ErrnoError") throw e;
93 | const errorTextPtr = await pyodide._module._strerror(e.errno);
94 | const errorText = await pyodide._module.UTF8ToString(errorTextPtr);
95 | throw new Error(`Filesystem Error ${e.errno} "${errorText}".`);
96 | }
97 | }).reduce((cur, next) => cur.then(next), Promise.resolve());
98 |
99 | statusText.textContent = `Pyodide environment setup`;
100 | await setupPython(pyodide);
101 |
102 | statusText.remove();
103 | if (statusContainer.children.length == 0) {
104 | statusContainer.parentNode.remove();
105 | }
106 | return pyodide;
107 | })().catch((err) => {
108 | statusText.style.color = "var(--exercise-editor-hl-er, #AD0000)";
109 | statusText.textContent = err.message;
110 | //indicatorContainer.querySelector(".spinner-grow").classList.add("d-none");
111 | throw err;
112 | });
113 |
114 | // Keep track of initial OJS block render
115 | const renderedOjs = {};
116 |
117 | const process = async (context, inputs) => {
118 | const pyodide = await pyodidePromise;
119 | const evaluator = new PyodideEvaluator(pyodide, context);
120 | await evaluator.process(inputs);
121 | return evaluator.container;
122 | }
123 |
124 | return {
125 | pyodidePromise,
126 | renderedOjs,
127 | process,
128 | };
129 | }
130 |
--------------------------------------------------------------------------------
/inst/app/quarto/_extensions/r-wasm/live/templates/webr-editor.ojs:
--------------------------------------------------------------------------------
1 | viewof _webr_editor_{{block_id}} = {
2 | const { WebRExerciseEditor, b64Decode } = window._exercise_ojs_runtime;
3 | const scriptContent = document.querySelector(`script[type=\"webr-{{block_id}}-contents\"]`).textContent;
4 | const block = JSON.parse(b64Decode(scriptContent));
5 |
6 | const options = Object.assign({ id: `webr-{{block_id}}-contents` }, block.attr);
7 | const editor = new WebRExerciseEditor(webROjs.webRPromise, block.code, options);
8 |
9 | return editor.container;
10 | }
11 | _webr_value_{{block_id}} = webROjs.process(_webr_editor_{{block_id}}, {{block_input}});
12 |
--------------------------------------------------------------------------------
/inst/app/quarto/_extensions/r-wasm/live/templates/webr-evaluate.ojs:
--------------------------------------------------------------------------------
1 | _webr_value_{{block_id}} = {
2 | const { highlightR, b64Decode } = window._exercise_ojs_runtime;
3 | const scriptContent = document.querySelector(`script[type=\"webr-{{block_id}}-contents\"]`).textContent;
4 | const block = JSON.parse(b64Decode(scriptContent));
5 |
6 | // Default evaluation configuration
7 | const options = Object.assign({
8 | id: "webr-{{block_id}}-contents",
9 | echo: true,
10 | output: true
11 | }, block.attr);
12 |
13 | // Evaluate the provided R code
14 | const result = webROjs.process({code: block.code, options}, {{block_input}});
15 |
16 | // Early yield while we wait for the first evaluation and render
17 | if (options.output && !("{{block_id}}" in webROjs.renderedOjs)) {
18 | const container = document.createElement("div");
19 | const spinner = document.createElement("div");
20 |
21 | if (options.echo) {
22 | // Show output as highlighted source
23 | const preElem = document.createElement("pre");
24 | container.className = "sourceCode";
25 | preElem.className = "sourceCode r";
26 | preElem.appendChild(highlightR(block.code));
27 | spinner.className = "spinner-grow spinner-grow-sm m-2 position-absolute top-0 end-0";
28 | preElem.appendChild(spinner);
29 | container.appendChild(preElem);
30 | } else {
31 | spinner.className = "spinner-border spinner-border-sm";
32 | container.appendChild(spinner);
33 | }
34 |
35 | yield container;
36 | }
37 |
38 | webROjs.renderedOjs["{{block_id}}"] = true;
39 | yield await result;
40 | }
41 |
--------------------------------------------------------------------------------
/inst/app/quarto/_extensions/r-wasm/live/templates/webr-exercise.ojs:
--------------------------------------------------------------------------------
1 | viewof _webr_editor_{{block_id}} = {
2 | const { WebRExerciseEditor, b64Decode } = window._exercise_ojs_runtime;
3 | const scriptContent = document.querySelector(`script[type=\"webr-{{block_id}}-contents\"]`).textContent;
4 | const block = JSON.parse(b64Decode(scriptContent));
5 |
6 | // Default exercise configuration
7 | const options = Object.assign(
8 | {
9 | id: "webr-{{block_id}}-contents",
10 | envir: `exercise-env-${block.attr.exercise}`,
11 | error: false,
12 | caption: 'Exercise',
13 | },
14 | block.attr
15 | );
16 |
17 | const editor = new WebRExerciseEditor(webROjs.webRPromise, block.code, options);
18 | return editor.container;
19 | }
20 | viewof _webr_value_{{block_id}} = webROjs.process(_webr_editor_{{block_id}}, {{block_input}});
21 | _webr_feedback_{{block_id}} = {
22 | const { WebRGrader } = window._exercise_ojs_runtime;
23 | const emptyFeedback = document.createElement('div');
24 |
25 | const grader = new WebRGrader(_webr_value_{{block_id}}.evaluator);
26 | const feedback = await grader.gradeExercise();
27 | if (!feedback) return emptyFeedback;
28 | return feedback;
29 | }
30 |
--------------------------------------------------------------------------------
/inst/app/quarto/_extensions/r-wasm/live/templates/webr-setup.ojs:
--------------------------------------------------------------------------------
1 | webROjs = {
2 | const { WebR } = window._exercise_ojs_runtime.WebR;
3 | const {
4 | WebREvaluator,
5 | WebREnvironmentManager,
6 | setupR,
7 | b64Decode,
8 | collapsePath
9 | } = window._exercise_ojs_runtime;
10 |
11 | const statusContainer = document.getElementById("exercise-loading-status");
12 | const indicatorContainer = document.getElementById("exercise-loading-indicator");
13 | indicatorContainer.classList.remove("d-none");
14 |
15 | let statusText = document.createElement("div")
16 | statusText.classList = "exercise-loading-details";
17 | statusText = statusContainer.appendChild(statusText);
18 | statusText.textContent = `Initialise`;
19 |
20 | // Hoist indicator out from final slide when running under reveal
21 | const revealStatus = document.querySelector(".reveal .exercise-loading-indicator");
22 | if (revealStatus) {
23 | revealStatus.remove();
24 | document.querySelector(".reveal > .slides").appendChild(revealStatus);
25 | }
26 |
27 | // Make any reveal slides with live cells scrollable
28 | document.querySelectorAll(".reveal .exercise-cell").forEach((el) => {
29 | el.closest('section.slide').classList.add("scrollable");
30 | })
31 |
32 | // webR supplemental data and options
33 | const dataContent = document.querySelector(`script[type=\"webr-data\"]`).textContent;
34 | const data = JSON.parse(b64Decode(dataContent));
35 |
36 | // Grab list of resources to be downloaded
37 | const filesContent = document.querySelector(`script[type=\"vfs-file\"]`).textContent;
38 | const files = JSON.parse(b64Decode(filesContent));
39 |
40 | // Initialise webR and setup for R code evaluation
41 | let webRPromise = (async (webR) => {
42 | statusText.textContent = `Downloading webR`;
43 | await webR.init();
44 |
45 | // Install provided list of packages
46 | // Ensure webR default repo is included
47 | data.packages.repos.push("https://repo.r-wasm.org")
48 | await data.packages.pkgs.map((pkg) => () => {
49 | statusText.textContent = `Downloading package: ${pkg}`;
50 | return webR.evalRVoid(`
51 | webr::install(pkg, repos = repos)
52 | library(pkg, character.only = TRUE)
53 | `, { env: {
54 | pkg: pkg,
55 | repos: data.packages.repos,
56 | }});
57 | }).reduce((cur, next) => cur.then(next), Promise.resolve());
58 |
59 | // Download and install resources
60 | await files.map((file) => async () => {
61 | const name = file.substring(file.lastIndexOf('/') + 1);
62 | statusText.textContent = `Downloading resource: ${name}`;
63 | const response = await fetch(file);
64 | if (!response.ok) {
65 | throw new Error(`Can't download \`${file}\`. Error ${response.status}: "${response.statusText}".`);
66 | }
67 | const data = await response.arrayBuffer();
68 |
69 | // Store URLs in the cwd without any subdirectory structure
70 | if (file.includes("://")) {
71 | file = name;
72 | }
73 |
74 | // Collapse higher directory structure
75 | file = collapsePath(file);
76 |
77 | // Create directory tree, ignoring "directory exists" VFS errors
78 | const parts = file.split('/').slice(0, -1);
79 | let path = '';
80 | while (parts.length > 0) {
81 | path += parts.shift() + '/';
82 | try {
83 | await webR.FS.mkdir(path);
84 | } catch (e) {
85 | if (!e.message.includes("FS error")) {
86 | throw e;
87 | }
88 | }
89 | }
90 |
91 | // Write this file to the VFS
92 | return await webR.FS.writeFile(file, new Uint8Array(data));
93 | }).reduce((cur, next) => cur.then(next), Promise.resolve());
94 |
95 | statusText.textContent = `Installing webR shims`;
96 | await webR.evalRVoid(`webr::shim_install()`);
97 |
98 | statusText.textContent = `WebR environment setup`;
99 | await setupR(webR, data);
100 |
101 | statusText.remove();
102 | if (statusContainer.children.length == 0) {
103 | statusContainer.parentNode.remove();
104 | }
105 | return webR;
106 | })(new WebR(data.options));
107 |
108 | // Keep track of initial OJS block render
109 | const renderedOjs = {};
110 |
111 | const process = async (context, inputs) => {
112 | const webR = await webRPromise;
113 | const evaluator = new WebREvaluator(webR, context)
114 | await evaluator.process(inputs);
115 | return evaluator.container;
116 | }
117 |
118 | return {
119 | process,
120 | webRPromise,
121 | renderedOjs,
122 | };
123 | }
124 |
--------------------------------------------------------------------------------
/inst/app/quarto/_extensions/r-wasm/live/templates/webr-widget.ojs:
--------------------------------------------------------------------------------
1 | {
2 | // Wait for output to be written to the DOM, then trigger widget rendering
3 | await _webr_value_{{block_id}};
4 | if (window.HTMLWidgets) {
5 | window.HTMLWidgets.staticRender();
6 | }
7 | if (window.PagedTableDoc) {
8 | window.PagedTableDoc.initAll();
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/inst/app/quarto/quarto-live-template.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | format:
3 | live-html:
4 | page-layout: full
5 | resources:
6 | - data.rds
7 | engine: knitr
8 | ---
9 |
10 | {{< include ./_extensions/r-wasm/live/_knitr.qmd >}}
11 |
12 | ```{webr}
13 | #| autorun: true
14 | #| echo: false
15 | $$$data_name$$$ <- readRDS("data.rds")
16 | ```
17 |
18 | ```{webr}
19 | #| caption: "R Console"
20 | #| autorun: true
21 | #| exercise: ex_2
22 | $$$code$$$
23 | ```
24 |
25 |
70 |
71 |
83 |
--------------------------------------------------------------------------------
/inst/app/www/helpers.js:
--------------------------------------------------------------------------------
1 | const chat = document.getElementById('chat');
2 |
3 | const observer = new MutationObserver((mutations) => {
4 | mutations.forEach((mutation) => {
5 | // Only listen to changes in `.message-content` children
6 | const target = mutation.target;
7 |
8 | if (!target?.classList.contains('message-content')) {
9 | return;
10 | }
11 | // Get all the code blocks and append a button below each
12 | // That sends the code to the server as a Shiny input value
13 | // TODO: automatically determine when the code block is finished
14 | // and send the code to the server when that happens
15 | const els = target.querySelectorAll("pre code");
16 | els.forEach((el) => {
17 | if (!hasRunButton(el)) {
18 | addRunButton(el);
19 | }
20 | });
21 |
22 | });
23 | });
24 |
25 | function addRunButton(code_el) {
26 | const button = document.createElement("button");
27 | button.innerHTML = "Run this code ->";
28 | button.classList.add("btn", "btn-sm", "btn-default", "run-code");
29 | function send_editor_code() {
30 | Shiny.setInputValue("editor_code", code_el.innerText);
31 | };
32 | button.onclick = send_editor_code;
33 | button.style.float = "right";
34 | // Append the button as a child of the pre block
35 | code_el.parentNode.appendChild(button);
36 | }
37 |
38 | function hasRunButton(code_el) {
39 | return code_el.parentNode.querySelector(":scope > .run-code") !== null;
40 | }
41 |
42 |
43 | observer.observe(chat, { childList: true, subtree: true });
44 |
45 |
46 |
47 | window.addEventListener("message", receiveMessage, false);
48 |
49 | function receiveMessage(event) {
50 | Shiny.setInputValue("editor_results", JSON.stringify(event.data));
51 | }
--------------------------------------------------------------------------------
/man/assist.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/assist.R
3 | \name{assist}
4 | \alias{assist}
5 | \title{Launch the AI-powered EDA assistant}
6 | \usage{
7 | assist(data, chat = NULL)
8 | }
9 | \arguments{
10 | \item{data}{A data frame.}
11 |
12 | \item{chat}{A \link[ellmer:Chat]{ellmer::Chat} instance (e.g., \code{ellmer::chat_ollama()}).
13 | Be aware that any \code{system_prompt} will be overwritten.}
14 | }
15 | \description{
16 | Launch the AI-powered EDA assistant
17 | }
18 | \examples{
19 |
20 | data(diamonds, package = "ggplot2")
21 | assist(diamonds)
22 | }
23 |
--------------------------------------------------------------------------------