-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
97993db
commit 1004cdd
Showing
10 changed files
with
1,280 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/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/[email protected]/min/vs' | ||
} | ||
}); | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.