├── .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 |
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, "')
172 | else
173 | quarto.log.error("Error: Unable to detect file type from the audio file path.")
174 | assert(false)
175 | end
176 |
177 | -- Add source element for browsers that do not support the audio tag
178 | table.insert(htmlTable, 'Your browser does not support the audio tag.')
179 |
180 | -- Add closing audio tag
181 | 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/).
--------------------------------------------------------------------------------