Skip to content

Commit

Permalink
Expose hash-pipe style cell options (#141)
Browse files Browse the repository at this point in the history
* Expose hash-pipe style cell options

* Add code cell option documentation

* Add release note

* Add code cell initial support demo

* Remove debug statement

* Remove quotations around values to avoid a mismatch in JS

* Switch demo to using panel-tabset with language to jump between active code and authoring code

* Title and code cell demo
  • Loading branch information
coatless authored Jan 30, 2024
1 parent e5f278c commit 25ec63e
Show file tree
Hide file tree
Showing 13 changed files with 532 additions and 21 deletions.
2 changes: 1 addition & 1 deletion _extensions/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.4.0-dev.7
version: 0.4.0-dev.8
quarto-required: ">=1.2.198"
contributes:
filters:
Expand Down
2 changes: 1 addition & 1 deletion _extensions/webr/qwebr-cell-initialization.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ qwebrInstance.then(
switch (evalType) {
case 'output':
// Run the code in a non-interactive state that is geared to displaying output
await qwebrExecuteCode(`${cellCode}`, qwebrCounter, EvalTypes.Output);
await qwebrExecuteCode(`${cellCode}`, qwebrCounter, entry.options);
break;
case 'setup':
const activeDiv = document.getElementById(`qwebr-noninteractive-setup-area-${qwebrCounter}`);
Expand Down
59 changes: 48 additions & 11 deletions _extensions/webr/qwebr-compute-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ globalThis.qwebrEscapeHTMLCharacters = function(unsafe) {
.replace(/'/g, "'");
};

// Passthrough results
globalThis.qwebrIdentity = function(x) {
return x;
};

// Function to parse the pager results
globalThis.qwebrParseTypePager = async function (msg) {

Expand Down Expand Up @@ -62,13 +67,26 @@ globalThis.qwebrComputeEngine = async function(
// Create a pager variable for help/file contents
let pager = [];

// Handle how output is processed
let processOutput;

if (options.results == "markup") {
processOutput = qwebrEscapeHTMLCharacters;
} else {
processOutput = qwebrIdentity;
}

// ----
// Convert from Inches to Pixels by using DPI (dots per inch)
// for bitmap devices (dpi * inches = pixels)
let fig_width = options["fig-width"] * options["dpi"]
let fig_height = options["fig-height"] * options["dpi"]

// Initialize webR
await mainWebR.init();

// Setup a webR canvas by making a namespace call into the {webr} package
await mainWebR.evalRVoid(`webr::canvas(width=${options["fig-width"]}, height=${options["fig-height"]})`);
await mainWebR.evalRVoid(`webr::canvas(width=${fig_width}, height=${fig_height})`);

const result = await mainWebRCodeShelter.captureR(codeToRun, {
withAutoprint: true,
Expand All @@ -80,17 +98,26 @@ globalThis.qwebrComputeEngine = async function(
// -----

// Start attempting to parse the result data
try {
processResultOutput:try {

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

// Avoid running through output processing
if (options.results === "hide") {
break processResultOutput;
}

// Merge output streams of STDOUT and STDErr (messages and errors are combined.)
// Require both `warning` and `message` to be true to display `STDErr`.
const out = result.output
.filter(evt => evt.type === "stdout" || evt.type === "stderr")
.filter(
evt => evt.type === "stdout" ||
( evt.type === "stderr" && (options.warning === "true" && options.message === "true"))
)
.map((evt, index) => {
const className = `qwebr-output-code-${evt.type}`;
return `<code id="${className}-editor-${elements.id}-result-${index + 1}" class="${className}">${qwebrEscapeHTMLCharacters(evt.data)}</code>`;
return `<code id="${className}-editor-${elements.id}-result-${index + 1}" class="${className}">${processOutput(evt.data)}</code>`;
})
.join("\n");

Expand All @@ -109,11 +136,15 @@ globalThis.qwebrComputeEngine = async function(
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 * options["fig-width"]);
canvas.setAttribute("height", 2 * options["fig-height"]);
canvas.style.width = "700px";
canvas.setAttribute("width", 2 * fig_width);
canvas.setAttribute("height", 2 * fig_height);
canvas.style.width = options["out-width"] ? options["out-width"] : `${fig_width}px`;
if (options["out-height"]) {
canvas.style.height = options["out-height"];
}
canvas.style.display = "block";
canvas.style.margin = "auto";
}
Expand Down Expand Up @@ -173,12 +204,18 @@ globalThis.qwebrComputeEngine = async function(
globalThis.qwebrExecuteCode = async function (
codeToRun,
id,
evalType = EvalTypes.Interactive,
options = {}) {

// If options are not passed, we fall back on the bare minimum to handle the computation
if (qwebrIsObjectEmpty(options)) {
options = { "fig-width": 504, "fig-height": 360 };
options = {
"context": "interactive",
"fig-width": 7, "fig-height": 5,
"out-width": "700px", "out-height": "",
"dpi": 72,
"results": "markup",
"warning": "true", "message": "true",
};
}

// Next, we access the compute areas values
Expand All @@ -194,7 +231,7 @@ globalThis.qwebrExecuteCode = async function (
btn.disabled = true;
});

if (evalType == EvalTypes.Interactive) {
if (options.context == EvalTypes.Interactive) {
// Emphasize the active code cell
elements.runButton.innerHTML = '<i class="fa-solid fa-spinner fa-spin qwebr-icon-status-spinner"></i> <span>Run Code</span>';
}
Expand All @@ -207,7 +244,7 @@ globalThis.qwebrExecuteCode = async function (
btn.disabled = false;
});

if (evalType == EvalTypes.Interactive) {
if (options.context == EvalTypes.Interactive) {
// Revert to the initial code cell state
elements.runButton.innerHTML = '<i class="fa-solid fa-play qwebr-icon-run-code"></i> <span>Run Code</span>';
}
Expand Down
13 changes: 7 additions & 6 deletions _extensions/webr/qwebr-monaco-editor-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ globalThis.qwebrCreateMonacoEditorInstance = function (cellData) {

const initialCode = cellData.code;
const qwebrCounter = cellData.id;
const qwebrOptions = cellData.options;

// Retrieve the previously created document elements
let runButton = document.getElementById(`qwebr-button-run-${qwebrCounter}`);
Expand Down Expand Up @@ -36,8 +37,9 @@ globalThis.qwebrCreateMonacoEditorInstance = function (cellData) {
// Store the official div container ID
editor.__qwebrEditorId = `qwebr-editor-${qwebrCounter}`;

// Store the initial code value
// Store the initial code value and options
editor.__qwebrinitialCode = initialCode;
editor.__qwebrOptions = qwebrOptions;

// Set at the model level the preferred end of line (EOL) character to LF.
// This prevent `\r\n` from being given to the webR engine if the user is on Windows.
Expand Down Expand Up @@ -79,7 +81,7 @@ globalThis.qwebrCreateMonacoEditorInstance = function (cellData) {
editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => {

// Retrieve all text inside the editor
qwebrExecuteCode(editor.getValue(), editor.__qwebrCounter);
qwebrExecuteCode(editor.getValue(), editor.__qwebrCounter, editor.__qwebrOptions);
});

// Add a keydown event listener for CMD/Ctrl+Enter to run selected code
Expand Down Expand Up @@ -108,14 +110,13 @@ globalThis.qwebrCreateMonacoEditorInstance = function (cellData) {
}

// Run the entire line of code.
qwebrExecuteCode(currentLine, editor.__qwebrCounter,
EvalTypes.Interactive);
qwebrExecuteCode(currentLine, editor.__qwebrCounter, editor.__qwebrOptions);

// Move cursor to new position
editor.setPosition(newPosition);
} else {
// Code to run when Ctrl+Enter is pressed with selected code
qwebrExecuteCode(selectedText, editor.__qwebrCounter, EvalTypes.Interactive);
qwebrExecuteCode(selectedText, editor.__qwebrCounter, editor.__qwebrOptions);
}
});
}
Expand All @@ -140,7 +141,7 @@ globalThis.qwebrCreateMonacoEditorInstance = function (cellData) {

// Add a click event listener to the run button
runButton.onclick = function () {
qwebrExecuteCode(editor.getValue(), editor.__qwebrCounter, EvalTypes.Interactive);
qwebrExecuteCode(editor.getValue(), editor.__qwebrCounter, editor.__qwebrOptions);
};

// Add a click event listener to the reset button
Expand Down
13 changes: 12 additions & 1 deletion _extensions/webr/webr.lua
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,15 @@ local qwebrCapturedCodeBlocks = {}

-- Initialize a table that contains the default cell-level options
local qwebRDefaultCellOptions = {
["context"] = "interactive"
["context"] = "interactive",
["warning"] = "true",
["message"] = "true",
["results"] = "markup",
["dpi"] = 72,
["fig-width"] = 7,
["fig-height"] = 5,
["out-width"] = "700px",
["out-height"] = ""
}

----
Expand Down Expand Up @@ -102,6 +110,9 @@ local function mergeCellOptions(localOptions)

-- Override default options with local options
for key, value in pairs(localOptions) do
if type(value) == "string" then
value = value:gsub("[\"']", "")
end
mergedOptions[key] = value
end

Expand Down
4 changes: 3 additions & 1 deletion docs/_quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ website:
- href: qwebr-first-steps.qmd
text: Your first webR-powered Quarto document
- href: qwebr-meta-options.qmd
text: Customization Options
text: Document Options
- href: qwebr-cell-options.qmd
text: Code Cell Options
- href: qwebr-using-r-packages.qmd
text: Using R Packages
- href: qwebr-internal-cell.qmd
Expand Down
Loading

0 comments on commit 25ec63e

Please sign in to comment.