Skip to content

Commit

Permalink
Merge pull request #4 from coatless-quarto-webr/upgrade-quarto-webr-e…
Browse files Browse the repository at this point in the history
…xtension-0-3-6

Upgrade quarto-webr from 0.3.0 to 0.3.6
  • Loading branch information
JamesHWade authored Sep 25, 2023
2 parents aa4a0c9 + 0e0dc21 commit 787e3e3
Show file tree
Hide file tree
Showing 14 changed files with 513 additions and 56 deletions.
2 changes: 1 addition & 1 deletion _extensions/coatless/webr/_extension.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: webr
title: Embedded webr code cells
author: James Joseph Balamuta
version: 0.3.0
version: 0.3.6
quarto-required: ">=1.2.198"
contributes:
filters:
Expand Down
10 changes: 10 additions & 0 deletions _extensions/coatless/webr/monaco-editor-init.html
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>
204 changes: 204 additions & 0 deletions _extensions/coatless/webr/webr-context-interactive.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<button class="btn btn-default btn-webr" disabled type="button" id="webr-run-button-{{WEBRCOUNTER}}">Loading
webR...</button>
<div id="webr-editor-{{WEBRCOUNTER}}"></div>
<div id="webr-code-output-{{WEBRCOUNTER}}" aria-live="assertive">
<pre style="visibility: hidden"></pre>
</div>
<script type="module">
// Retrieve webR code cell information
const runButton = document.getElementById("webr-run-button-{{WEBRCOUNTER}}");
const outputDiv = document.getElementById("webr-code-output-{{WEBRCOUNTER}}");
const editorDiv = document.getElementById("webr-editor-{{WEBRCOUNTER}}");

// Add a light grey outline around the code editor
editorDiv.style.border = "1px solid #eee";

// 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.5rem', // 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) {
// Disable run button for code cell active
runButton.disabled = true;

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

// Initialize webR
await globalThis.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) => evt.data).join("\n");

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

// Output each image 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";
}
}
});

// Nullify the outputDiv of content
outputDiv.innerHTML = "";

// Design an output object for messages
const pre = document.createElement("pre");
if (/\S/.test(out)) {
// Display results as text
const code = document.createElement("code");
code.innerText = out;
pre.appendChild(code);
} else {
// If nothing is present, hide the element.
pre.style.visibility = "hidden";
}
outputDiv.appendChild(pre);

// Place the graphics on the canvas
if (canvas) {
const p = document.createElement("p");
p.appendChild(canvas);
outputDiv.appendChild(p);
}
} finally {
// Clean up the remaining code
webRCodeShelter.purge();
runButton.disabled = false;
}
}

// Add a click event listener to the run button
runButton.onclick = function () {
executeCode(editor.getValue());
};
</script>
90 changes: 90 additions & 0 deletions _extensions/coatless/webr/webr-context-output.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<div id="webr-code-output-{{WEBRCOUNTER}}" aria-live="assertive">
<pre style="visibility: hidden"></pre>
</div>
<script type="module">
// Retrieve webR code cell information
const outputDiv = document.getElementById("webr-code-output-{{WEBRCOUNTER}}");

// Function to execute the code (accepts code as an argument)
async function executeOutputOnlyCode(codeToRun) {
// Create a canvas variable for graphics
let canvas = undefined;

// Initialize webR
await globalThis.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) => evt.data).join("\n");

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

// Output each image 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";
}
}
});

// Nullify the outputDiv of content
outputDiv.innerHTML = "";

// Design an output object for messages
const pre = document.createElement("pre");
if (/\S/.test(out)) {
// Display results as text
const code = document.createElement("code");
code.innerText = out;
pre.appendChild(code);
} else {
// If nothing is present, hide the element.
pre.style.visibility = "hidden";
}
outputDiv.appendChild(pre);

// Place the graphics on the canvas
if (canvas) {
const p = document.createElement("p");
p.appendChild(canvas);
outputDiv.appendChild(p);
}
} finally {
// Clean up the remaining code
webRCodeShelter.purge();
}
}

// Run the code
executeOutputOnlyCode(`{{WEBRCODE}}`)
</script>
9 changes: 9 additions & 0 deletions _extensions/coatless/webr/webr-context-setup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script type="module">
// Initialization WebR
await globalThis.webR.init();

// Run R code without focusing on storing data.
await globalThis.webR.evalRVoid(`
{{WEBRCODE}}
`)
</script>
Loading

0 comments on commit 787e3e3

Please sign in to comment.