Skip to content

Commit

Permalink
Cell state manager (#120)
Browse files Browse the repository at this point in the history
* Remove height and width from cell

* Positive conditions

* Remove HTML views

* Lay the ground work inside the Lua filter for the insertion point logic.

* Add logic that handles removing empty space after code options

* Hand off cell data into JS

* Add default cell options

* Inject the cell information at the end of the document

* Simplify if condition inside of cell

* More compartmentalization. Switch to using a dynamic import.

* Remove unused version check

* Aim to do only one configuration substitution instead of two. Re-arrange eval definition.

* Suppress HTML from entrying the examples/ directory

* Clean up generic HTML element creation tool

* Test out adding interactive cells on the page.

* Associate cellData instead of init code & qwebR counter information with MonacoEditor initialization

* Re-add status helpers

* Add interactive cell unlock

* Handle cell evaluation for non-interactive information

* Add some more proofing logic

* Restore install/load inside of engine initialization

* :'(

* Update status when being worked on...

* Add new test case

* Add a tibble...

* Mirror other options

* Bump version

* Rename for non-interactive tests

* Add a test with just output

* Add another test on the non-interactive side

* Improve the rigor of the test...

* Ensure each result is evaluated asynchronously in sequence by opting for a for() loop

* Add another longer form test document showing chained setup and output contexts

* Add non-interactive area visualization

* Smaller tweaks to propagate status updates

* Mirror the demo in the test file

* Add some release notes
  • Loading branch information
coatless authored Jan 18, 2024
1 parent 1250474 commit c9fd938
Show file tree
Hide file tree
Showing 22 changed files with 753 additions and 2,249 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ webr-serviceworker.js
/.luarc.json
docs/_site/*
tests/_site/*
examples/*/*.html
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.4
version: 0.4.0-dev.5
quarto-required: ">=1.2.198"
contributes:
filters:
Expand Down
80 changes: 72 additions & 8 deletions _extensions/webr/qwebr-cell-elements.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,42 @@
// Supported Evaluation Types for Context
globalThis.EvalTypes = Object.freeze({
Interactive: 'interactive',
Setup: 'setup',
Output: 'output',
});

// Function that dispatches the creation request
globalThis.qwebrCreateHTMLElement = function (insertElement,
qwebrCounter,
evalType = EvalTypes.Interactive,
options = {}) {
globalThis.qwebrCreateHTMLElement = function (
cellData
) {

// Extract key components
const evalType = cellData.options.context;
const qwebrCounter = cellData.id;

// We make an assumption that insertion points are defined by the Lua filter as:
// qwebr-insertion-location-{qwebrCounter}
const elementLocator = document.getElementById(`qwebr-insertion-location-${qwebrCounter}`);

// Figure out the routine to use to insert the element.
let qwebrElement;
switch ( evalType ) {
case EvalTypes.Interactive:
case EvalTypes.Interactive:
qwebrElement = qwebrCreateInteractiveElement(qwebrCounter);
break;
case EvalTypes.Output:
qwebrElement = qwebrCreateNonInteractiveOutputElement(qwebrCounter);
break;
case EvalTypes.Setup:
qwebrElement = qwebrCreateNonInteractiveSetupElement(qwebrCounter);
break;
default:
qwebrElement = document.createElement('div');
qwebrElement.textContent = 'Error creating element';
qwebrElement.textContent = 'Error creating `quarto-webr` element';
}

// Insert the dynamically generated object at the document location.
insertElement.appendChild(qwebrElement);
elementLocator.appendChild(qwebrElement);
};

// Function that setups the interactive element creation
Expand Down Expand Up @@ -81,6 +98,9 @@ globalThis.qwebrCreateNonInteractiveOutputElement = function(qwebrCounter) {
mainDiv.id = 'qwebr-noninteractive-area-' + qwebrCounter;
mainDiv.className = 'qwebr-noninteractive-area';

// Create a status container div
var statusContainer = createLoadingContainer(qwebrCounter);

// Create output code area div
var outputCodeAreaDiv = document.createElement('div');
outputCodeAreaDiv.id = 'qwebr-output-code-area-' + qwebrCounter;
Expand All @@ -98,6 +118,7 @@ globalThis.qwebrCreateNonInteractiveOutputElement = function(qwebrCounter) {
outputGraphAreaDiv.className = 'qwebr-output-graph-area';

// Append all elements to the main div
mainDiv.appendChild(statusContainer);
mainDiv.appendChild(outputCodeAreaDiv);
mainDiv.appendChild(outputGraphAreaDiv);

Expand All @@ -108,8 +129,51 @@ globalThis.qwebrCreateNonInteractiveOutputElement = function(qwebrCounter) {
globalThis.qwebrCreateNonInteractiveSetupElement = function(qwebrCounter) {
// Create main div element
var mainDiv = document.createElement('div');
mainDiv.id = 'qwebr-noninteractive-setup-area-' + qwebrCounter;
mainDiv.id = `qwebr-noninteractive-setup-area-${qwebrCounter}`;
mainDiv.className = 'qwebr-noninteractive-setup-area';

// Create a status container div
var statusContainer = createLoadingContainer(qwebrCounter);

// Append status onto the main div
mainDiv.appendChild(statusContainer);

return mainDiv;
}


// Function to create loading container with specified ID
globalThis.createLoadingContainer = function(qwebrCounter) {

// Create a status container
const container = document.createElement('div');
container.id = `qwebr-non-interactive-loading-container-${qwebrCounter}`;
container.className = 'qwebr-non-interactive-loading-container qwebr-cell-needs-evaluation';

// Create an R project logo to indicate its a code space
const rProjectIcon = document.createElement('i');
rProjectIcon.className = 'fa-brands fa-r-project fa-3x qwebr-r-project-logo';

// Setup a loading icon from font awesome
const spinnerIcon = document.createElement('i');
spinnerIcon.className = 'fa-solid fa-spinner fa-spin fa-1x qwebr-icon-status-spinner';

// Add a section for status text
const statusText = document.createElement('p');
statusText.id = `qwebr-status-text-${qwebrCounter}`;
statusText.className = `qwebr-status-text qwebr-cell-needs-evaluation`;
statusText.innerText = 'Loading webR...';

// Incorporate an inner container
const innerContainer = document.createElement('div');

// Append elements to the inner container
innerContainer.appendChild(spinnerIcon);
innerContainer.appendChild(statusText);

// Append elements to the main container
container.appendChild(rProjectIcon);
container.appendChild(innerContainer);

return container;
}
96 changes: 96 additions & 0 deletions _extensions/webr/qwebr-cell-initialization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Handle cell initialization initialization
qwebrCellDetails.map(
(entry) => {
// Handle the creation of the element
qwebrCreateHTMLElement(entry);
// In the event of interactive, initialize the monaco editor
if (entry.options.context == EvalTypes.Interactive) {
qwebrCreateMonacoEditorInstance(entry);
}
}
);

// Identify non-interactive cells (in order)
const filteredEntries = qwebrCellDetails.filter(entry => {
const contextOption = entry.options && entry.options.context;
return ['output', 'setup'].includes(contextOption);
});

// Condition non-interactive cells to only be run after webR finishes its initialization.
qwebrInstance.then(
async () => {
const nHiddenCells = filteredEntries.length;
var currentHiddenCell = 0;


// Modify button state
qwebrSetInteractiveButtonState(`🟡 Running hidden code cells ...`, false);

// Begin processing non-interactive sections
// Due to the iteration policy, we must use a for() loop.
// Otherwise, we would need to switch to using reduce with an empty
// starting promise
for (const entry of filteredEntries) {

// Determine cell being examined
currentHiddenCell = currentHiddenCell + 1;
const formattedMessage = `Evaluating hidden cell ${currentHiddenCell} out of ${nHiddenCells}`;

// Update the document status header
if (qwebrShowStartupMessage) {
qwebrUpdateStatusHeader(formattedMessage);
}

// Display the update in non-active areas
qwebrUpdateStatusMessage(formattedMessage);

// Extract details on the active cell
const evalType = entry.options.context;
const cellCode = entry.code;
const qwebrCounter = entry.id;

// Disable further global status updates
const activeContainer = document.getElementById(`qwebr-non-interactive-loading-container-${qwebrCounter}`);
activeContainer.classList.remove('qwebr-cell-needs-evaluation');
activeContainer.classList.add('qwebr-cell-evaluated');

// Update status on the code cell
const activeStatus = document.getElementById(`qwebr-status-text-${qwebrCounter}`);
activeStatus.innerText = " Evaluating hidden code cell...";
activeStatus.classList.remove('qwebr-cell-needs-evaluation');
activeStatus.classList.add('qwebr-cell-evaluated');

switch (evalType) {
case 'output':
// Run the code in a non-interactive state that is geared to displaying output
await qwebrExecuteCode(`${cellCode}`, qwebrCounter, EvalTypes.Output);
break;
case 'setup':
const activeDiv = document.getElementById(`qwebr-noninteractive-setup-area-${qwebrCounter}`);
//activeDiv.textContent = "Computing hidden webR Startup ...";
// Run the code in a non-interactive state with all output thrown away
await mainWebR.evalRVoid(`${cellCode}`);
//activeDiv.textContent = "";
break;
default:
break;
}
// Disable visibility
activeContainer.style.visibility = 'hidden';
activeContainer.style.display = 'none';
}
}
).then(
() => {
// Release document status as ready

if (qwebrShowStartupMessage) {
qwebrStartupMessage.innerText = "🟢 Ready!"
}

qwebrSetInteractiveButtonState(
`<i class="fa-solid fa-play qwebr-icon-run-code"></i> <span>Run Code</span>`,
true
);
}
);
34 changes: 19 additions & 15 deletions _extensions/webr/qwebr-compute-engine.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
// Supported Evaluation Types for Context
globalThis.EvalTypes = Object.freeze({
Interactive: 'interactive',
Setup: 'setup',
Output: 'output',
});

// Function to verify a given JavaScript Object is empty
globalThis.qwebrIsObjectEmpty = function (arr) {
return Object.keys(arr).length === 0;
}

// Global version of the Escape HTML function that converts HTML
// characters to their HTML entities.
globalThis.qwebrEscapeHTMLCharacters = function(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
};

// Function to parse the pager results
globalThis.qwebrParseTypePager = async function (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) => {
const paged_data = await mainWebR.FS.readFile(path).then((data) => {
// Obtain the file content
let content = new TextDecoder().decode(data);

Expand All @@ -32,7 +36,7 @@ globalThis.qwebrParseTypePager = async function (msg) {

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

// Return extracted data with spaces
Expand Down Expand Up @@ -61,12 +65,12 @@ globalThis.qwebrComputeEngine = async function(
// ----

// Initialize webR
await webR.init();
await mainWebR.init();

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

const result = await webRCodeShelter.captureR(codeToRun, {
const result = await mainWebRCodeShelter.captureR(codeToRun, {
withAutoprint: true,
captureStreams: true,
captureConditions: false//,
Expand All @@ -79,7 +83,7 @@ globalThis.qwebrComputeEngine = async function(
try {

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

// Merge output streams of STDOUT and STDErr (messages and errors are combined.)
const out = result.output
Expand All @@ -95,7 +99,7 @@ globalThis.qwebrComputeEngine = async function(
// We're now able to process both graphics and pager events.
// As a result, we cannot maintain a true 1-to-1 output order
// without individually feeding each line
const msgs = await webR.flush();
const msgs = await mainWebR.flush();

// Output each image event stored
msgs.forEach((msg) => {
Expand Down Expand Up @@ -161,7 +165,7 @@ globalThis.qwebrComputeEngine = async function(
}
} finally {
// Clean up the remaining code
webRCodeShelter.purge();
mainWebRCodeShelter.purge();
}
}

Expand Down
Loading

0 comments on commit c9fd938

Please sign in to comment.