├── .github └── workflows │ └── publish-website.yml ├── .gitignore ├── README.md ├── _extensions ├── coatless-quarto │ └── embedio │ │ ├── _extension.yml │ │ └── embedio.lua ├── gadenbuie │ └── countdown │ │ ├── _extension.yml │ │ ├── assets │ │ ├── countdown.css │ │ ├── countdown.js │ │ └── smb_stage_clear.mp3 │ │ ├── config.lua │ │ └── countdown.lua └── r-wasm │ ├── drop │ ├── _extension.yml │ ├── drop-runtime.css │ └── drop-runtime.js │ └── 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.yml ├── index.qmd ├── slides └── lecture-01.qmd └── tutorials └── demo-lab.qmd /.github/workflows/publish-website.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main, master] 4 | release: 5 | types: [published] 6 | workflow_dispatch: {} 7 | 8 | name: generate-website 9 | 10 | jobs: 11 | demo-website: 12 | runs-on: ubuntu-latest 13 | # Only restrict concurrency for non-PR jobs 14 | concurrency: 15 | group: quarto-publish-${{ github.event_name != 'pull_request' || github.run_id }} 16 | permissions: 17 | contents: read 18 | pages: write 19 | id-token: write 20 | steps: 21 | - name: "Check out repository" 22 | uses: actions/checkout@v4 23 | 24 | # To render using knitr, we need a few more setup steps... 25 | # If we didn't want the examples to use `engine: knitr`, we could 26 | # skip a few of the setup steps. 27 | - name: "Setup pandoc" 28 | uses: r-lib/actions/setup-pandoc@v2 29 | 30 | - name: "Setup R" 31 | uses: r-lib/actions/setup-r@v2 32 | 33 | - name: "Setup R dependencies for Quarto's knitr engine" 34 | uses: r-lib/actions/setup-r-dependencies@v2 35 | with: 36 | packages: 37 | any::knitr 38 | any::rmarkdown 39 | any::downlit 40 | any::xml2 41 | 42 | # Back to our regularly scheduled Quarto output 43 | - name: "Set up Quarto" 44 | uses: quarto-dev/quarto-actions/setup@v2 45 | with: 46 | version: "pre-release" 47 | 48 | # Generate the documentation website 49 | - name: Render Documentation website 50 | uses: quarto-dev/quarto-actions/render@v2 51 | 52 | # Publish the docs directory onto gh-pages 53 | 54 | # Upload a tar file that will work with GitHub Pages 55 | # Make sure to set a retention day to avoid running into a cap 56 | # This artifact shouldn't be required after deployment onto pages was a success. 57 | - name: Upload Pages artifact 58 | uses: actions/upload-pages-artifact@v2 59 | with: 60 | retention-days: 1 61 | 62 | # Use an Action deploy to push the artifact onto GitHub Pages 63 | # This requires the `Action` tab being structured to allow for deployment 64 | # instead of using `docs/` or the `gh-pages` branch of the repository 65 | - name: Deploy to GitHub Pages 66 | id: deployment 67 | uses: actions/deploy-pages@v2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.quarto/ 2 | _site/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Next Generation of Data Science Education with WebAssembly 2 | 3 | This repository contains the code powering **[The Next Generation of Data Science Education with WebAssembly](https://tutorials.thecoatlessprofessor.com/next-gen-data-science-education-wasm/)** Quarto website demonstration. 4 | 5 | ## Installation 6 | 7 | To create your own version of the demonstration, you need to install the following software: 8 | 9 | - RStudio IDE, VS Code, Positron, or another text editor 10 | - For VS Code or Positron, please install the [Quarto plugin](https://open-vsx.org/extension/quarto/quarto). 11 | - [Quarto](https://quarto.org) v1.4.0 or later 12 | - Quarto Extensions 13 | - [`quarto-live`](https://r-wasm.github.io/quarto-live/) 14 | - [`quarto-drop`](https://github.com/r-wasm/quarto-drop) 15 | - [`quarto-countdown`](https://github.com/gadenbuie/countdown/tree/main/quarto) 16 | - [`quarto-embedio`](https://github.com/coatless-quarto/embedio) 17 | 18 | You can install the Quarto Extensions by typing the following commands in your terminal: 19 | 20 | ```bash 21 | quarto add r-wasm/quarto-live 22 | quarto add r-wasm/quarto-drop 23 | quarto add gadenbuie/countdown/quarto 24 | quarto add coatless-quarto/embedio 25 | ``` 26 | 27 | ## GitHub Pages 28 | 29 | The demonstration is hosted on GitHub Pages using a GitHub Action ([`publish-website.yml`](.github/workflows/publish-website.yml)). The action is triggered on every push to the `main` branch. The action builds the Quarto website and pushes the output to the `gh-pages`. 30 | 31 | Please make sure to enable GitHub Pages in the repository settings, select the GitHub Actions as the source, 32 | and check the **Enforce HTTPS** option. 33 | 34 | Setup GitHub Pages for Deploying `quarto-live` 35 | -------------------------------------------------------------------------------- /_extensions/coatless-quarto/embedio/_extension.yml: -------------------------------------------------------------------------------- 1 | name: embedio 2 | title: Embedded different kinds of files 3 | author: James Joseph Balamuta 4 | version: 0.0.2-dev.1 5 | quarto-required: ">=1.4.549" 6 | contributes: 7 | shortcodes: 8 | - embedio.lua -------------------------------------------------------------------------------- /_extensions/coatless-quarto/embedio/embedio.lua: -------------------------------------------------------------------------------- 1 | -- Check if variable missing or an empty string 2 | local function isVariableEmpty(s) 3 | return s == nil or s == '' 4 | end 5 | 6 | -- Check if variable is present 7 | local function isVariablePopulated(s) 8 | return not isVariableEmpty(s) 9 | end 10 | 11 | -- Check whether an argument is present in kwargs 12 | -- If it is, return the value 13 | local function tryOption(options, key) 14 | 15 | -- Protect against an empty options 16 | if not (options and options[key]) then 17 | return nil 18 | end 19 | 20 | -- Retrieve the option 21 | local option_value = pandoc.utils.stringify(options[key]) 22 | -- Verify the option's value exists, return value otherwise nil. 23 | if isVariablePopulated(option_value) then 24 | return option_value 25 | else 26 | return nil 27 | end 28 | end 29 | 30 | -- Retrieve the option value or use the default value 31 | local function getOption(options, key, default) 32 | return tryOption(options, key) or default 33 | end 34 | 35 | -- Validate input file path is not empty 36 | local function checkFile(input) 37 | if input then 38 | return true 39 | else 40 | quarto.log.error("Error: file path is required for the embedio shortcode.") 41 | assert(false) 42 | end 43 | end 44 | 45 | -- Avoid duplicate class definitions 46 | local initializedSlideCSS = false 47 | 48 | -- Load CSS into header once 49 | local function ensureSlideCSSPresent() 50 | if initializedSlideCSS then return end 51 | initializedSlideCSS = true 52 | 53 | -- Default CSS class 54 | local slideCSS = [[ 55 | 61 | ]] 62 | 63 | -- Inject into the header 64 | quarto.doc.include_text("in-header", slideCSS) 65 | end 66 | 67 | -- Define a function to generate HTML code for an iframe element 68 | local function iframe_helper(file_name, height, full_screen_link, template, type) 69 | -- Check if the file exists 70 | checkFile(file_name) 71 | 72 | -- Define a template for displaying a full-screen link 73 | local template_full_screen = [[ 74 |

View %s in full screen

75 | ]] 76 | 77 | -- Combine the template with file name and height to generate HTML code 78 | local combined_str = string.format( 79 | [[%s %s]], 80 | -- Include full-screen link if specified 81 | (full_screen_link == "true" and string.format(template_full_screen, file_name, type) or ""), 82 | -- Insert the iframe template with file name and height 83 | string.format(template, file_name, height) 84 | ) 85 | 86 | -- Return the combined HTML as a pandoc RawBlock 87 | return pandoc.RawBlock('html', combined_str) 88 | end 89 | 90 | -- Define the html() function for embedding HTML files 91 | local function html(args, kwargs, meta, raw_args) 92 | -- Check if the output format is HTML 93 | if not quarto.doc.is_format("html") then return end 94 | 95 | -- Get the HTML file name, height, and full-screen link option 96 | local file_name = pandoc.utils.stringify(args[1] or kwargs["file"]) 97 | local height = getOption(kwargs, "height", "475px") 98 | local full_screen_link = getOption(kwargs, "full-screen-link", "true") 99 | 100 | -- Define the template for embedding HTML files 101 | local template_html = [[ 102 |
103 | 104 |
105 | ]] 106 | 107 | -- Call the iframe_helper() function with the HTML template 108 | return iframe_helper(file_name, height, full_screen_link, template_html, "webpage") 109 | end 110 | 111 | -- Define the revealjs() function for embedding Reveal.js slides 112 | local function revealjs(args, kwargs, meta, raw_args) 113 | -- Check if the output format is HTML 114 | if not quarto.doc.is_format("html") then return end 115 | 116 | -- Ensure that the Reveal.js CSS is present 117 | ensureSlideCSSPresent() 118 | 119 | -- Get the slide file name, height, and full-screen link option 120 | local file_name = pandoc.utils.stringify(args[1] or kwargs["file"]) 121 | local height = getOption(kwargs, "height", "475px") 122 | local full_screen_link = getOption(kwargs, "full-screen-link", "true") 123 | 124 | -- Define the template for embedding Reveal.js slides 125 | local template_revealjs = [[ 126 |
127 | 128 |
129 | ]] 130 | 131 | -- Call the iframe_helper() function with the Reveal.js template 132 | return iframe_helper(file_name, height, full_screen_link, template_revealjs, "slides") 133 | end 134 | 135 | local function audio(args, kwargs, meta) 136 | 137 | if not quarto.doc.is_format("html") then return end 138 | 139 | -- Start of HTML tag 140 | local htmlTable = {"
') 154 | end 155 | 156 | -- Add download link if provided 157 | if download_link == "true" then 158 | table.insert(htmlTable, '

Download audio file


') 159 | end 160 | 161 | -- Start the audio tag 162 | table.insert(htmlTable, "") 182 | 183 | -- Add caption if provided 184 | if caption then 185 | table.insert(htmlTable, '
' .. caption .. '
') 186 | end 187 | 188 | -- Add closing figure tag 189 | table.insert(htmlTable, "
") 190 | 191 | return pandoc.RawBlock('html', table.concat(htmlTable)) 192 | end 193 | 194 | 195 | local function pdf(args, kwargs, meta) 196 | 197 | if not quarto.doc.is_format("html") then return end 198 | 199 | -- Supported options for now 200 | local pdf_file_name = pandoc.utils.stringify(args[1] or kwargs["file"]) 201 | checkFile(pdf_file_name) 202 | 203 | local height = getOption(kwargs, "height", "600px") 204 | local width = getOption(kwargs, "width", "100%") 205 | local download_link = getOption(kwargs, "download-link", "true") 206 | 207 | -- HTML block 208 | local template_pdf = [[ 209 | 210 |

Unable to display PDF file. Download instead.

211 |
212 | ]] 213 | 214 | local template_pdf_download_link = [[ 215 |

Download PDF File

216 | ]] 217 | 218 | -- Obtain the combined template 219 | local combined_str = string.format( 220 | [[%s %s]], 221 | (download_link == "true" and string.format(template_pdf_download_link, pdf_file_name) or ""), 222 | string.format(template_pdf, pdf_file_name, width, height, pdf_file_name) 223 | ) 224 | 225 | -- Return as HTML block 226 | return pandoc.RawBlock('html', combined_str) 227 | end 228 | 229 | return { 230 | ['audio'] = audio, 231 | ['pdf'] = pdf, 232 | ['revealjs'] = revealjs, 233 | ['html'] = html 234 | } 235 | -------------------------------------------------------------------------------- /_extensions/gadenbuie/countdown/_extension.yml: -------------------------------------------------------------------------------- 1 | title: countdown 2 | author: Garrick Aden-Buie and James Joseph Balamuta 3 | version: 0.0.0-dev.1 4 | quarto-required: ">=1.4.0" 5 | contributes: 6 | shortcodes: 7 | - countdown.lua 8 | 9 | -------------------------------------------------------------------------------- /_extensions/gadenbuie/countdown/assets/countdown.css: -------------------------------------------------------------------------------- 1 | .countdown { 2 | --_margin: 0.6em; 3 | --_running-color: var(--countdown-color-running-text, rgba(0, 0, 0, 0.8)); 4 | --_running-background: var(--countdown-color-running-background, #43AC6A); 5 | --_running-border-color: var(--countdown-color-running-border, rgba(0, 0, 0, 0.1)); 6 | --_finished-color: var(--countdown-color-finished-text, rgba(0, 0, 0, 0.7)); 7 | --_finished-background: var(--countdown-color-finished-background, #F04124); 8 | --_finished-border-color: var(--countdown-color-finished-border, rgba(0, 0, 0, 0.1)); 9 | 10 | position: absolute; 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | cursor: pointer; 15 | background: var(--countdown-color-background, inherit); 16 | font-size: var(--countdown-font-size, 3rem); 17 | line-height: var(--countdown-line-height, 1); 18 | border-color: var(--countdown-color-border, #ddd); 19 | border-width: var(--countdown-border-width, 0.1875rem); 20 | border-style: solid; 21 | border-radius: var(--countdown-border-radius, 0.9rem); 22 | box-shadow: var(--countdown-box-shadow, 0px 4px 10px 0px rgba(50, 50, 50, 0.4)); 23 | margin: var(--countdown-margin, var(--_margin, 0.6em)); 24 | padding: var(--countdown-padding, 0.625rem 0.9rem); 25 | text-align: center; 26 | z-index: 10; 27 | -webkit-user-select: none; 28 | -moz-user-select: none; 29 | -ms-user-select: none; 30 | user-select: none; 31 | } 32 | 33 | .countdown.inline { 34 | position: relative; 35 | width: max-content; 36 | max-width: 100%; 37 | } 38 | 39 | .countdown .countdown-time { 40 | background: none; 41 | font-size: 100%; 42 | padding: 0; 43 | color: currentColor; 44 | } 45 | 46 | .countdown-digits { 47 | color: var(--countdown-color-text); 48 | } 49 | 50 | .countdown.running { 51 | border-color: var(--_running-border-color); 52 | background-color: var(--_running-background); 53 | } 54 | 55 | .countdown.running .countdown-digits { 56 | color: var(--_running-color); 57 | } 58 | 59 | .countdown.finished { 60 | border-color: var(--_finished-border-color); 61 | background-color: var(--_finished-background); 62 | } 63 | 64 | .countdown.finished .countdown-digits { 65 | color: var(--_finished-color); 66 | } 67 | 68 | .countdown.running.warning { 69 | border-color: var(--countdown-color-warning-border, rgba(0, 0, 0, 0.1)); 70 | background-color: var(--countdown-color-warning-background, #E6C229); 71 | } 72 | 73 | .countdown.running.warning .countdown-digits { 74 | color: var(--countdown-color-warning-text, rgba(0, 0, 0, 0.7)); 75 | } 76 | 77 | .countdown.running.blink-colon .countdown-digits.colon { 78 | opacity: 0.1; 79 | } 80 | 81 | /* ------ Controls ------ */ 82 | .countdown:not(.running) .countdown-controls, 83 | .countdown.no-controls .countdown-controls { 84 | display: none; 85 | } 86 | 87 | .countdown-controls { 88 | position: absolute; 89 | top: -0.5rem; 90 | right: -0.5rem; 91 | left: -0.5rem; 92 | display: flex; 93 | justify-content: space-between; 94 | margin: 0; 95 | padding: 0; 96 | } 97 | 98 | .countdown-controls>button { 99 | position: relative; 100 | font-size: 1.5rem; 101 | width: 1rem; 102 | height: 1rem; 103 | display: flex; 104 | flex-direction: column; 105 | align-items: center; 106 | justify-content: center; 107 | font-family: monospace; 108 | padding: 10px; 109 | margin: 0; 110 | background: inherit; 111 | border: 2px solid; 112 | border-radius: 100%; 113 | transition: 50ms transform ease-in-out, 150ms opacity ease-in; 114 | box-shadow: 0px 2px 5px 0px rgba(50, 50, 50, 0.4); 115 | -webkit-box-shadow: 0px 2px 5px 0px rgba(50, 50, 50, 0.4); 116 | --_button-bump: 0; 117 | opacity: var(--_opacity, 0); 118 | transform: translate(0, var(--_button-bump)); 119 | } 120 | 121 | /* increase hit area of the +/- buttons */ 122 | .countdown .countdown-controls > button::after { 123 | content: ""; 124 | height: 200%; 125 | width: 200%; 126 | position: absolute; 127 | border-radius: 50%; 128 | } 129 | 130 | .countdown .countdown-controls>button:last-child { 131 | color: var(--_running-color); 132 | background-color: var(--_running-background); 133 | border-color: var(--_running-border-color); 134 | } 135 | 136 | .countdown .countdown-controls>button:first-child { 137 | color: var(--_finished-color); 138 | background-color: var(--_finished-background); 139 | border-color: var(--_finished-border-color); 140 | } 141 | 142 | .countdown.running:hover, .countdown.running:focus-within { 143 | --_opacity: 1; 144 | } 145 | 146 | .countdown.running:hover .countdown-controls>button, 147 | .countdown.running:focus-within .countdown-controls>button { 148 | --_button-bump: -3px; 149 | } 150 | 151 | .countdown.running:hover .countdown-controls>button:active, 152 | .countdown.running:focus-within .countdown-controls>button:active { 153 | --_button-bump: 0; 154 | } 155 | 156 | /* ---- Quarto Reveal.js ---- */ 157 | .reveal .countdown { 158 | --_margin: 0; 159 | } 160 | 161 | /* ----- Fullscreen ----- */ 162 | .countdown.countdown-fullscreen { 163 | z-index: 0; 164 | } 165 | 166 | .countdown-fullscreen.running .countdown-controls { 167 | top: 1rem; 168 | left: 0; 169 | right: 0; 170 | justify-content: center; 171 | } 172 | 173 | .countdown-fullscreen.running .countdown-controls>button+button { 174 | margin-left: 1rem; 175 | } 176 | -------------------------------------------------------------------------------- /_extensions/gadenbuie/countdown/assets/countdown.js: -------------------------------------------------------------------------------- 1 | /* globals Shiny,Audio */ 2 | class CountdownTimer { 3 | constructor (el, opts) { 4 | if (typeof el === 'string' || el instanceof String) { 5 | el = document.querySelector(el) 6 | } 7 | 8 | if (el.counter) { 9 | return el.counter 10 | } 11 | 12 | const minutes = parseInt(el.querySelector('.minutes').innerText || '0') 13 | const seconds = parseInt(el.querySelector('.seconds').innerText || '0') 14 | const duration = minutes * 60 + seconds 15 | 16 | function attrIsTrue (x) { 17 | if (typeof x === 'undefined') return false 18 | if (x === true) return true 19 | return !!(x === 'true' || x === '' || x === '1') 20 | } 21 | 22 | this.element = el 23 | this.duration = duration 24 | this.end = null 25 | this.is_running = false 26 | this.warn_when = parseInt(el.dataset.warnWhen) || -1 27 | this.update_every = parseInt(el.dataset.updateEvery) || 1 28 | this.play_sound = attrIsTrue(el.dataset.playSound) || el.dataset.playSound 29 | this.blink_colon = attrIsTrue(el.dataset.blinkColon) 30 | this.startImmediately = attrIsTrue(el.dataset.startImmediately) 31 | this.timeout = null 32 | this.display = { minutes, seconds } 33 | 34 | if (opts.src_location) { 35 | this.src_location = opts.src_location 36 | } 37 | 38 | this.addEventListeners() 39 | } 40 | 41 | addEventListeners () { 42 | const self = this 43 | 44 | if (this.startImmediately) { 45 | if (window.remark && window.slideshow) { 46 | // Remark (xaringan) support 47 | const isOnVisibleSlide = () => { 48 | return document.querySelector('.remark-visible').contains(self.element) 49 | } 50 | if (isOnVisibleSlide()) { 51 | self.start() 52 | } else { 53 | let started_once = 0 54 | window.slideshow.on('afterShowSlide', function () { 55 | if (started_once > 0) return 56 | if (isOnVisibleSlide()) { 57 | self.start() 58 | started_once = 1 59 | } 60 | }) 61 | } 62 | } else if (window.Reveal) { 63 | // Revealjs (quarto) support 64 | const isOnVisibleSlide = () => { 65 | const currentSlide = document.querySelector('.reveal .slide.present') 66 | return currentSlide ? currentSlide.contains(self.element) : false 67 | } 68 | if (isOnVisibleSlide()) { 69 | self.start() 70 | } else { 71 | const revealStartTimer = () => { 72 | if (isOnVisibleSlide()) { 73 | self.start() 74 | window.Reveal.off('slidechanged', revealStartTimer) 75 | } 76 | } 77 | window.Reveal.on('slidechanged', revealStartTimer) 78 | } 79 | } else if (window.IntersectionObserver) { 80 | // All other situations use IntersectionObserver 81 | const onVisible = (element, callback) => { 82 | new window.IntersectionObserver((entries, observer) => { 83 | entries.forEach(entry => { 84 | if (entry.intersectionRatio > 0) { 85 | callback(element) 86 | observer.disconnect() 87 | } 88 | }) 89 | }).observe(element) 90 | } 91 | onVisible(this.element, el => el.countdown.start()) 92 | } else { 93 | // or just start the timer as soon as it's initialized 94 | this.start() 95 | } 96 | } 97 | 98 | function haltEvent (ev) { 99 | ev.preventDefault() 100 | ev.stopPropagation() 101 | } 102 | function isSpaceOrEnter (ev) { 103 | return ev.code === 'Space' || ev.code === 'Enter' 104 | } 105 | function isArrowUpOrDown (ev) { 106 | return ev.code === 'ArrowUp' || ev.code === 'ArrowDown' 107 | } 108 | 109 | ;['click', 'touchend'].forEach(function (eventType) { 110 | self.element.addEventListener(eventType, function (ev) { 111 | haltEvent(ev) 112 | self.is_running ? self.stop({manual: true}) : self.start() 113 | }) 114 | }) 115 | this.element.addEventListener('keydown', function (ev) { 116 | if (ev.code === "Escape") { 117 | self.reset() 118 | haltEvent(ev) 119 | } 120 | if (!isSpaceOrEnter(ev) && !isArrowUpOrDown(ev)) return 121 | haltEvent(ev) 122 | if (isSpaceOrEnter(ev)) { 123 | self.is_running ? self.stop({manual: true}) : self.start() 124 | return 125 | } 126 | 127 | if (!self.is_running) return 128 | 129 | if (ev.code === 'ArrowUp') { 130 | self.bumpUp() 131 | } else if (ev.code === 'ArrowDown') { 132 | self.bumpDown() 133 | } 134 | }) 135 | this.element.addEventListener('dblclick', function (ev) { 136 | haltEvent(ev) 137 | if (self.is_running) self.reset() 138 | }) 139 | this.element.addEventListener('touchmove', haltEvent) 140 | 141 | const btnBumpDown = this.element.querySelector('.countdown-bump-down') 142 | ;['click', 'touchend'].forEach(function (eventType) { 143 | btnBumpDown.addEventListener(eventType, function (ev) { 144 | haltEvent(ev) 145 | if (self.is_running) self.bumpDown() 146 | }) 147 | }) 148 | btnBumpDown.addEventListener('keydown', function (ev) { 149 | if (!isSpaceOrEnter(ev) || !self.is_running) return 150 | haltEvent(ev) 151 | self.bumpDown() 152 | }) 153 | 154 | const btnBumpUp = this.element.querySelector('.countdown-bump-up') 155 | ;['click', 'touchend'].forEach(function (eventType) { 156 | btnBumpUp.addEventListener(eventType, function (ev) { 157 | haltEvent(ev) 158 | if (self.is_running) self.bumpUp() 159 | }) 160 | }) 161 | btnBumpUp.addEventListener('keydown', function (ev) { 162 | if (!isSpaceOrEnter(ev) || !self.is_running) return 163 | haltEvent(ev) 164 | self.bumpUp() 165 | }) 166 | this.element.querySelector('.countdown-controls').addEventListener('dblclick', function (ev) { 167 | haltEvent(ev) 168 | }) 169 | } 170 | 171 | remainingTime () { 172 | const remaining = this.is_running 173 | ? (this.end - Date.now()) / 1000 174 | : this.remaining || this.duration 175 | 176 | let minutes = Math.floor(remaining / 60) 177 | let seconds = Math.ceil(remaining - minutes * 60) 178 | 179 | if (seconds > 59) { 180 | minutes = minutes + 1 181 | seconds = seconds - 60 182 | } 183 | 184 | return { remaining, minutes, seconds } 185 | } 186 | 187 | start () { 188 | if (this.is_running) return 189 | 190 | this.is_running = true 191 | 192 | if (this.remaining) { 193 | // Having a static remaining time indicates timer was paused 194 | this.end = Date.now() + this.remaining * 1000 195 | this.remaining = null 196 | } else { 197 | this.end = Date.now() + this.duration * 1000 198 | } 199 | 200 | this.emitStateEvent('start') 201 | 202 | this.element.classList.remove('finished') 203 | this.element.classList.add('running') 204 | this.update(true) 205 | this.tick() 206 | } 207 | 208 | tick (run_again) { 209 | if (typeof run_again === 'undefined') { 210 | run_again = true 211 | } 212 | 213 | if (!this.is_running) return 214 | 215 | const { seconds: secondsWas } = this.display 216 | this.update() 217 | 218 | if (run_again) { 219 | const delay = (this.end - Date.now() > 10000) ? 1000 : 250 220 | this.blinkColon(secondsWas) 221 | this.timeout = setTimeout(this.tick.bind(this), delay) 222 | } 223 | } 224 | 225 | blinkColon (secondsWas) { 226 | // don't blink unless option is set 227 | if (!this.blink_colon) return 228 | // warn_when always updates the seconds 229 | if (this.warn_when > 0 && Date.now() + this.warn_when > this.end) { 230 | this.element.classList.remove('blink-colon') 231 | return 232 | } 233 | const { seconds: secondsIs } = this.display 234 | if (secondsIs > 10 || secondsWas !== secondsIs) { 235 | this.element.classList.toggle('blink-colon') 236 | } 237 | } 238 | 239 | update (force) { 240 | if (typeof force === 'undefined') { 241 | force = false 242 | } 243 | 244 | const { remaining, minutes, seconds } = this.remainingTime() 245 | 246 | const setRemainingTime = (selector, time) => { 247 | const timeContainer = this.element.querySelector(selector) 248 | if (!timeContainer) return 249 | time = Math.max(time, 0) 250 | timeContainer.innerText = String(time).padStart(2, 0) 251 | } 252 | 253 | if (this.is_running && remaining < 0.25) { 254 | this.stop() 255 | setRemainingTime('.minutes', 0) 256 | setRemainingTime('.seconds', 0) 257 | this.playSound() 258 | return 259 | } 260 | 261 | const should_update = force || 262 | Math.round(remaining) < this.warn_when || 263 | Math.round(remaining) % this.update_every === 0 264 | 265 | if (should_update) { 266 | const is_warning = remaining <= this.warn_when 267 | if (is_warning && !this.element.classList.contains('warning')) { 268 | this.emitStateEvent('warning') 269 | } 270 | this.element.classList.toggle('warning', is_warning) 271 | this.display = { minutes, seconds } 272 | setRemainingTime('.minutes', minutes) 273 | setRemainingTime('.seconds', seconds) 274 | } 275 | } 276 | 277 | stop ({manual = false} = {}) { 278 | const { remaining } = this.remainingTime() 279 | if (remaining > 1) { 280 | this.remaining = remaining 281 | } 282 | this.element.classList.remove('running') 283 | this.element.classList.remove('warning') 284 | this.element.classList.remove('blink-colon') 285 | this.element.classList.add('finished') 286 | this.is_running = false 287 | this.end = null 288 | this.emitStateEvent(manual ? 'stop' : 'finished') 289 | this.timeout = clearTimeout(this.timeout) 290 | } 291 | 292 | reset () { 293 | this.stop({manual: true}) 294 | this.remaining = null 295 | this.update(true) 296 | 297 | this.element.classList.remove('finished') 298 | this.element.classList.remove('warning') 299 | this.emitEvents = true 300 | this.emitStateEvent('reset') 301 | } 302 | 303 | setValues (opts) { 304 | if (typeof opts.warn_when !== 'undefined') { 305 | this.warn_when = opts.warn_when 306 | } 307 | if (typeof opts.update_every !== 'undefined') { 308 | this.update_every = opts.update_every 309 | } 310 | if (typeof opts.blink_colon !== 'undefined') { 311 | this.blink_colon = opts.blink_colon 312 | if (!opts.blink_colon) { 313 | this.element.classList.remove('blink-colon') 314 | } 315 | } 316 | if (typeof opts.play_sound !== 'undefined') { 317 | this.play_sound = opts.play_sound 318 | } 319 | if (typeof opts.duration !== 'undefined') { 320 | this.duration = opts.duration 321 | if (this.is_running) { 322 | this.reset() 323 | this.start() 324 | } 325 | } 326 | this.emitStateEvent('update') 327 | this.update(true) 328 | } 329 | 330 | bumpTimer (val, round) { 331 | round = typeof round === 'boolean' ? round : true 332 | const { remaining } = this.remainingTime() 333 | let newRemaining = remaining + val 334 | if (newRemaining <= 0) { 335 | this.setRemaining(0) 336 | this.stop() 337 | return 338 | } 339 | if (round && newRemaining > 10) { 340 | newRemaining = Math.round(newRemaining / 5) * 5 341 | } 342 | this.setRemaining(newRemaining) 343 | this.emitStateEvent(val > 0 ? 'bumpUp' : 'bumpDown') 344 | this.update(true) 345 | } 346 | 347 | bumpUp (val) { 348 | if (!this.is_running) { 349 | console.error('timer is not running') 350 | return 351 | } 352 | this.bumpTimer( 353 | val || this.bumpIncrementValue(), 354 | typeof val === 'undefined' 355 | ) 356 | } 357 | 358 | bumpDown (val) { 359 | if (!this.is_running) { 360 | console.error('timer is not running') 361 | return 362 | } 363 | this.bumpTimer( 364 | val || -1 * this.bumpIncrementValue(), 365 | typeof val === 'undefined' 366 | ) 367 | } 368 | 369 | setRemaining (val) { 370 | if (!this.is_running) { 371 | console.error('timer is not running') 372 | return 373 | } 374 | this.end = Date.now() + val * 1000 375 | this.update(true) 376 | } 377 | 378 | playSound () { 379 | let url = this.play_sound 380 | if (!url || url === "false") return 381 | if (typeof url === 'boolean') { 382 | const src = this.src_location 383 | ? this.src_location.replace('/countdown.js', '') 384 | : 'libs/countdown' 385 | url = src + '/smb_stage_clear.mp3' 386 | } 387 | const sound = new Audio(url) 388 | sound.play() 389 | } 390 | 391 | bumpIncrementValue (val) { 392 | val = val || this.remainingTime().remaining 393 | if (val <= 30) { 394 | return 5 395 | } else if (val <= 300) { 396 | return 15 397 | } else if (val <= 3000) { 398 | return 30 399 | } else { 400 | return 60 401 | } 402 | } 403 | 404 | emitStateEvent (action) { 405 | const data = { 406 | action, 407 | time: new Date().toISOString(), 408 | timer: { 409 | is_running: this.is_running, 410 | end: this.end ? new Date(this.end).toISOString() : null, 411 | remaining: this.remainingTime() 412 | } 413 | } 414 | 415 | this.reportStateToShiny(data) 416 | this.element.dispatchEvent(new CustomEvent('countdown', { detail: data, bubbles: true })) 417 | } 418 | 419 | reportStateToShiny (data) { 420 | if (!window.Shiny) return 421 | 422 | if (!window.Shiny.setInputValue) { 423 | // We're in Shiny but it isn't ready for input updates yet 424 | setTimeout(() => this.reportStateToShiny(data), 100) 425 | return 426 | } 427 | 428 | const { action, time, timer } = data 429 | 430 | const shinyData = { event: { action, time }, timer } 431 | 432 | window.Shiny.setInputValue(this.element.id, shinyData) 433 | } 434 | } 435 | 436 | (function () { 437 | const CURRENT_SCRIPT = document.currentScript.getAttribute('src') 438 | 439 | document.addEventListener('DOMContentLoaded', function () { 440 | const els = document.querySelectorAll('.countdown') 441 | if (!els || !els.length) { 442 | return 443 | } 444 | els.forEach(function (el) { 445 | el.countdown = new CountdownTimer(el, { src_location: CURRENT_SCRIPT }) 446 | }) 447 | 448 | if (window.Shiny) { 449 | Shiny.addCustomMessageHandler('countdown:update', function (x) { 450 | if (!x.id) { 451 | console.error('No `id` provided, cannot update countdown') 452 | return 453 | } 454 | const el = document.getElementById(x.id) 455 | el.countdown.setValues(x) 456 | }) 457 | 458 | Shiny.addCustomMessageHandler('countdown:start', function (id) { 459 | const el = document.getElementById(id) 460 | if (!el) return 461 | el.countdown.start() 462 | }) 463 | 464 | Shiny.addCustomMessageHandler('countdown:stop', function (id) { 465 | const el = document.getElementById(id) 466 | if (!el) return 467 | el.countdown.stop({manual: true}) 468 | }) 469 | 470 | Shiny.addCustomMessageHandler('countdown:reset', function (id) { 471 | const el = document.getElementById(id) 472 | if (!el) return 473 | el.countdown.reset() 474 | }) 475 | 476 | Shiny.addCustomMessageHandler('countdown:bumpUp', function (id) { 477 | const el = document.getElementById(id) 478 | if (!el) return 479 | el.countdown.bumpUp() 480 | }) 481 | 482 | Shiny.addCustomMessageHandler('countdown:bumpDown', function (id) { 483 | const el = document.getElementById(id) 484 | if (!el) return 485 | el.countdown.bumpDown() 486 | }) 487 | } 488 | }) 489 | })() 490 | -------------------------------------------------------------------------------- /_extensions/gadenbuie/countdown/assets/smb_stage_clear.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coatless-tutorials/next-gen-data-science-education-wasm/7164b95d3feaefaf682d93b1d6a2103a882b7715/_extensions/gadenbuie/countdown/assets/smb_stage_clear.mp3 -------------------------------------------------------------------------------- /_extensions/gadenbuie/countdown/config.lua: -------------------------------------------------------------------------------- 1 | local countdown_version = '0.5.0' 2 | 3 | return { countdownVersion = countdown_version } 4 | -------------------------------------------------------------------------------- /_extensions/gadenbuie/countdown/countdown.lua: -------------------------------------------------------------------------------- 1 | -- Retrieve countdown configuration data 2 | local configData = require("config") 3 | -- Specify the embedded version 4 | local countdownVersion = configData.countdownVersion 5 | 6 | -- Only embed resources once if there are multiple timers present 7 | local needsToExportDependencies = true 8 | 9 | -- List CSS default options 10 | local default_style_keys = { 11 | "font_size", 12 | "margin", 13 | "padding", 14 | "box_shadow", 15 | "border_width", 16 | "border_radius", 17 | "line_height", 18 | "color_border", 19 | "color_background", 20 | "color_text", 21 | "color_running_background", 22 | "color_running_border", 23 | "color_running_text", 24 | "color_finished_background", 25 | "color_finished_border", 26 | "color_finished_text", 27 | "color_warning_background", 28 | "color_warning_border", 29 | "color_warning_text" 30 | } 31 | 32 | -- Check if variable missing or an empty string 33 | local function isVariableEmpty(s) 34 | return s == nil or s == '' 35 | end 36 | 37 | -- Check if variable is present 38 | local function isVariablePopulated(s) 39 | return not isVariableEmpty(s) 40 | end 41 | 42 | -- Check if a table is empty 43 | local function isTableEmpty(tbl) 44 | return next(tbl) == nil 45 | end 46 | 47 | -- Check if a table is populated 48 | local function isTablePopulated(tbl) 49 | return not isTableEmpty(tbl) 50 | end 51 | 52 | -- Check whether an argument is present in kwargs 53 | -- If it is, return the value 54 | local function tryOption(options, key) 55 | 56 | -- Protect against an empty options 57 | if not (options and options[key]) then 58 | return nil 59 | end 60 | 61 | -- Retrieve the option 62 | local option_value = pandoc.utils.stringify(options[key]) 63 | -- Verify the option's value exists, return value otherwise nil. 64 | if isVariablePopulated(option_value) then 65 | return option_value 66 | else 67 | return nil 68 | end 69 | end 70 | 71 | -- Retrieve the option value or use the default value 72 | local function getOption(options, key, default) 73 | return tryOption(options, key) or default 74 | end 75 | 76 | -- Check whether the play_sound parameter contains `"true"`/`"false"` or 77 | -- if it is a custom path 78 | local function tryPlaySound(play_sound) 79 | if play_sound == "false" or play_sound == "true" then 80 | return play_sound 81 | elseif type(play_sound) == "string" and string.len(play_sound) > 0 then 82 | return play_sound 83 | else 84 | return "false" 85 | end 86 | end 87 | 88 | -- Define the infix operator %:?% to handle styling if missing 89 | local function safeStyle(options, key, fmtString) 90 | -- Attempt to retrieve the style option 91 | local style_option = tryOption(options, key) 92 | -- If it is present, format it as a CSS value 93 | if isVariablePopulated(style_option) then 94 | return string.format(fmtString, key:gsub("_", "-"), style_option) 95 | end 96 | -- Otherwise, return an empty string that when concatenated does nothing. 97 | return "" 98 | end 99 | 100 | -- Construct the CSS style attributes 101 | local function structureCountdownCSSVars(options) 102 | -- Concatenate style properties with their values using %:?% from kwargs 103 | local stylePositional = {"top", "right", "bottom", "left"} 104 | local stylePositionalTable = {} 105 | local styleDefaultOptionsTable = {} 106 | 107 | -- Build the positional style without prefixing countdown variables 108 | for i, key in ipairs(stylePositional) do 109 | stylePositionalTable[i] = safeStyle(options, key, "%s: %s;") 110 | end 111 | 112 | -- Build the countdown variables for styling 113 | for i, key in ipairs(default_style_keys) do 114 | styleDefaultOptionsTable[i] = safeStyle(options, key, "--countdown-%s: %s;") 115 | end 116 | 117 | -- Concatenate entries together 118 | return table.concat(stylePositionalTable) .. table.concat(styleDefaultOptionsTable) 119 | end 120 | 121 | -- Handle global styling options by reading options set in the meta key 122 | local function countdown_style(options) 123 | 124 | -- Check if options have values; if it is empty, just exit. 125 | if isVariableEmpty(options) or isTableEmpty(options) then 126 | return nil 127 | end 128 | 129 | -- Determine the selector value 130 | local possibleSelector = getOption(options, "selector", ":root") 131 | 132 | -- Restructure options to ("key:value;--countdown-: ;) string 133 | local structuredCSS = structureCountdownCSSVars(options) 134 | 135 | -- Embed into the document to avoid rendering to disk and, then, embedding a URL. 136 | quarto.doc.include_text('in-header', 137 | string.format( 138 | "\n", 139 | possibleSelector, 140 | structuredCSS 141 | ) 142 | ) 143 | -- Note: This feature or using `add_supporting` requires Quarto v1.4 or above 144 | 145 | end 146 | 147 | -- Handle embedding/creation of assets once 148 | local function ensureHTMLDependency(meta) 149 | -- Register _all_ assets together. 150 | quarto.doc.addHtmlDependency({ 151 | name = "countdown", 152 | version = countdownVersion, 153 | scripts = { "assets/countdown.js"}, 154 | stylesheets = { "assets/countdown.css"}, 155 | resources = {"assets/smb_stage_clear.mp3"} 156 | }) 157 | 158 | -- Embed custom settings into the document based on document-level settings 159 | countdown_style(meta.countdown) 160 | 161 | -- Disable re-exporting if no-longer needed 162 | needsToExportDependencies = false 163 | end 164 | 165 | -- Function to parse an unnamed time string argument supplied 166 | -- in the format of 'MM:SS' 167 | local function parseTimeString(args) 168 | -- Check if the input argument is provided and is of type string 169 | if #args == 0 or type(args[1]) ~= "string" then 170 | return nil 171 | end 172 | 173 | -- Attempt to extract minutes and seconds from the time string 174 | local minutes, seconds = args[1]:match("(%d+):(%d+)") 175 | 176 | -- Check if the pattern matching was successful 177 | if isVariableEmpty(minutes) or isVariableEmpty(seconds) then 178 | -- Log an error message if the format is incorrect 179 | quarto.log.error( 180 | "The quartodown time string must be in the format 'MM:SS'.\n" .. 181 | "Please correct countdown timer with time string given as `" .. args[1] .. "`" 182 | ) 183 | -- Raise an assertion error to stop further execution (optional, depending on your requirements) 184 | assert("true" == "false") 185 | end 186 | 187 | -- Return a table containing minutes and seconds as numbers 188 | return { minutes = tonumber(minutes), seconds = tonumber(seconds) } 189 | end 190 | 191 | local function countdown(args, kwargs, meta) 192 | local minutes, seconds 193 | 194 | -- Retrieve named time arguments and fallback on default values if missing 195 | local arg_time = parseTimeString(args) 196 | if isVariablePopulated(arg_time) then 197 | minutes = arg_time.minutes 198 | seconds = arg_time.seconds 199 | if isVariablePopulated(tryOption(kwargs, "minutes")) or 200 | isVariablePopulated(tryOption(kwargs, "seconds")) then 201 | quarto.log.warning( 202 | "Please do not specify `minutes` or `seconds` parameters" .. 203 | "when using the time string format.") 204 | end 205 | else 206 | minutes = tonumber(getOption(kwargs, "minutes", 1)) 207 | seconds = tonumber(getOption(kwargs, "seconds", 0)) 208 | end 209 | 210 | -- Calculate total time in seconds 211 | local time = minutes * 60 + seconds 212 | 213 | -- Calculate minutes by dividing total time by 60 and rounding down 214 | minutes = math.floor(time / 60) 215 | 216 | -- Calculate remaining seconds after extracting minutes 217 | seconds = time - minutes * 60 218 | 219 | -- Check if minutes is greater than or equal to 100 (the maximum possible for display) 220 | if minutes >= 100 then 221 | quarto.log.error("The number of minutes must be less than 100.") 222 | assert("true" == "false") 223 | end 224 | 225 | if needsToExportDependencies then 226 | ensureHTMLDependency(meta) 227 | end 228 | 229 | -- Retrieve the ID given by the user or attempt to create a unique ID by timestamp 230 | local id = getOption(kwargs, "id", "timer_" .. pandoc.utils.sha1(tostring(os.time()))) 231 | 232 | -- Construct the 'class' attribute by appending "countdown" to the existing class (if any) 233 | local class = getOption(kwargs, "class", "") 234 | class = class ~= "" and "countdown " .. class or "countdown" 235 | 236 | -- Determine if a warning should be given 237 | local warn_when = tonumber(getOption(kwargs, "warn_when", 0)) 238 | 239 | -- Retrieve and convert "update_every" attribute to a number, default to 1 if not present or invalid 240 | local update_every = tonumber(getOption(kwargs, "update_every", 1)) 241 | 242 | -- Retrieve "blink_colon" attribute and set 'blink_colon' to true if it equals "true", otherwise false 243 | local blink_colon = getOption(kwargs, "blink_colon", update_every > 1) == "true" 244 | 245 | -- Retrieve "start_immediately" attribute and set 'start_immediately' to true if it equals "true", otherwise false 246 | local start_immediately = getOption(kwargs, "start_immediately", "false") == "true" 247 | 248 | -- Retrieve "play_sound" attribute as a string, default to "false" if not present 249 | local play_sound = tryPlaySound(getOption(kwargs, "play_sound", "false")) 250 | 251 | -- Check to see if positional outcomes are set; if not, default both bottom and right to 0. 252 | if isVariableEmpty(tryOption(kwargs, "top")) and 253 | isVariableEmpty(tryOption(kwargs, "bottom")) then 254 | kwargs["bottom"] = 0 255 | end 256 | 257 | if isVariableEmpty(tryOption(kwargs, "left")) and 258 | isVariableEmpty(tryOption(kwargs, "right")) then 259 | kwargs["right"] = 0 260 | end 261 | 262 | local style = structureCountdownCSSVars(kwargs) 263 | 264 | local rawHtml = table.concat({ 265 | '
', 275 | '\n
', 276 | '\n ', 277 | '\n ', 278 | '\n
', 279 | '\n ', 280 | '', string.format("%02d", minutes), 281 | ':', 282 | '', string.format("%02d", seconds), '', 283 | '\n
' 284 | }) 285 | 286 | -- Return a new Div element with modified attributes 287 | return pandoc.RawBlock("html", rawHtml) 288 | end 289 | 290 | 291 | 292 | return { 293 | ['countdown'] = countdown 294 | } 295 | -------------------------------------------------------------------------------- /_extensions/r-wasm/drop/_extension.yml: -------------------------------------------------------------------------------- 1 | title: Quarto Drop 2 | author: George Stagg 3 | version: 0.1.0-dev 4 | quarto-required: ">=1.3.0" 5 | contributes: 6 | revealjs-plugins: 7 | - name: RevealDrop 8 | script: 9 | - drop-runtime.js 10 | stylesheet: 11 | - drop-runtime.css 12 | config: 13 | drop: 14 | button: true 15 | shortcut: "`" 16 | engine: webr 17 | webr: 18 | packages: [] 19 | pyodide: 20 | packages: [] 21 | -------------------------------------------------------------------------------- /_extensions/r-wasm/drop/drop-runtime.css: -------------------------------------------------------------------------------- 1 | .reveal .drop-clip{position:absolute;inset:0;overflow:hidden}.reveal .drop{position:relative;transition:all .25s ease-in-out;bottom:100%;background-color:var(--quarto-body-bg);color:var(--quarto-body-color);box-shadow:inset 0 0 0 1px var(--quarto-border-color);z-index:5}.reveal .drop.active{transition:all .25s ease-in-out;bottom:0}.reveal .drop-button{position:fixed;z-index:30;bottom:6px;left:54px}.reveal .drop-button~.slide-chalkboard-buttons.slide-menu-offset{left:95px}@media screen and (max-width: 800px){.reveal .drop-button a>svg{width:18px;height:18px}.reveal .drop-button{bottom:3px;left:36px}.reveal .drop-button~.slide-chalkboard-buttons.slide-menu-offset{left:62px}}@media print{.reveal .drop-clip,.drop-button{display:none!important}}.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;inset:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{position:absolute;inset:0;z-index:10;color:transparent;pointer-events:none}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}#terminal{color:var(--quarto-body-color);background-color:var(--quarto-body-bg);padding:0 5px 40px}.terminal-container{flex-grow:1;overflow:auto}@layer rdg{@layer Defaults,FocusSink,CheckboxInput,CheckboxIcon,CheckboxLabel,Cell,HeaderCell,SummaryCell,EditCell,Row,HeaderRow,SummaryRow,GroupedRow,Root;}.mlln6zg7-0-0-beta-44{@layer rdg.MeasuringCell{contain:strict;grid-row:1;visibility:hidden}}.cj343x07-0-0-beta-44{@layer rdg.Cell{position:relative;padding-block:0;padding-inline:8px;border-inline-end:1px solid var(--rdg-border-color);border-block-end:1px solid var(--rdg-border-color);grid-row-start:var(--rdg-grid-row-start);background-color:inherit;white-space:nowrap;overflow:clip;text-overflow:ellipsis;outline:none;&[aria-selected=true]{outline:2px solid var(--rdg-selection-color);outline-offset:-2px}}}.csofj7r7-0-0-beta-44{@layer rdg.Cell{position:sticky;z-index:1;&:nth-last-child(1 of&){box-shadow:var(--rdg-cell-frozen-box-shadow)}}}.c1bn88vv7-0-0-beta-44{@layer rdg.CheckboxLabel{cursor:pointer;display:flex;align-items:center;justify-content:center;position:absolute;inset:0;margin-inline-end:1px}}.c1qt073l7-0-0-beta-44{@layer rdg.CheckboxInput{all:unset}}.cf71kmq7-0-0-beta-44{@layer rdg.CheckboxIcon{content:"";inline-size:20px;block-size:20px;border:2px solid var(--rdg-border-color);background-color:var(--rdg-background-color);.c1qt073l7-0-0-beta-44:checked+&{background-color:var(--rdg-checkbox-color);outline:4px solid var(--rdg-background-color);outline-offset:-6px}.c1qt073l7-0-0-beta-44:focus+&{border-color:var(--rdg-checkbox-focus-color)}}}.c1lwve4p7-0-0-beta-44{@layer rdg.CheckboxLabel{cursor:default;.cf71kmq7-0-0-beta-44{border-color:var(--rdg-checkbox-disabled-border-color);background-color:var(--rdg-checkbox-disabled-background-color)}}}.g1s9ylgp7-0-0-beta-44{@layer rdg.GroupCellContent{outline:none}}.cz54e4y7-0-0-beta-44{@layer rdg.GroupCellCaret{margin-inline-start:4px;stroke:currentColor;stroke-width:1.5px;fill:transparent;vertical-align:middle;>path{transition:d .1s}}}.c1w9bbhr7-0-0-beta-44{@layer rdg.DragHandle{--rdg-drag-handle-size: 8px;z-index:0;cursor:move;inline-size:var(--rdg-drag-handle-size);block-size:var(--rdg-drag-handle-size);background-color:var(--rdg-selection-color);place-self:end;&:hover{--rdg-drag-handle-size: 16px;border:2px solid var(--rdg-selection-color);background-color:var(--rdg-background-color)}}}.c1creorc7-0-0-beta-44{@layer rdg.DragHandle{z-index:1;position:sticky}}.cis5rrm7-0-0-beta-44{@layer rdg.EditCell{padding:0}}.h44jtk67-0-0-beta-44{@layer rdg.SortableHeaderCell{display:flex}}.hcgkhxz7-0-0-beta-44{@layer rdg.SortableHeaderCellName{flex-grow:1;overflow:clip;text-overflow:ellipsis}}.c6l2wv17-0-0-beta-44{@layer rdg.HeaderCell{cursor:pointer}}.c1kqdw7y7-0-0-beta-44{@layer rdg.HeaderCell{touch-action:none}}.r1y6ywlx7-0-0-beta-44{@layer rdg.HeaderCell{cursor:col-resize;position:absolute;inset-block-start:0;inset-inline-end:0;inset-block-end:0;inline-size:10px}}.c1bezg5o7-0-0-beta-44{opacity:.5}.c1vc96037-0-0-beta-44{background-color:var(--rdg-header-draggable-background-color)}.r1upfr807-0-0-beta-44{@layer rdg.Row{display:contents;line-height:var(--rdg-row-height);background-color:var(--rdg-background-color);&:hover{background-color:var(--rdg-row-hover-background-color)}&[aria-selected=true]{background-color:var(--rdg-row-selected-background-color);&:hover{background-color:var(--rdg-row-selected-hover-background-color)}}}}.r190mhd37-0-0-beta-44{@layer rdg.FocusSink{outline:2px solid var(--rdg-selection-color);outline-offset:-2px}}.r139qu9m7-0-0-beta-44{@layer rdg.FocusSink{&:before{content:"";display:inline-block;height:100%;position:sticky;inset-inline-start:0;border-inline-start:2px solid var(--rdg-selection-color)}}}.h10tskcx7-0-0-beta-44{@layer rdg.HeaderRow{display:contents;line-height:var(--rdg-header-row-height);background-color:var(--rdg-header-background-color);font-weight:700;>.cj343x07-0-0-beta-44{z-index:2;position:sticky}>.csofj7r7-0-0-beta-44{z-index:3}}}.c6ra8a37-0-0-beta-44{@layer rdg.Cell{background-color:#ccf}}.cq910m07-0-0-beta-44{@layer rdg.Cell{background-color:#ccf;&.c6ra8a37-0-0-beta-44{background-color:#99f}}}.a3ejtar7-0-0-beta-44{@layer rdg.SortIcon{fill:currentColor;>path{transition:d .1s}}}.rnvodz57-0-0-beta-44{@layer rdg.Defaults{*,*:before,*:after{box-sizing:inherit}}@layer rdg.Root{--rdg-color: #000;--rdg-border-color: #ddd;--rdg-summary-border-color: #aaa;--rdg-background-color: hsl(0deg 0% 100%);--rdg-header-background-color: hsl(0deg 0% 97.5%);--rdg-header-draggable-background-color: hsl(0deg 0% 90.5%);--rdg-row-hover-background-color: hsl(0deg 0% 96%);--rdg-row-selected-background-color: hsl(207deg 76% 92%);--rdg-row-selected-hover-background-color: hsl(207deg 76% 88%);--rdg-checkbox-color: hsl(207deg 100% 29%);--rdg-checkbox-focus-color: hsl(207deg 100% 69%);--rdg-checkbox-disabled-border-color: #ccc;--rdg-checkbox-disabled-background-color: #ddd;--rdg-selection-color: #66afe9;--rdg-font-size: 14px;--rdg-cell-frozen-box-shadow: calc(2px * var(--rdg-sign)) 0 5px -2px rgba(136, 136, 136, .3);display:grid;color-scheme:var(--rdg-color-scheme, light dark);contain:content;content-visibility:auto;block-size:350px;border:1px solid var(--rdg-border-color);box-sizing:border-box;overflow:auto;background-color:var(--rdg-background-color);color:var(--rdg-color);font-size:var(--rdg-font-size);&:before{content:"";grid-column:1/-1;grid-row:1/-1}&.rdg-dark{--rdg-color-scheme: dark;--rdg-color: #ddd;--rdg-border-color: #444;--rdg-summary-border-color: #555;--rdg-background-color: hsl(0deg 0% 13%);--rdg-header-background-color: hsl(0deg 0% 10.5%);--rdg-header-draggable-background-color: hsl(0deg 0% 17.5%);--rdg-row-hover-background-color: hsl(0deg 0% 9%);--rdg-row-selected-background-color: hsl(207deg 76% 42%);--rdg-row-selected-hover-background-color: hsl(207deg 76% 38%);--rdg-checkbox-color: hsl(207deg 100% 79%);--rdg-checkbox-focus-color: hsl(207deg 100% 89%);--rdg-checkbox-disabled-border-color: #000;--rdg-checkbox-disabled-background-color: #333}&.rdg-light{--rdg-color-scheme: light}@media (prefers-color-scheme: dark){&:not(.rdg-light){--rdg-color: #ddd;--rdg-border-color: #444;--rdg-summary-border-color: #555;--rdg-background-color: hsl(0deg 0% 13%);--rdg-header-background-color: hsl(0deg 0% 10.5%);--rdg-header-draggable-background-color: hsl(0deg 0% 17.5%);--rdg-row-hover-background-color: hsl(0deg 0% 9%);--rdg-row-selected-background-color: hsl(207deg 76% 42%);--rdg-row-selected-hover-background-color: hsl(207deg 76% 38%);--rdg-checkbox-color: hsl(207deg 100% 79%);--rdg-checkbox-focus-color: hsl(207deg 100% 89%);--rdg-checkbox-disabled-border-color: #000;--rdg-checkbox-disabled-background-color: #333}}>:nth-last-child(1 of.rdg-top-summary-row){>.cj343x07-0-0-beta-44{border-block-end:2px solid var(--rdg-summary-border-color)}}>:nth-child(1 of.rdg-bottom-summary-row){>.cj343x07-0-0-beta-44{border-block-start:2px solid var(--rdg-summary-border-color)}}}}.vlqv91k7-0-0-beta-44{@layer rdg.Root{user-select:none;.r1upfr807-0-0-beta-44{cursor:move}}}.f1lsfrzw7-0-0-beta-44{@layer rdg.FocusSink{grid-column:1/-1;pointer-events:none;z-index:1}}.f1cte0lg7-0-0-beta-44{@layer rdg.FocusSink{z-index:3}}.s8wc6fl7-0-0-beta-44{@layer rdg.SummaryCell{inset-block-start:var(--rdg-summary-row-top);inset-block-end:var(--rdg-summary-row-bottom)}}.skuhp557-0-0-beta-44{@layer rdg.SummaryRow{line-height:var(--rdg-summary-row-height);>.cj343x07-0-0-beta-44{position:sticky}}}.tf8l5ub7-0-0-beta-44{@layer rdg.SummaryRow{>.cj343x07-0-0-beta-44{z-index:2}>.csofj7r7-0-0-beta-44{z-index:3}}}.g1yxluv37-0-0-beta-44{@layer rdg.GroupedRow{&:not([aria-selected=true]){background-color:var(--rdg-header-background-color)}>.cj343x07-0-0-beta-44:not(:last-child,.csofj7r7-0-0-beta-44),>:nth-last-child(n+2 of.csofj7r7-0-0-beta-44){border-inline-end:none}}}.t7vyx3i7-0-0-beta-44{@layer rdg.TextEditor{appearance:none;box-sizing:border-box;inline-size:100%;block-size:100%;padding-block:0;padding-inline:6px;border:2px solid #ccc;vertical-align:top;color:var(--rdg-color);background-color:var(--rdg-background-color);font-family:inherit;font-size:var(--rdg-font-size);&:focus{border-color:var(--rdg-selection-color);outline:none}&::placeholder{color:#999;opacity:1}}}#editor{position:relative}.editor-container{flex:1;overflow:auto}.editor-header{display:flex;justify-content:end;align-items:center;border-bottom:1px solid var(--quarto-text-muted)}.editor-actions{line-height:0}.editor-actions>button{background-color:transparent;border:none;color:var(--quarto-text-muted);padding:8px 5px;font-size:16px}.editor-actions>button:hover{color:var(--quarto-body-color)}.d-none{display:none!important}.quarto-light{--cm-primary-rgb: "13, 110, 253";--cm-default: #dee2e6;--cm-cap-bg: #f8f8f8;--cm-line-bg: rgba(var(--cm-primary-rgb), .05);--cm-line-gutter-bg: rgba(var(--cm-primary-rgb), .1)}.quarto-dark{--cm-primary-rgb: "55, 90, 127";--cm-default: #434343;--cm-cap-bg: #505050;--cm-line-bg: rgba(var(--cm-primary-rgb), .2);--cm-line-gutter-bg: rgba(var(--cm-primary-rgb), .4)}.reveal .drop .cm-editor{border:none;outline:none;height:100%;font-size:22px;color:var(--quarto-body-color);background-color:var(--quarto-body-bg)}.reveal .drop .cm-content{caret-color:var(--quarto-body-color)}.reveal .drop .cm-cursor,.reveal .drop .cm-dropCursor{border-left-color:var(--quarto-body-color)}.reveal .drop .cm-focused .cm-selectionBackgroundm .cm-selectionBackground,.reveal .drop .cm-content ::selection{border:none;outline:none;background-color:rgba(var(--cm-primary-rgb),.1)}.reveal .drop .cm-activeLine{background-color:var(--cm-line-bg)}.reveal .drop .cm-activeLineGutter{background-color:var(--cm-line-gutter-bg)}.reveal .drop .cm-gutters{background-color:var(--cm-cap-bg);color:var(--quarto-body-color);border-right:1px solid var(--cm-default)}:root{--cm-editor-hl-al: var(--quarto-hl-al-color, #AD0000);--cm-editor-hl-an: var(--quarto-hl-an-color, #5E5E5E);--cm-editor-hl-at: var(--quarto-hl-at-color, #657422);--cm-editor-hl-bn: var(--quarto-hl-bn-color, #AD0000);--cm-editor-hl-ch: var(--quarto-hl-ch-color, #20794D);--cm-editor-hl-co: var(--quarto-hl-co-color, #5E5E5E);--cm-editor-hl-cv: var(--quarto-hl-cv-color, #5E5E5E);--cm-editor-hl-cn: var(--quarto-hl-cn-color, #8f5902);--cm-editor-hl-cf: var(--quarto-hl-cf-color, #003B4F);--cm-editor-hl-dt: var(--quarto-hl-dt-color, #AD0000);--cm-editor-hl-dv: var(--quarto-hl-dv-color, #AD0000);--cm-editor-hl-do: var(--quarto-hl-do-color, #5E5E5E);--cm-editor-hl-er: var(--quarto-hl-er-color, #AD0000);--cm-editor-hl-fl: var(--quarto-hl-fl-color, #AD0000);--cm-editor-hl-fu: var(--quarto-hl-fu-color, #4758AB);--cm-editor-hl-im: var(--quarto-hl-im-color, #00769E);--cm-editor-hl-in: var(--quarto-hl-in-color, #5E5E5E);--cm-editor-hl-kw: var(--quarto-hl-kw-color, #003B4F);--cm-editor-hl-op: var(--quarto-hl-op-color, #5E5E5E);--cm-editor-hl-ot: var(--quarto-hl-ot-color, #003B4F);--cm-editor-hl-pp: var(--quarto-hl-pp-color, #AD0000);--cm-editor-hl-sc: var(--quarto-hl-sc-color, #5E5E5E);--cm-editor-hl-ss: var(--quarto-hl-ss-color, #20794D);--cm-editor-hl-st: var(--quarto-hl-st-color, #20794D);--cm-editor-hl-va: var(--quarto-hl-va-color, #111111);--cm-editor-hl-vs: var(--quarto-hl-vs-color, #20794D);--cm-editor-hl-wa: var(--quarto-hl-wa-color, #5E5E5E)}*[data-bs-theme=dark]{--cm-editor-hl-al: var(--quarto-hl-al-color, #f07178);--cm-editor-hl-an: var(--quarto-hl-an-color, #d4d0ab);--cm-editor-hl-at: var(--quarto-hl-at-color, #00e0e0);--cm-editor-hl-bn: var(--quarto-hl-bn-color, #d4d0ab);--cm-editor-hl-bu: var(--quarto-hl-bu-color, #abe338);--cm-editor-hl-ch: var(--quarto-hl-ch-color, #abe338);--cm-editor-hl-co: var(--quarto-hl-co-color, #f8f8f2);--cm-editor-hl-cv: var(--quarto-hl-cv-color, #ffd700);--cm-editor-hl-cn: var(--quarto-hl-cn-color, #ffd700);--cm-editor-hl-cf: var(--quarto-hl-cf-color, #ffa07a);--cm-editor-hl-dt: var(--quarto-hl-dt-color, #ffa07a);--cm-editor-hl-dv: var(--quarto-hl-dv-color, #d4d0ab);--cm-editor-hl-do: var(--quarto-hl-do-color, #f8f8f2);--cm-editor-hl-er: var(--quarto-hl-er-color, #f07178);--cm-editor-hl-ex: var(--quarto-hl-ex-color, #00e0e0);--cm-editor-hl-fl: var(--quarto-hl-fl-color, #d4d0ab);--cm-editor-hl-fu: var(--quarto-hl-fu-color, #ffa07a);--cm-editor-hl-im: var(--quarto-hl-im-color, #abe338);--cm-editor-hl-in: var(--quarto-hl-in-color, #d4d0ab);--cm-editor-hl-kw: var(--quarto-hl-kw-color, #ffa07a);--cm-editor-hl-op: var(--quarto-hl-op-color, #ffa07a);--cm-editor-hl-ot: var(--quarto-hl-ot-color, #00e0e0);--cm-editor-hl-pp: var(--quarto-hl-pp-color, #dcc6e0);--cm-editor-hl-re: var(--quarto-hl-re-color, #00e0e0);--cm-editor-hl-sc: var(--quarto-hl-sc-color, #abe338);--cm-editor-hl-ss: var(--quarto-hl-ss-color, #abe338);--cm-editor-hl-st: var(--quarto-hl-st-color, #abe338);--cm-editor-hl-va: var(--quarto-hl-va-color, #00e0e0);--cm-editor-hl-vs: var(--quarto-hl-vs-color, #abe338);--cm-editor-hl-wa: var(--quarto-hl-wa-color, #dcc6e0)}.reveal .drop .cm-editor span.tok-keyword{color:var(--cm-editor-hl-kw)}.reveal .drop .cm-editor span.tok-operator{color:var(--cm-editor-hl-op)}.reveal .drop .cm-editor span.tok-definitionOperator,.reveal .drop .cm-editor span.tok-compareOperator{color:var(--cm-editor-hl-ot)}.reveal .drop .cm-editor span.tok-attributeName{color:var(--cm-editor-hl-at)}.reveal .drop .cm-editor span.tok-controlKeyword{color:var(--cm-editor-hl-cf)}.reveal .drop .cm-editor span.tok-comment{color:var(--cm-editor-hl-co)}.reveal .drop .cm-editor span.tok-string{color:var(--cm-editor-hl-st)}.reveal .drop .cm-editor span.tok-string2{color:var(--cm-editor-hl-ss)}.reveal .drop .cm-editor span.tok-variableName{color:var(--cm-editor-hl-va)}.reveal .drop .cm-editor span.tok-bool,.reveal .drop .cm-editor span.tok-literal,.reveal .drop .cm-editor span.tok-separator{color:var(--cm-editor-hl-cn)}.reveal .drop .cm-editor span.tok-number,.reveal .drop .cm-editor span.tok-integer{color:var(--cm-editor-hl-dv)}.reveal .drop .cm-editor span.tok-function-variableName{color:var(--cm-editor-hl-fu)}.reveal .drop .cm-editor span.tok-function-attributeName{color:var(--cm-editor-hl-at)}.plot-background{background-color:var(--quarto-body-bg);flex-grow:1;overflow:auto;display:flex;justify-content:center;align-items:center}.plot-container{max-width:90%;max-height:90%;overflow:hidden;background-color:#fff}.plot-container canvas{width:100%;height:100%;display:block}.reveal .drop .app{width:100vw;height:100vh;height:100dvh}div[data-panel]{display:flex;flex-direction:column}div[data-resize-handle]{padding:2px;background-color:var(--quarto-border-color)} 2 | /*! Bundled license information: 3 | 4 | xterm/css/xterm.css: 5 | (** 6 | * Copyright (c) 2014 The xterm.js authors. All rights reserved. 7 | * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) 8 | * https://github.com/chjj/term.js 9 | * @license MIT 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a copy 12 | * of this software and associated documentation files (the "Software"), to deal 13 | * in the Software without restriction, including without limitation the rights 14 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | * copies of the Software, and to permit persons to whom the Software is 16 | * furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included in 19 | * all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | * THE SOFTWARE. 28 | * 29 | * Originally forked from (with the author's permission): 30 | * Fabrice Bellard's javascript vt100 for jslinux: 31 | * http://bellard.org/jslinux/ 32 | * Copyright (c) 2011 Fabrice Bellard 33 | * The original design remains. The terminal itself 34 | * has been extended to include xterm CSI codes, among 35 | * other features. 36 | *) 37 | */ 38 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_quarto.yml: -------------------------------------------------------------------------------- 1 | project: 2 | type: website 3 | output-dir: _site 4 | 5 | website: 6 | title: "The Next Generation of Data Science Education" 7 | reader-mode: true 8 | repo-url: https://github.com/coatless-tutorials/next-gen-data-science-education 9 | repo-actions: [edit, issue] 10 | navbar: 11 | background: light 12 | foreground: dark 13 | align: right 14 | right: 15 | - href: index.qmd 16 | text: Home 17 | - href: tutorials/demo-lab.qmd 18 | text: Demo Lab 19 | - href: https://r-wasm.github.io/quarto-live/ 20 | text: Quarto Live 21 | - icon: github 22 | href: https://github.com/coatless-tutorials/next-gen-data-science-education 23 | aria-label: GitHub 24 | -------------------------------------------------------------------------------- /index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "The Next Generation of Data Science Education" 3 | subtitle: "Interactive Coding in Web Browsers: A WebAssembly Demonstration" 4 | format: 5 | html: 6 | toc: true 7 | toc-depth: 2 8 | --- 9 | 10 | ## Welcome to the Future of Data Science Education 11 | 12 | In this demonstration, we showcase cutting-edge technology that brings interactive coding directly into slide decks using WebAssembly (WASM) through the new official Quarto WebAssembly backend: [`quarto-live`](https://r-wasm.github.io/quarto-live/) by George Stagg. This innovative approach revolutionizes how we present and teach data science concepts by allowing for real-time code execution, visualization, and exercises within the presentation itself. For more on the Quarto WebAssembly backend, see the [official documentation](https://r-wasm.github.io/quarto-live/). 13 | 14 | You can view the demonstration of this technology in the following slide deck: 15 | 16 | {{< revealjs file="slides/lecture-01.html" height="500px">}} 17 | 18 | In interactive lab form, we have: 19 | 20 | - [Exploring Palmer Penguins](tutorials/tutorials/demo-lab.qmd) 21 | 22 | 23 | ## What's Inside 24 | 25 | This demonstration includes a Linear Regression overview that uses both R and Python code snippets to illustrate the concepts. You can interact with the code blocks, modify them, and see the results instantly. We've also included a built-in timer on the exercise 26 | page to provide a stoppage time for the exercise. 27 | 28 | ## The Power of WebAssembly in Presentations 29 | 30 | WebAssembly is a binary instruction format for a stack-based virtual machine, designed as a portable target for high-level languages like C, C++, and Rust. By leveraging WebAssembly: 31 | 32 | - We can run R and Python code directly in the browser. 33 | - Presentations become interactive, allowing audience members to modify and run code in real-time. 34 | - Complex computations and visualizations can be performed client-side, reducing server load and improving responsiveness. 35 | 36 | ## How It Works 37 | 38 | 1. **R Integration**: [webR](https://docs.r-wasm.org/webr/latest/), an R distribution compiled to WebAssembly, is used by `quarto-live` to run R code in the browser. 39 | 2. **Python Integration**: [Pyodide](https://pyodide.org/en/stable/), a Python distribution for the browser, is used by `quarto-live` to execute Python code. 40 | 3. **Quarto + RevealJS**: The presentation is built using [Quarto](https://quarto.org/) and [RevealJS](https://revealjs.com/), providing a smooth, web-based slide experience. 41 | 4. **Quarto Extensions**: Additional Quarto extensions like `quarto-live`, `quarto-drop`, and `quarto-countdown` enhance the interactivity and functionality of the presentation. 42 | 43 | ## Benefits of This Approach 44 | 45 | - **Engagement**: Audience members can experiment with code in real-time, fostering active learning. 46 | - **Flexibility**: Presenters can easily modify examples on the fly to answer questions or explore different scenarios. 47 | - **Accessibility**: No need for local installations; everything runs in the browser. 48 | - **Reproducibility**: Ensures everyone sees the same results, regardless of their local setup. 49 | 50 | ## Getting Started 51 | 52 | To explore this demo: 53 | 54 | 1. Navigate through the links above to view each component. 55 | 2. In the slide decks and tutorials, look for interactive code blocks where you can modify and run code. 56 | 3. Experiment with different inputs and see how the outputs change in real-time. 57 | 58 | ## Technical Requirements 59 | 60 | ### For Viewers and Presenters 61 | 62 | - A modern web browser with WebAssembly support (most up-to-date browsers support this). 63 | - For the best experience, use a desktop or laptop computer rather than a mobile device. 64 | 65 | ### Authoring 66 | 67 | To create your own version of the demonstration, you need to install the following software: 68 | 69 | - RStudio IDE, VS Code, Positron, or another text editor 70 | - For VS Code or Positron, please install the [Quarto plugin](https://open-vsx.org/extension/quarto/quarto). 71 | - [Quarto](https://quarto.org) v1.4.0 or later 72 | - Quarto Extensions 73 | - [`quarto-live`](https://r-wasm.github.io/quarto-live/) 74 | - [`quarto-drop`](https://github.com/r-wasm/quarto-drop) 75 | - [`quarto-countdown`](https://github.com/gadenbuie/countdown/tree/main/quarto) 76 | - [`quarto-embedio`](https://github.com/coatless-quarto/embedio) 77 | 78 | You can install the Quarto Extensions by typing the following commands in your terminal: 79 | 80 | ```bash 81 | quarto add r-wasm/quarto-live 82 | quarto add r-wasm/quarto-drop 83 | quarto add gadenbuie/countdown/quarto 84 | quarto add coatless-quarto/embedio 85 | ``` 86 | 87 | 88 | ## Feedback and Questions 89 | 90 | Question or comments? Let me know either on the issue tracker or via socials. 91 | 92 | Enjoy exploring the future of interactive data science presentations! -------------------------------------------------------------------------------- /slides/lecture-01.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Demo: Data Science Education with WebAssembly" 3 | subtitle: "Linear Regression in R and Python" 4 | format: 5 | live-revealjs: 6 | scrollable: true 7 | webr: 8 | packages: 9 | - ggplot2 10 | pyodide: 11 | packages: 12 | - scikit-learn 13 | - pandas 14 | - matplotlib 15 | - seaborn 16 | - statsmodels 17 | 18 | engine: knitr 19 | --- 20 | 21 | {{< include ../_extensions/r-wasm/live/_knitr.qmd >}} 22 | 23 | ## Overview 24 | 25 | The goal of this presentation is to showcase the power of WebAssembly (WASM) in data science education by allowing real-time code execution, visualization, and exercises directly within the slide deck. 26 | 27 | We do this by exploring the concept of linear regression using both R and Python code snippets. 28 | 29 | 30 | --- 31 | 32 | ## Introduction 33 | 34 | Linear regression is a fundamental statistical technique used to model the relationship between a dependent variable and one or more independent variables. 35 | 36 | This presentation will cover: 37 | 38 | 1. Basic Concepts 39 | 2. Implementation in R and Python 40 | 3. Model Evaluation 41 | 4. Assumptions and Diagnostics 42 | 43 | --- 44 | 45 | ## Basic Concepts 46 | 47 | Linear regression aims to find the best-fitting straight line through the data points. 48 | 49 | The general form of a simple linear regression model is: 50 | 51 | $$Y = \beta_0 + \beta_1X + \epsilon$$ 52 | 53 | Where: 54 | 55 | - $Y$ is the dependent variable 56 | - $X$ is the independent variable 57 | - $\beta_0$ is the y-intercept 58 | - $\beta_1$ is the slope 59 | - $\epsilon$ is the error term 60 | 61 | --- 62 | 63 | ## Generating Data 64 | 65 | Let's look at how to implement linear regression in R and Python by first simulating some data 66 | 67 | ::: {.panel-tabset group="language"} 68 | 69 | ## R 70 | 71 | ```{webr} 72 | # Create sample data 73 | set.seed(123) 74 | x <- 1:100 75 | y <- 2 * x + 1 + rnorm(100, mean = 0, sd = 3) 76 | df <- data.frame(x = x, y = y) 77 | 78 | head(df) 79 | ``` 80 | 81 | ## Python 82 | 83 | ```{pyodide} 84 | import numpy as np 85 | import pandas as pd 86 | 87 | # Create sample data 88 | np.random.seed(123) 89 | x = np.arange(1, 101) 90 | y = 2 * x + 1 + np.random.normal(0, 3, 100) 91 | data = pd.DataFrame({'x': x, 'y': y}) 92 | 93 | data.head() 94 | ``` 95 | 96 | ::: 97 | 98 | --- 99 | 100 | ## Guessing the Coefficients 101 | 102 | Try to fit a linear regression model by hand through manipulating coefficients below: 103 | 104 | The linear regression with $\beta_0 =$ 105 | `{ojs} beta_0_Tgl` and $\beta_1 =$ `{ojs} beta_1_Tgl` is: 106 | 107 | ```{ojs} 108 | //| echo: false 109 | import {Tangle} from "@mbostock/tangle" 110 | 111 | // Setup Tangle reactive inputs 112 | viewof beta_0 = Inputs.input(0); 113 | viewof beta_1 = Inputs.input(1); 114 | beta_0_Tgl = Inputs.bind(Tangle({min: -30, max: 300, minWidth: "1em", step: 1}), viewof beta_0); 115 | beta_1_Tgl = Inputs.bind(Tangle({min: -5, max: 5, minWidth: "1em", step: 0.25}), viewof beta_1); 116 | 117 | // draw plot in R 118 | regression_plot(beta_0, beta_1) 119 | ``` 120 | 121 | ```{webr} 122 | #| edit: false 123 | #| output: false 124 | #| define: 125 | #| - regression_plot 126 | regression_plot <- function(beta_0, beta_1) { 127 | # Figure out why scope changed between Live august and now. 128 | set.seed(123) 129 | x <- 1:100 130 | y <- 2 * x + 1 + rnorm(100, mean = 0, sd = 3) 131 | df <- data.frame(x = x, y = y) 132 | 133 | # Create scatter plot 134 | plot( 135 | df$x, df$y, 136 | xlim = c(min(df$x) - 10, max(df$x) + 10), 137 | ylim = c(min(df$y) - 10, max(df$y) + 10) 138 | ) 139 | 140 | # Graph regression line 141 | abline(a = beta_0, b = beta_1, col = "red") 142 | } 143 | ``` 144 | 145 | --- 146 | 147 | ## Fit Linear Regression Model 148 | 149 | Now that we have our data, let's fit a linear regression model to it: 150 | 151 | ::: {.panel-tabset group="language"} 152 | 153 | ## R 154 | 155 | ```{webr} 156 | # Fit linear regression model 157 | model <- lm(y ~ x, data = df) 158 | 159 | # View summary of the model 160 | summary(model) 161 | ``` 162 | 163 | ## Python 164 | 165 | ```{pyodide} 166 | import matplotlib.pyplot as plt 167 | from sklearn.linear_model import LinearRegression 168 | 169 | # Fit linear regression model 170 | model = LinearRegression() 171 | model.fit(data[['x']], data['y']) 172 | 173 | # Print model coefficients 174 | print(f"Intercept: {model.intercept_:.2f}") 175 | print(f"Slope: {model.coef_[0]:.2f}") 176 | ``` 177 | 178 | ::: 179 | 180 | ## Visualize the Results 181 | 182 | We can visualize the data and the regression line to see how well the model fits the data using ggplot2 in R and Matplotlib in Python. 183 | 184 | ::: {.panel-tabset group="language"} 185 | 186 | ## R 187 | 188 | ```{webr} 189 | library(ggplot2) 190 | 191 | # Plot the data and regression line 192 | ggplot(df, aes(x = x, y = y)) + 193 | geom_point() + 194 | geom_smooth(method = "lm", se = FALSE, color = "red") + 195 | theme_minimal() + 196 | labs(title = "Linear Regression in R", 197 | x = "X", y = "Y") 198 | ``` 199 | 200 | ## Python 201 | 202 | ```{pyodide} 203 | # Plot the data and regression line 204 | plt.figure(figsize=(10, 6)) 205 | plt.scatter(data['x'], data['y']) 206 | plt.plot(data['x'], model.predict(data[['x']]), color='red') 207 | plt.title("Linear Regression in Python") 208 | plt.xlabel("X") 209 | plt.ylabel("Y") 210 | plt.show() 211 | ``` 212 | 213 | ::: 214 | 215 | --- 216 | 217 | ## Predicting New Values 218 | 219 | We can use our linear regression model to make predictions on new data: 220 | 221 | ::: {.panel-tabset group="language"} 222 | 223 | ## R 224 | 225 | ```{webr} 226 | # Predict new values 227 | new_data <- data.frame(x = c(101, 102, 103)) 228 | predictions <- predict(model, newdata = new_data) 229 | 230 | predictions 231 | ``` 232 | 233 | ## Python 234 | 235 | ```{pyodide} 236 | # Predict new values 237 | new_data = pd.DataFrame({'x': [101, 102, 103]}) 238 | predictions = model.predict(new_data) 239 | ``` 240 | 241 | ::: 242 | 243 | --- 244 | 245 | ## Your Turn: Predict New Values! 246 | 247 | {{< countdown "01:30" top="10px" right="5px">}} 248 | 249 | Create a new data frame with `x` values 10, 30, and 60, then use the model to predict the corresponding y values. 250 | 251 | ::: {.panel-tabset group="language"} 252 | 253 | ## R 254 | 255 | ```{webr} 256 | #| exercise: ex_1_r 257 | # Create your new data frame here 258 | _______ 259 | 260 | # Make predictions here 261 | _______ 262 | 263 | # Print the predictions 264 | _______ 265 | ``` 266 | 267 | ```{webr} 268 | #| exercise: ex_1_r 269 | #| check: true 270 | 271 | # Create your new data frame here 272 | new_data <- data.frame(x = c(10, 30, 60)) 273 | 274 | # Make predictions here 275 | predictions <- predict(model, newdata = new_data) 276 | 277 | if (isTRUE(all.equal(.result, predictions))) { 278 | list(correct = TRUE, message = "Nice work!") 279 | } else { 280 | list(correct = FALSE, message = "That's incorrect, sorry.") 281 | } 282 | ``` 283 | 284 | ## Python 285 | 286 | ```{pyodide} 287 | #| exercise: ex_1_py 288 | # Create your new Pandas data frame here 289 | _______ 290 | 291 | # Make predictions using the model 292 | _______ 293 | 294 | # Print the predictions 295 | _______ 296 | ``` 297 | 298 | ```{pyodide} 299 | #| exercise: ex_1_py 300 | #| check: true 301 | 302 | # Create a new DataFrame with x values 10, 30, and 60 303 | new_data_solution = pd.DataFrame({'x': [10, 30, 60]}) 304 | 305 | # Make predictions using the model 306 | predictions_solution = model.predict(new_data_solution) 307 | 308 | feedback = None 309 | if (result == predictions_solution): 310 | feedback = { "correct": True, "message": "Nice work!" } 311 | else: 312 | feedback = { "correct": False, "message": "That's incorrect, sorry." } 313 | 314 | feedback 315 | ``` 316 | 317 | ::: 318 | 319 | 320 | --- 321 | 322 | ## Model Evaluation 323 | 324 | We can evaluate the performance of our linear regression model using various metrics: 325 | 326 | ::: {.panel-tabset} 327 | 328 | ## R 329 | 330 | ```{webr} 331 | # R-squared 332 | summary(model)$r.squared 333 | 334 | # Root Mean Squared Error (RMSE) 335 | sqrt(mean(residuals(model)^2)) 336 | 337 | # Mean Absolute Error (MAE) 338 | mean(abs(residuals(model))) 339 | ``` 340 | 341 | ## Python 342 | 343 | ```{pyodide} 344 | from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error 345 | 346 | # R-squared 347 | r2 = r2_score(data['y'], model.predict(data[['x']])) 348 | 349 | # Root Mean Squared Error (RMSE) 350 | rmse = np.sqrt(mean_squared_error(data['y'], model.predict(data[['x']]))) 351 | 352 | # Mean Absolute Error (MAE) 353 | mae = mean_absolute_error(data['y'], model.predict(data[['x']])) 354 | 355 | print(f"R-squared: {r2:.4f}") 356 | print(f"RMSE: {rmse:.4f}") 357 | print(f"MAE: {mae:.4f}") 358 | ``` 359 | 360 | ::: 361 | 362 | --- 363 | 364 | ## Assumptions 365 | 366 | Linear regression relies on several assumptions: 367 | 368 | 1. Linearity 369 | 2. Independence 370 | 3. Homoscedasticity 371 | 4. Normality of residuals 372 | 373 | --- 374 | 375 | ## Checking Assumptions with Diagnostics Plots 376 | 377 | Let's look at some diagnostic plots: 378 | 379 | ::: {.panel-tabset} 380 | 381 | ## R 382 | 383 | ```{webr} 384 | par(mfrow = c(2, 2)) 385 | plot(model) 386 | ``` 387 | 388 | ## Python 389 | 390 | ```{pyodide} 391 | import seaborn as sns 392 | 393 | # Residual plot 394 | plt.figure(figsize=(10, 6)) 395 | sns.residplot(x=model.predict(data[['x']]), y=data['y'], lowess=True) 396 | plt.title("Residual Plot") 397 | plt.xlabel("Predicted Values") 398 | plt.ylabel("Residuals") 399 | plt.show() 400 | 401 | # Q-Q plot 402 | from scipy import stats 403 | 404 | fig, ax = plt.subplots(figsize=(10, 6)) 405 | _, (__, ___, r) = stats.probplot(model.resid, plot=ax, fit=True) 406 | ax.set_title("Q-Q Plot") 407 | plt.show() 408 | ``` 409 | 410 | ::: 411 | 412 | --- 413 | 414 | ## Conclusion 415 | 416 | - Linear regression is a powerful tool for modeling relationships between variables. 417 | - Both R and Python offer robust implementations and diagnostic tools. 418 | - Always check assumptions and perform diagnostics to ensure the validity of your model. 419 | - Consider more advanced techniques (e.g., multiple regression, polynomial regression) for complex relationships. -------------------------------------------------------------------------------- /tutorials/demo-lab.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Exploring Palmer Penguins" 3 | subtitle: "Demonstration with Quarto Live" 4 | format: live-html 5 | engine: knitr 6 | toc: true 7 | webr: 8 | packages: 9 | - palmerpenguins 10 | - dplyr 11 | - ggplot2 12 | pyodide: 13 | packages: 14 | - pandas 15 | - seaborn 16 | - scipy 17 | --- 18 | 19 | {{< include ../_extensions/r-wasm/live/_knitr.qmd >}} 20 | 21 | ## Introduction 22 | 23 | Welcome to this interactive lab where we'll explore data science concepts using the Palmer Penguins dataset. We'll work with both R and Python to learn: 24 | 25 | - Data exploration and visualization 26 | - Basic statistical analysis 27 | - Data manipulation and transformation 28 | - Creating publication-quality plots 29 | 30 | The Palmer Penguins dataset contains measurements from three penguin species observed on three islands in the Palmer Archipelago, Antarctica. 31 | 32 | ## Getting Started with R 33 | 34 | Let's first load our required R packages and examine the data: 35 | 36 | ```{webr} 37 | library(palmerpenguins) 38 | library(dplyr) 39 | library(ggplot2) 40 | 41 | # Take a look at the first few rows 42 | head(penguins) 43 | ``` 44 | 45 | ### Exercise 1: Basic Data Exploration 46 | 47 | Using R, find out: 48 | 49 | 1. How many penguins are in the dataset? 50 | 2. What are the unique species? 51 | 3. What's the average flipper length? 52 | 53 | ```{webr} 54 | #| exercise: explore_r 55 | #| caption: Data Exploration in R 56 | # Your code here: 57 | list( 58 | "Total penguins:" = _______, 59 | "Species:" = _______, 60 | "Mean flipper length:" = _______, 61 | ) 62 | ``` 63 | 64 | ```{webr} 65 | #| exercise: explore_r 66 | #| check: true 67 | 68 | if (all(c("Total penguins:", "Species:", "Mean flipper length:") %in% names(.result))) { 69 | 70 | check_obs <- .result[["Total penguins:"]] == nrow(penguins) 71 | check_species <- all(.result[["Species:"]] %in% unique(penguins$species)) 72 | check_mean <- isTRUE(all.equal(round(.result[["Mean flipper length:"]], 2), round(mean(penguins$flipper_length_mm, na.rm = TRUE), 2))) 73 | 74 | if (check_obs && check_species && check_mean) { 75 | list(correct = TRUE, message = "Great job exploring the data!") 76 | } else { 77 | list(correct = FALSE, message = "One or more answers are incorrect. Try again.") 78 | } 79 | } else { 80 | list(correct = FALSE, message = "Make sure to calculate all three requested values.") 81 | } 82 | ``` 83 | 84 | ::: {.hint exercise="explore_r"} 85 | - Use `nrow()` to count rows 86 | - Use `unique()` to find unique values 87 | - Use `mean()` with `na.rm=TRUE` to calculate means 88 | ::: 89 | 90 | ::: { .solution exercise="explore_r" } 91 | ```r 92 | list( 93 | "Total penguins:" = nrow(penguins), 94 | "Species:" = unique(penguins$species), 95 | "Mean flipper length:" = round(mean(penguins$flipper_length_mm, na.rm = TRUE), 2) 96 | ) 97 | ``` 98 | ::: 99 | 100 | ## Visualizing Data in R 101 | 102 | Let's create some plots to understand relationships in our data. 103 | 104 | ```{webr} 105 | ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g, color = species)) + 106 | geom_point(alpha = 0.7) + 107 | theme_minimal() + 108 | labs(title = "Penguin Size by Species", 109 | x = "Flipper Length (mm)", 110 | y = "Body Mass (g)") 111 | ``` 112 | 113 | ### Exercise 2: Create a Plot 114 | 115 | Create a box plot showing bill length by species and sex. Use `ggplot2` and color the boxes by sex. 116 | 117 | ```{webr} 118 | #| exercise: plot_r 119 | #| caption: Create a Box Plot 120 | ggplot(penguins, aes(x = ______, y = ______, fill = ______)) + 121 | geom_boxplot() + 122 | theme_minimal() + 123 | labs(title = "Bill Length by Species and Sex") 124 | ``` 125 | 126 | ::: {.solution exercise="plot_r"} 127 | ```{webr} 128 | #| exercise: plot_r 129 | #| solution: true 130 | ggplot(penguins, aes(x = species, y = bill_length_mm, fill = sex)) + 131 | geom_boxplot() + 132 | theme_minimal() + 133 | labs(title = "Bill Length by Species and Sex") 134 | ``` 135 | ::: 136 | 137 | 138 | 139 | ## Python Analysis 140 | 141 | Now let's switch to Python and perform similar analyses: 142 | 143 | ```{pyodide} 144 | import pandas as pd 145 | import seaborn as sns 146 | 147 | # Hot load the palmerpenguins package 148 | import micropip 149 | await micropip.install([ 150 | "palmerpenguins", 151 | "setuptools" # dependency 152 | ]) 153 | 154 | from palmerpenguins import load_penguins 155 | import matplotlib.pyplot as plt 156 | 157 | # Load the data 158 | penguins = load_penguins() 159 | penguins.head() 160 | ``` 161 | 162 | ### Exercise 3: Python Data Summary 163 | 164 | Create a summary of the numerical variables using pandas: 165 | 166 | ```{pyodide} 167 | #| setup: true 168 | #| exercise: explore_py 169 | import pandas as pd 170 | import seaborn as sns 171 | 172 | # Hot load the palmerpenguins package 173 | import micropip 174 | await micropip.install([ 175 | "palmerpenguins", 176 | "setuptools" # dependency 177 | ]) 178 | 179 | from palmerpenguins import load_penguins 180 | import matplotlib.pyplot as plt 181 | 182 | # Load the data 183 | penguins = load_penguins() 184 | penguins.head() 185 | ``` 186 | 187 | ```{pyodide} 188 | #| exercise: explore_py 189 | #| caption: Data Summary in Python 190 | # Your code here: 191 | ______ 192 | ``` 193 | 194 | ::: {.hint exercise="explore_py"} 195 | Use the pandas `describe()` method to get summary statistics 196 | ::: 197 | 198 | ::: {.solution exercise="explore_py"} 199 | ```{pyodide} 200 | #| exercise: explore_py 201 | #| solution: true 202 | penguins.describe() 203 | ``` 204 | ::: 205 | 206 | ### Exercise 4: Statistical Analysis 207 | 208 | Using Python, test if there's a significant difference in flipper length between male and female penguins: 209 | 210 | 211 | ```{pyodide} 212 | #| setup: true 213 | #| exercise: stats_py 214 | import pandas as pd 215 | import seaborn as sns 216 | 217 | # Hot load the palmerpenguins package 218 | import micropip 219 | await micropip.install([ 220 | "palmerpenguins", 221 | "setuptools" # dependency 222 | ]) 223 | 224 | from palmerpenguins import load_penguins 225 | import matplotlib.pyplot as plt 226 | 227 | # Load the data 228 | penguins = load_penguins() 229 | penguins.head() 230 | ``` 231 | 232 | 233 | ```{pyodide} 234 | #| exercise: stats_py 235 | #| caption: Statistical Test 236 | from scipy import stats 237 | 238 | # Filter out NA values 239 | males = penguins[penguins['sex'] == 'male']['flipper_length_mm'].dropna() 240 | females = penguins[penguins['sex'] == 'female']['flipper_length_mm'].dropna() 241 | 242 | # Perform t-test 243 | ______ 244 | ``` 245 | 246 | ::: {.solution exercise="stats_py"} 247 | ```{pyodide} 248 | #| exercise: stats_py 249 | #| solution: true 250 | # Perform t-test 251 | t_stat, p_val = stats.ttest_ind(males, females) 252 | print(f"T-statistic: {t_stat:.4f}") 253 | print(f"P-value: {p_val:.4f}") 254 | ``` 255 | ::: 256 | 257 | ## Visualization with Seaborn 258 | 259 | Let's create an advanced visualization using seaborn: 260 | 261 | ```{pyodide} 262 | # Set the style 263 | sns.set_style("whitegrid") 264 | 265 | # Create the plot 266 | g = sns.FacetGrid(penguins, col="species", height=5) 267 | g.map_dataframe(sns.scatterplot, x="bill_length_mm", y="bill_depth_mm", hue="sex") 268 | g.add_legend() 269 | plt.show() 270 | ``` 271 | 272 | ### Exercise 5: Create a Complex Visualization 273 | 274 | Create a violin plot showing the distribution of body mass by species, with separate plots for each sex: 275 | 276 | 277 | ```{pyodide} 278 | #| setup: true 279 | #| exercise: plot_py 280 | import pandas as pd 281 | import seaborn as sns 282 | 283 | # Hot load the palmerpenguins package 284 | import micropip 285 | await micropip.install([ 286 | "palmerpenguins", 287 | "setuptools" # dependency 288 | ]) 289 | 290 | from palmerpenguins import load_penguins 291 | import matplotlib.pyplot as plt 292 | 293 | # Load the data 294 | penguins = load_penguins() 295 | penguins.head() 296 | ``` 297 | 298 | ```{pyodide} 299 | #| exercise: plot_py 300 | #| caption: Create a Violin Plot 301 | # Your code here: 302 | plt.figure(figsize=(10, 6)) 303 | ______ 304 | plt.show() 305 | ``` 306 | 307 | ::: {.hint exercise="plot_py"} 308 | Use `sns.violinplot()` with the following parameters: 309 | 310 | - `data=penguins` 311 | - `x="species"` 312 | - `y="body_mass_g"` 313 | - `hue="sex"` 314 | ::: 315 | 316 | ::: {.solution exercise="plot_py"} 317 | ```python 318 | plt.figure(figsize=(10, 6)) 319 | sns.violinplot(data=penguins, x="species", y="body_mass_g", hue="sex") 320 | plt.title("Distribution of Body Mass by Species and Sex") 321 | plt.show() 322 | ``` 323 | ::: 324 | 325 | ## Final Challenge 326 | 327 | ### Exercise 6: Data Analysis Project 328 | 329 | Choose either R or Python to complete the following analysis: 330 | 331 | 1. Filter the data to include only complete cases (no missing values) 332 | 2. Calculate the average measurements for each species-sex combination 333 | 3. Create a visualization showing these averages 334 | 4. Add error bars showing standard error 335 | 336 | Use the code block below and refer to previous examples for guidance: 337 | 338 | ::: {.panel-tabset} 339 | 340 | ## R Version 341 | 342 | ```{webr} 343 | #| exercise: final_r 344 | #| caption: Final Project - R 345 | # Your analysis here 346 | ______ 347 | ``` 348 | 349 | ::: {.solution exercise="final_r"} 350 | ```r 351 | library(tidyverse) 352 | 353 | penguins |> 354 | drop_na()|> 355 | group_by(species, sex) |> 356 | summarise( 357 | mean_flipper = mean(flipper_length_mm), 358 | se_flipper = sd(flipper_length_mm)/sqrt(n()) 359 | ) |> 360 | ggplot(aes(x=species, y=mean_flipper, fill=sex)) + 361 | geom_bar(stat="identity", position="dodge") + 362 | geom_errorbar(aes(ymin=mean_flipper-se_flipper, 363 | ymax=mean_flipper+se_flipper), 364 | position=position_dodge(0.9), 365 | width=0.25) + 366 | theme_minimal() + 367 | labs(title="Average Flipper Length by Species and Sex", 368 | y="Flipper Length (mm)") 369 | ``` 370 | ::: 371 | 372 | ## Python Version 373 | 374 | ```{pyodide} 375 | #| exercise: final_py 376 | #| caption: Final Project - Python 377 | # Your analysis here 378 | ______ 379 | ``` 380 | 381 | ::: {.solution exercise="final_py"} 382 | ```python 383 | import numpy as np 384 | 385 | # Calculate statistics 386 | stats_df = penguins.groupby(['species', 'sex'])['flipper_length_mm'].agg(['mean', 'std', 'count']).reset_index() 387 | stats_df['se'] = stats_df['std'] / np.sqrt(stats_df['count']) 388 | 389 | # Create plot 390 | plt.figure(figsize=(10, 6)) 391 | species_list = stats_df['species'].unique() 392 | x = np.arange(len(species_list)) 393 | width = 0.35 394 | 395 | plt.bar(x - width/2, stats_df[stats_df['sex']=='male']['mean'], 396 | width, label='Male', yerr=stats_df[stats_df['sex']=='male']['se']) 397 | plt.bar(x + width/2, stats_df[stats_df['sex']=='female']['mean'], 398 | width, label='Female', yerr=stats_df[stats_df['sex']=='female']['se']) 399 | 400 | plt.xlabel('Species') 401 | plt.ylabel('Flipper Length (mm)') 402 | plt.title('Average Flipper Length by Species and Sex') 403 | plt.xticks(x, species_list) 404 | plt.legend() 405 | plt.show() 406 | ``` 407 | ::: 408 | 409 | ::: 410 | 411 | ## Conclusion 412 | 413 | In this lab, we've: 414 | 415 | - Explored the Palmer Penguins dataset using both R and Python 416 | - Created various types of visualizations 417 | - Performed basic statistical analyses 418 | - Learned about data manipulation techniques 419 | 420 | For more information about the dataset, visit the [Palmer Penguins website](https://allisonhorst.github.io/palmerpenguins/). --------------------------------------------------------------------------------