Skip to content

Commit

Permalink
Added quarto-webr output files
Browse files Browse the repository at this point in the history
  • Loading branch information
tomsing1 committed Nov 18, 2023
1 parent 97880f1 commit cd157f1
Show file tree
Hide file tree
Showing 32 changed files with 8,241 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: webr
title: Embedded webr code cells
author: James Joseph Balamuta
version: 0.4.0-dev.1
quarto-required: ">=1.2.198"
contributes:
filters:
- webr.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.43.0/min/vs/loader.js"></script>
<script type="module" id="webr-monaco-editor-init">

// Configure the Monaco Editor's loader
require.config({
paths: {
'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.43.0/min/vs'
}
});
</script>
31 changes: 31 additions & 0 deletions docs/posts/quarto-webr/_extensions/coatless/webr/template.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
title: "WebR-enabled code cell"
format: html
engine: knitr
#webr:
# show-startup-message: false # Display status of webR initialization
# show-header-message: false # Check to see if COOP&COEP headers are set for speed.
# packages: ['ggplot2', 'dplyr'] # Pre-install dependencies
# home-dir: "/home/rstudio" # Customize where the working directory is
# base-url: '' # Base URL used for downloading R WebAssembly binaries
# service-worker-url: '' # URL from where to load JavaScript worker scripts when loading webR with the ServiceWorker communication channel.
filters:
- webr
---

## Demo

This is a webr-enabled code cell in a Quarto HTML document.

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

```{webr-r}
fit = lm(mpg ~ am, data = mtcars)
summary(fit)
```

```{webr-r}
plot(pressure)
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
<div id="qwebr-interactive-area-{{WEBRCOUNTER}}" class="qwebr-interactive-area">
<button class="btn btn-default qwebr-button-run" disabled type="button" id="qwebr-button-run-{{WEBRCOUNTER}}">🟡 Loading
webR...</button>
<div id="qwebr-console-area-{{WEBRCOUNTER}}" class="qwebr-console-area">
<div id="qwebr-editor-{{WEBRCOUNTER}}" class="qwebr-editor"></div>
<div id="qwebr-output-code-area-{{WEBRCOUNTER}}" class="qwebr-output-code-area" aria-live="assertive">
<pre style="visibility: hidden"></pre>
</div>
</div>
<div id="qwebr-output-graph-area-{{WEBRCOUNTER}}" class="qwebr-output-graph-area">
</div>
</div>
<script type="module">
// Retrieve webR code cell information
const runButton = document.getElementById("qwebr-button-run-{{WEBRCOUNTER}}");
const outputCodeDiv = document.getElementById("qwebr-output-code-area-{{WEBRCOUNTER}}");
const editorDiv = document.getElementById("qwebr-editor-{{WEBRCOUNTER}}");
const outputGraphDiv = document.getElementById("qwebr-output-graph-area-{{WEBRCOUNTER}}");

// Load the Monaco Editor and create an instance
let editor;
require(['vs/editor/editor.main'], function () {
editor = monaco.editor.create(editorDiv, {
value: `{{WEBRCODE}}`,
language: 'r',
theme: 'vs-light',
automaticLayout: true, // TODO: Could be problematic for slide decks
scrollBeyondLastLine: false,
minimap: {
enabled: false
},
fontSize: '17.5pt', // Bootstrap is 1 rem
renderLineHighlight: "none", // Disable current line highlighting
hideCursorInOverviewRuler: true // Remove cursor indictor in right hand side scroll bar
});

// Dynamically modify the height of the editor window if new lines are added.
let ignoreEvent = false;
const updateHeight = () => {
const contentHeight = editor.getContentHeight();
// We're avoiding a width change
//editorDiv.style.width = `${width}px`;
editorDiv.style.height = `${contentHeight}px`;
try {
ignoreEvent = true;

// The key to resizing is this call
editor.layout();
} finally {
ignoreEvent = false;
}
};

// Helper function to check if selected text is empty
function isEmptyCodeText(selectedCodeText) {
return (selectedCodeText === null || selectedCodeText === undefined || selectedCodeText === "");
}

// Registry of keyboard shortcuts that should be re-added to each editor window
// when focus changes.
const addWebRKeyboardShortCutCommands = () => {
// Add a keydown event listener for Shift+Enter to run all code in cell
editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => {

// Retrieve all text inside the editor
executeCode(editor.getValue());
});

// Add a keydown event listener for CMD/Ctrl+Enter to run selected code
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {

// Get the selected text from the editor
const selectedText = editor.getModel().getValueInRange(editor.getSelection());
// Check if no code is selected
if (isEmptyCodeText(selectedText)) {
// Obtain the current cursor position
let currentPosition = editor.getPosition();
// Retrieve the current line content
let currentLine = editor.getModel().getLineContent(currentPosition.lineNumber);

// Propose a new position to move the cursor to
let newPosition = new monaco.Position(currentPosition.lineNumber + 1, 1);

// Check if the new position is beyond the last line of the editor
if (newPosition.lineNumber > editor.getModel().getLineCount()) {
// Add a new line at the end of the editor
editor.executeEdits("addNewLine", [{
range: new monaco.Range(newPosition.lineNumber, 1, newPosition.lineNumber, 1),
text: "\n",
forceMoveMarkers: true,
}]);
}

// Run the entire line of code.
executeCode(currentLine);

// Move cursor to new position
editor.setPosition(newPosition);
} else {
// Code to run when Ctrl+Enter is pressed with selected code
executeCode(selectedText);
}
});
}

// Register an on focus event handler for when a code cell is selected to update
// what keyboard shortcut commands should work.
// This is a workaround to fix a regression that happened with multiple
// editor windows since Monaco 0.32.0
// https://github.com/microsoft/monaco-editor/issues/2947
editor.onDidFocusEditorText(addWebRKeyboardShortCutCommands);

// Register an on change event for when new code is added to the editor window
editor.onDidContentSizeChange(updateHeight);

// Manually re-update height to account for the content we inserted into the call
updateHeight();
});

// Function to execute the code (accepts code as an argument)
async function executeCode(codeToRun) {

// Disallowing execution of other code cells
document.querySelectorAll(".qwebr-button-run").forEach((btn) => {
btn.disabled = true;
});

// Emphasize the active code cell
runButton.innerHTML = '<i class="fa-solid fa-spinner fa-spin qwebr-icon-status-spinner"></i> <span>Run Code</span>';

// Create a canvas variable for graphics
let canvas = undefined;

// Create a pager variable for help/file contents
let pager = [];

// Process
async function parseTypePager(msg) {

// Split out the event data
const { path, title, deleteFile } = msg.data;

// Process the pager data by reading the information from disk
const paged_data = await webR.FS.readFile(path).then((data) => {
// Obtain the file content
let content = new TextDecoder().decode(data);

// Remove excessive backspace characters until none remain
while(content.match(/.[\b]/)){
content = content.replace(/.[\b]/g, '');
}

// Returned cleaned data
return content;
});

// Unlink file if needed
if (deleteFile) {
await webR.FS.unlink(path);
}

// Return extracted data with spaces
return paged_data;
}

// Initialize webR
await webR.init();

// Setup a webR canvas by making a namespace call into the {webr} package
await webR.evalRVoid("webr::canvas(width={{WIDTH}}, height={{HEIGHT}})");

// Capture output data from evaluating the code
const result = await webRCodeShelter.captureR(codeToRun, {
withAutoprint: true,
captureStreams: true,
captureConditions: false//,
// env: webR.objs.emptyEnv, // maintain a global environment for webR v0.2.0
});

// Start attempting to parse the result data
try {

// Stop creating images
await webR.evalRVoid("dev.off()");

// Merge output streams of STDOUT and STDErr (messages and errors are combined.)
const out = result.output
.filter(evt => evt.type === "stdout" || evt.type === "stderr")
.map((evt, index) => {
const className = `qwebr-output-code-${evt.type}`;
return `<code id="${className}-editor-{{WEBRCOUNTER}}-result-${index + 1}" class="${className}">${evt.data}</code>`;
})
.join("\n");


// Clean the state
const msgs = await webR.flush();

// Output each image event stored
msgs.forEach((msg) => {
// Determine if old canvas can be used or a new canvas is required.
if (msg.type === 'canvas'){
// Add image to the current canvas
if (msg.data.event === 'canvasImage') {
canvas.getContext('2d').drawImage(msg.data.image, 0, 0);
} else if (msg.data.event === 'canvasNewPage') {
// Generate a new canvas element
canvas = document.createElement("canvas");
canvas.setAttribute("width", 2 * {{WIDTH}});
canvas.setAttribute("height", 2 * {{HEIGHT}});
canvas.style.width = "700px";
canvas.style.display = "block";
canvas.style.margin = "auto";
}
}
});

// Use `map` to process the filtered "pager" events asynchronously
const pager = await Promise.all(
msgs.filter(msg => msg.type === 'pager').map(
async (msg) => {
return await parseTypePager(msg);
}
)
);

// Nullify the output area of content
outputCodeDiv.innerHTML = "";
outputGraphDiv.innerHTML = "";

// Design an output object for messages
const pre = document.createElement("pre");
if (/\S/.test(out)) {
// Display results as HTML elements to retain output styling
const div = document.createElement("div");
div.innerHTML = out;
pre.appendChild(div);
} else {
// If nothing is present, hide the element.
pre.style.visibility = "hidden";
}
outputCodeDiv.appendChild(pre);

// Place the graphics on the canvas
if (canvas) {
outputGraphDiv.appendChild(canvas);
}

// Display the pager data
if (pager) {
// Use the `pre` element to preserve whitespace.
pager.forEach((paged_data, index) => {
let pre_pager = document.createElement("pre");
pre_pager.innerText = paged_data;
pre_pager.classList.add("qwebr-output-code-pager");
pre_pager.setAttribute("id", "qwebr-output-code-pager-editor-{{WEBRCOUNTER}}-result-" + (index + 1));
outputCodeDiv.appendChild(pre_pager);
});
}
} finally {
// Clean up the remaining code
webRCodeShelter.purge();
}

// Switch to allowing execution of code
document.querySelectorAll(".qwebr-button-run").forEach((btn) => {
btn.disabled = false;
});

// Revert to the initial code cell state
runButton.innerHTML = '<i class="fa-solid fa-play qwebr-icon-run-code"></i> <span>Run Code</span>';
}

// Add a click event listener to the run button
runButton.onclick = function () {
executeCode(editor.getValue());
};
</script>
Loading

0 comments on commit cd157f1

Please sign in to comment.