Skip to content

Commit

Permalink
Hidden code cells (#30)
Browse files Browse the repository at this point in the history
* Bump development version

* Add example engine tag usage into the webR demo document.

* Only block top-level webr HTML generated example documents from being included.

* Block CSVs as well

* Add logic to support an internal code cell.

Close Feature Proposal: Internal Code Cells #11 and Preload objects/functions #25.

* Add a demo document describing the internal cell options

* Incorporate the example in Preload objects/functions #25
  • Loading branch information
coatless authored Sep 14, 2023
1 parent deb3cf6 commit 6fba5f6
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 7 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
*_files
webr-worker.js
webr-serviceworker.js
webr-*.html
/webr-*.html
*.csv
5 changes: 5 additions & 0 deletions _extensions/webr/webr-context-internal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script type="module">
await globalThis.webR.evalRVoid(`
{{WEBRCODE}}
`)
</script>
68 changes: 63 additions & 5 deletions _extensions/webr/webr.lua
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,20 @@ function editorTemplateFile()
return readTemplateFile("webr-editor.html")
end

-- Obtain the internal template file at webr-context-internal.html
function internalTemplateFile()
return readTemplateFile("webr-context-internal.html")
end

-- Obtain the initialization template file at webr-init.html
function initializationTemplateFile()
return readTemplateFile("webr-init.html")
end

-- Cache a copy of the template to avoid multiple read/writes.
-- Cache a copy of each public-facing templates to avoid multiple read/writes.
editor_template = editorTemplateFile()

internal_template = internalTemplateFile()
----

-- Define a function that escape control sequence
Expand Down Expand Up @@ -302,6 +309,44 @@ function substitute_in_file(contents, substitutions)
return contents
end

-- Extract Quarto code cell options from the block's text
function extractCodeBlockOptions(block)

-- Access the text aspect of the code block
local code = block.text

-- Define two local tables:
-- the block's attributes
-- the block's code lines
local newAttributes = {}
local newCodeLines = {}

-- Iterate over each line in the code block
for line in code:gmatch("[^\r\n]+") do
-- Check if the line starts with "#|" and extract the key-value pairing
-- e.g. #| key: value goes to newAttributes[key] -> value
local key, value = line:match("^#|%s*(.-):%s*(.-)%s*$")

-- If a special comment is found, then add the key-value pairing to the newAttributes table
if key and value then
newAttributes[key] = value
else
-- Otherwise, it's not a special comment, keep the code line
table.insert(newCodeLines, line)
end
end

-- Set the new attributes for the code block
block.attributes = newAttributes

-- Set the codeblock text to exclude the special comments.
block.text = table.concat(newCodeLines, '\n')

-- Return the full block
return block
end

-- Replace the code cell with a webR editor
function enableWebRCodeCell(el)

-- Let's see what's going on here:
Expand Down Expand Up @@ -336,6 +381,9 @@ function enableWebRCodeCell(el)
-- Modify the counter variable each time this is run to create
-- unique code cells
counter = counter + 1

-- Convert webr-specific option commands into attributes
el = extractCodeBlockOptions(el)

-- 7 is the default height and width for knitr. But, that doesn't translate to pixels.
-- So, we have 504 and 360 respectively.
Expand All @@ -348,14 +396,24 @@ function enableWebRCodeCell(el)
["WEBRCODE"] = escapeControlSequences(el.text)
}

-- Make sure we perform a copy
local copied_editor_template = editor_template
-- Retrieve the newly defined attributes
local cell_context = el.attributes.context

-- Decide the correct template
-- Make sure we perform a copy of each template
local copied_code_template = nil
if is_variable_empty(cell_context) then
copied_code_template = editor_template
else
copied_code_template = internal_template
end

-- Make the necessary substitutions
local webr_enabled_code_cell = substitute_in_file(copied_editor_template, substitutions)
-- Make the necessary substitutions into the template
local webr_enabled_code_cell = substitute_in_file(copied_code_template, substitutions)

-- Return the modified HTML template as a raw cell
return pandoc.RawInline('html', webr_enabled_code_cell)

end
end
-- Allow for a pass through in other languages
Expand Down
3 changes: 2 additions & 1 deletion update-quarto-dev.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
QUARTO_VERSION="1.4.349"
QUARTO_VERSION="1.4.358"

wget -O quarto-latest.deb https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-amd64.deb

sudo dpkg -i ./quarto-latest.deb

rm quarto-latest.deb

9 changes: 9 additions & 0 deletions webr-demo.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ webR-enabled code cell are established by using `{webr-r}` in a Quarto HTML docu
1 + 1
```

For instance, the above `webr`-enabled code cell was created by typing into the Quarto document:

```{{webr-r}}
1 + 1
```

## Sample cases

### Fit a linear regression model
Expand Down Expand Up @@ -98,12 +104,15 @@ add_one <- \(x) x + 1
add_one(2)
```



### Empty code cell

```{webr-r}
```


### Prior code cell

```{webr}
Expand Down
149 changes: 149 additions & 0 deletions webr-internal-cell.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
---
title: "Hidden webR code cells"
format:
html:
toc: true
engine: knitr
filters:
- webr
---

## Hidden Evaluation without Output

In this example, we create a hidden setup code cell within the document by using the special comment of `#| context: setup`. The setup code cell executes code in the background and _does not_ display the code or its output.

```{{webr-r}}
#| context: setup
meaning_of_life = 42
```

Thus, we have pre-loaded the `meaning_of_life` variable. So, if we run the next code cell, then we will see the value of `meaning_of_life` being displayed as `42` instead of an error.

```{webr-r}
#| context: setup
meaning_of_life = 42
```


```{webr-r}
meaning_of_life
```

::: {.callout-caution}
Be advised that the contents of the hidden code cell is displayed if the web page's source is viewed.
:::

## Hidden Loading of a Dataset

Outside of just specifying a single variable, we can use the setup hidden code cell to pre-load and wrangle an entire data set. This allows for students to directly interact with a loaded data set.

```{{webr-r}}
#| context: hidden
# Download a data set
download.file(
'https://raw.githubusercontent.com/coatless/raw-data/main/penguins.csv',
'penguins.csv'
)
# Read data
df = read.csv("penguins.csv")
```

```{webr-r}
#| context: setup
# Download a data set
download.file(
'https://raw.githubusercontent.com/coatless/raw-data/main/penguins.csv',
'penguins.csv'
)
# Read data
df = read.csv("penguins.csv")
```

```{webr-r}
# Show the head of the data
head(df)
```

::: {.callout-note}
If the setup code needs the present of R packages, we suggest specifying the required packages in the document's YAML. This option communicates to the end user that the webpage is not yet ready to explore through a clear status update at the top. For example, we could add `dplyr` and `ggplot2` using:

```yaml
---
webr:
packages: ['ggplot2', 'dplyr']
---
```
:::

## Hidden Solution Checking of Student Work

:::{.callout-warning}
Be advised that any solution written into a webR hidden code cell can be obtained by viewing the document's HTML source. **Please _avoid_ using this option for formal assessment (exams, quizzes, homework, ...).**
:::

Lastly, the webR document can be used to check student answers. We can make available an answer key and a comparison function within the document.

For instance, the solution data frame might look a bit like:

```{{webr-r}}
#| context: hidden
answer_frame <- data.frame(
problem = c("1a", "1b", "2"),
answer = c(10, 2, 3/16),
tol = c(0.001, 0, 1/32)
)
```

Next, we can define an internal check function like so:

```{{webr-r}}
#| context: hidden
check <- function(problem, answer) {
aframe <- answer_frame
if(!problem %in% aframe$problem) stop(paste0("Please enter a valid problem. (", paste0(aframe$problem, collapse = ","), ")"))
solution <- aframe[which(aframe$problem == problem), "answer"]
ifelse(
all.equal(answer, solution, tolerance = 0.001) == TRUE,
"Correct! Well done.",
"Incorrect! Good attempt. Let's try again?")
}
```

```{webr-r}
#| context: hidden
answer_frame <- data.frame(
problem = c("1a", "1b", "2"),
answer = c(10, 2, 3/16),
tol = c(0.001, 0, 1/32)
)
check <- function(problem, answer) {
aframe <- answer_frame
if(!problem %in% aframe$problem) stop(paste0("Please enter a valid problem. (", paste0(aframe$problem, collapse = ","), ")"))
solution <- aframe[which(aframe$problem == problem), "answer"]
ifelse(
all.equal(answer, solution, tolerance = 0.001) == TRUE,
"Correct! Well done.",
"Incorrect! Good attempt. Let's try again?")
}
```

Students can then compare their answers to the answer key by using the `check()` function.

Consider the question:

> What is 9 + 1?
```{webr-r}
student_solution = 11
check(problem = "1a", answer = student_solution)
```

0 comments on commit 6fba5f6

Please sign in to comment.