├── .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 | Screenshot 2024-10-10 at 12 57 51 PM 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 | Screenshot 2024-10-10 at 12 58 34 PM 45 | 46 |
47 | 48 | When clicked, the code is run in a sidebar, and results displayed below the interactive code editor. 49 | 50 | Screenshot 2024-10-10 at 12 59 07 PM 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 | Screenshot 2024-10-10 at 12 59 43 PM 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 | --------------------------------------------------------------------------------