Skip to content

Commit ff8a0ee

Browse files
committed
Add extensions
1 parent 34eeee2 commit ff8a0ee

18 files changed

+2436
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
name: webr
2+
title: Embedded webr code cells
3+
author: James Joseph Balamuta
4+
version: 0.4.1-dev.1
5+
quarto-required: ">=1.2.198"
6+
contributes:
7+
filters:
8+
- webr.lua
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
// Supported Evaluation Types for Context
2+
globalThis.EvalTypes = Object.freeze({
3+
Interactive: 'interactive',
4+
Setup: 'setup',
5+
Output: 'output',
6+
});
7+
8+
// Function that dispatches the creation request
9+
globalThis.qwebrCreateHTMLElement = function (
10+
cellData
11+
) {
12+
13+
// Extract key components
14+
const evalType = cellData.options.context;
15+
const qwebrCounter = cellData.id;
16+
17+
// We make an assumption that insertion points are defined by the Lua filter as:
18+
// qwebr-insertion-location-{qwebrCounter}
19+
const elementLocator = document.getElementById(`qwebr-insertion-location-${qwebrCounter}`);
20+
21+
// Figure out the routine to use to insert the element.
22+
let qwebrElement;
23+
switch ( evalType ) {
24+
case EvalTypes.Interactive:
25+
qwebrElement = qwebrCreateInteractiveElement(qwebrCounter, cellData.options);
26+
break;
27+
case EvalTypes.Output:
28+
qwebrElement = qwebrCreateNonInteractiveOutputElement(qwebrCounter, cellData.options);
29+
break;
30+
case EvalTypes.Setup:
31+
qwebrElement = qwebrCreateNonInteractiveSetupElement(qwebrCounter, cellData.options);
32+
break;
33+
default:
34+
qwebrElement = document.createElement('div');
35+
qwebrElement.textContent = 'Error creating `quarto-webr` element';
36+
}
37+
38+
// Insert the dynamically generated object at the document location.
39+
elementLocator.appendChild(qwebrElement);
40+
};
41+
42+
// Function that setups the interactive element creation
43+
globalThis.qwebrCreateInteractiveElement = function (qwebrCounter, qwebrOptions) {
44+
45+
// Create main div element
46+
var mainDiv = document.createElement('div');
47+
mainDiv.id = 'qwebr-interactive-area-' + qwebrCounter;
48+
mainDiv.className = `qwebr-interactive-area`;
49+
if (qwebrOptions.classes) {
50+
mainDiv.className += " " + qwebrOptions.classes
51+
}
52+
53+
// Add a unique cell identifier that users can customize
54+
if (qwebrOptions.label) {
55+
mainDiv.setAttribute('data-id', qwebrOptions.label);
56+
}
57+
58+
// Create toolbar div
59+
var toolbarDiv = document.createElement('div');
60+
toolbarDiv.className = 'qwebr-editor-toolbar';
61+
toolbarDiv.id = 'qwebr-editor-toolbar-' + qwebrCounter;
62+
63+
// Create a div to hold the left buttons
64+
var leftButtonsDiv = document.createElement('div');
65+
leftButtonsDiv.className = 'qwebr-editor-toolbar-left-buttons';
66+
67+
// Create a div to hold the right buttons
68+
var rightButtonsDiv = document.createElement('div');
69+
rightButtonsDiv.className = 'qwebr-editor-toolbar-right-buttons';
70+
71+
// Create Run Code button
72+
var runCodeButton = document.createElement('button');
73+
runCodeButton.className = 'btn btn-default qwebr-button qwebr-button-run';
74+
runCodeButton.disabled = true;
75+
runCodeButton.type = 'button';
76+
runCodeButton.id = 'qwebr-button-run-' + qwebrCounter;
77+
runCodeButton.textContent = '🟡 Loading webR...';
78+
runCodeButton.title = `Run code (Shift + Enter)`;
79+
80+
// Append buttons to the leftButtonsDiv
81+
leftButtonsDiv.appendChild(runCodeButton);
82+
83+
// Create Reset button
84+
var resetButton = document.createElement('button');
85+
resetButton.className = 'btn btn-light btn-xs qwebr-button qwebr-button-reset';
86+
resetButton.type = 'button';
87+
resetButton.id = 'qwebr-button-reset-' + qwebrCounter;
88+
resetButton.title = 'Start over';
89+
resetButton.innerHTML = '<i class="fa-solid fa-arrows-rotate"></i>';
90+
91+
// Create Copy button
92+
var copyButton = document.createElement('button');
93+
copyButton.className = 'btn btn-light btn-xs qwebr-button qwebr-button-copy';
94+
copyButton.type = 'button';
95+
copyButton.id = 'qwebr-button-copy-' + qwebrCounter;
96+
copyButton.title = 'Copy code';
97+
copyButton.innerHTML = '<i class="fa-regular fa-copy"></i>';
98+
99+
// Append buttons to the rightButtonsDiv
100+
rightButtonsDiv.appendChild(resetButton);
101+
rightButtonsDiv.appendChild(copyButton);
102+
103+
// Create console area div
104+
var consoleAreaDiv = document.createElement('div');
105+
consoleAreaDiv.id = 'qwebr-console-area-' + qwebrCounter;
106+
consoleAreaDiv.className = 'qwebr-console-area';
107+
108+
// Create editor div
109+
var editorDiv = document.createElement('div');
110+
editorDiv.id = 'qwebr-editor-' + qwebrCounter;
111+
editorDiv.className = 'qwebr-editor';
112+
113+
// Create output code area div
114+
var outputCodeAreaDiv = document.createElement('div');
115+
outputCodeAreaDiv.id = 'qwebr-output-code-area-' + qwebrCounter;
116+
outputCodeAreaDiv.className = 'qwebr-output-code-area';
117+
outputCodeAreaDiv.setAttribute('aria-live', 'assertive');
118+
119+
// Create pre element inside output code area
120+
var preElement = document.createElement('pre');
121+
preElement.style.visibility = 'hidden';
122+
outputCodeAreaDiv.appendChild(preElement);
123+
124+
// Create output graph area div
125+
var outputGraphAreaDiv = document.createElement('div');
126+
outputGraphAreaDiv.id = 'qwebr-output-graph-area-' + qwebrCounter;
127+
outputGraphAreaDiv.className = 'qwebr-output-graph-area';
128+
129+
// Append buttons to the toolbar
130+
toolbarDiv.appendChild(leftButtonsDiv);
131+
toolbarDiv.appendChild(rightButtonsDiv);
132+
133+
// Append all elements to the main div
134+
mainDiv.appendChild(toolbarDiv);
135+
consoleAreaDiv.appendChild(editorDiv);
136+
consoleAreaDiv.appendChild(outputCodeAreaDiv);
137+
mainDiv.appendChild(consoleAreaDiv);
138+
mainDiv.appendChild(outputGraphAreaDiv);
139+
140+
return mainDiv;
141+
}
142+
143+
// Function that adds output structure for non-interactive output
144+
globalThis.qwebrCreateNonInteractiveOutputElement = function(qwebrCounter, qwebrOptions) {
145+
// Create main div element
146+
var mainDiv = document.createElement('div');
147+
mainDiv.id = 'qwebr-noninteractive-area-' + qwebrCounter;
148+
mainDiv.className = `qwebr-noninteractive-area`;
149+
if (qwebrOptions.classes) {
150+
mainDiv.className += " " + qwebrOptions.classes
151+
}
152+
153+
// Add a unique cell identifier that users can customize
154+
if (qwebrOptions.label) {
155+
mainDiv.setAttribute('data-id', qwebrOptions.label);
156+
}
157+
158+
// Create a status container div
159+
var statusContainer = createLoadingContainer(qwebrCounter);
160+
161+
// Create output code area div
162+
var outputCodeAreaDiv = document.createElement('div');
163+
outputCodeAreaDiv.id = 'qwebr-output-code-area-' + qwebrCounter;
164+
outputCodeAreaDiv.className = 'qwebr-output-code-area';
165+
outputCodeAreaDiv.setAttribute('aria-live', 'assertive');
166+
167+
// Create pre element inside output code area
168+
var preElement = document.createElement('pre');
169+
preElement.style.visibility = 'hidden';
170+
outputCodeAreaDiv.appendChild(preElement);
171+
172+
// Create output graph area div
173+
var outputGraphAreaDiv = document.createElement('div');
174+
outputGraphAreaDiv.id = 'qwebr-output-graph-area-' + qwebrCounter;
175+
outputGraphAreaDiv.className = 'qwebr-output-graph-area';
176+
177+
// Append all elements to the main div
178+
mainDiv.appendChild(statusContainer);
179+
mainDiv.appendChild(outputCodeAreaDiv);
180+
mainDiv.appendChild(outputGraphAreaDiv);
181+
182+
return mainDiv;
183+
};
184+
185+
// Function that adds a stub in the page to indicate a setup cell was used.
186+
globalThis.qwebrCreateNonInteractiveSetupElement = function(qwebrCounter, qwebrOptions) {
187+
// Create main div element
188+
var mainDiv = document.createElement('div');
189+
mainDiv.id = `qwebr-noninteractive-setup-area-${qwebrCounter}`;
190+
mainDiv.className = `qwebr-noninteractive-setup-area`;
191+
if (qwebrOptions.classes) {
192+
mainDiv.className += " " + qwebrOptions.classes
193+
}
194+
195+
196+
// Add a unique cell identifier that users can customize
197+
if (qwebrOptions.label) {
198+
mainDiv.setAttribute('data-id', qwebrOptions.label);
199+
}
200+
201+
// Create a status container div
202+
var statusContainer = createLoadingContainer(qwebrCounter);
203+
204+
// Append status onto the main div
205+
mainDiv.appendChild(statusContainer);
206+
207+
return mainDiv;
208+
}
209+
210+
211+
// Function to create loading container with specified ID
212+
globalThis.createLoadingContainer = function(qwebrCounter) {
213+
214+
// Create a status container
215+
const container = document.createElement('div');
216+
container.id = `qwebr-non-interactive-loading-container-${qwebrCounter}`;
217+
container.className = 'qwebr-non-interactive-loading-container qwebr-cell-needs-evaluation';
218+
219+
// Create an R project logo to indicate its a code space
220+
const rProjectIcon = document.createElement('i');
221+
rProjectIcon.className = 'fa-brands fa-r-project fa-3x qwebr-r-project-logo';
222+
223+
// Setup a loading icon from font awesome
224+
const spinnerIcon = document.createElement('i');
225+
spinnerIcon.className = 'fa-solid fa-spinner fa-spin fa-1x qwebr-icon-status-spinner';
226+
227+
// Add a section for status text
228+
const statusText = document.createElement('p');
229+
statusText.id = `qwebr-status-text-${qwebrCounter}`;
230+
statusText.className = `qwebr-status-text qwebr-cell-needs-evaluation`;
231+
statusText.innerText = 'Loading webR...';
232+
233+
// Incorporate an inner container
234+
const innerContainer = document.createElement('div');
235+
236+
// Append elements to the inner container
237+
innerContainer.appendChild(spinnerIcon);
238+
innerContainer.appendChild(statusText);
239+
240+
// Append elements to the main container
241+
container.appendChild(rProjectIcon);
242+
container.appendChild(innerContainer);
243+
244+
return container;
245+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Handle cell initialization initialization
2+
qwebrCellDetails.map(
3+
(entry) => {
4+
// Handle the creation of the element
5+
qwebrCreateHTMLElement(entry);
6+
// In the event of interactive, initialize the monaco editor
7+
if (entry.options.context == EvalTypes.Interactive) {
8+
qwebrCreateMonacoEditorInstance(entry);
9+
}
10+
}
11+
);
12+
13+
// Identify non-interactive cells (in order)
14+
const filteredEntries = qwebrCellDetails.filter(entry => {
15+
const contextOption = entry.options && entry.options.context;
16+
return ['output', 'setup'].includes(contextOption) || (contextOption == "interactive" && entry.options && entry.options.autorun === 'true');
17+
});
18+
19+
// Condition non-interactive cells to only be run after webR finishes its initialization.
20+
qwebrInstance.then(
21+
async () => {
22+
const nHiddenCells = filteredEntries.length;
23+
var currentHiddenCell = 0;
24+
25+
26+
// Modify button state
27+
qwebrSetInteractiveButtonState(`🟡 Running hidden code cells ...`, false);
28+
29+
// Begin processing non-interactive sections
30+
// Due to the iteration policy, we must use a for() loop.
31+
// Otherwise, we would need to switch to using reduce with an empty
32+
// starting promise
33+
for (const entry of filteredEntries) {
34+
35+
// Determine cell being examined
36+
currentHiddenCell = currentHiddenCell + 1;
37+
const formattedMessage = `Evaluating hidden cell ${currentHiddenCell} out of ${nHiddenCells}`;
38+
39+
// Update the document status header
40+
if (qwebrShowStartupMessage) {
41+
qwebrUpdateStatusHeader(formattedMessage);
42+
}
43+
44+
// Display the update in non-active areas
45+
qwebrUpdateStatusMessage(formattedMessage);
46+
47+
// Extract details on the active cell
48+
const evalType = entry.options.context;
49+
const cellCode = entry.code;
50+
const qwebrCounter = entry.id;
51+
52+
if (['output', 'setup'].includes(evalType)) {
53+
// Disable further global status updates
54+
const activeContainer = document.getElementById(`qwebr-non-interactive-loading-container-${qwebrCounter}`);
55+
activeContainer.classList.remove('qwebr-cell-needs-evaluation');
56+
activeContainer.classList.add('qwebr-cell-evaluated');
57+
58+
// Update status on the code cell
59+
const activeStatus = document.getElementById(`qwebr-status-text-${qwebrCounter}`);
60+
activeStatus.innerText = " Evaluating hidden code cell...";
61+
activeStatus.classList.remove('qwebr-cell-needs-evaluation');
62+
activeStatus.classList.add('qwebr-cell-evaluated');
63+
}
64+
65+
switch (evalType) {
66+
case 'interactive':
67+
// TODO: Make this more standardized.
68+
// At the moment, we're overriding the interactive status update by pretending its
69+
// output-like.
70+
const tempOptions = entry.options;
71+
tempOptions["context"] = "output"
72+
// Run the code in a non-interactive state that is geared to displaying output
73+
await qwebrExecuteCode(`${cellCode}`, qwebrCounter, tempOptions);
74+
break;
75+
case 'output':
76+
// Run the code in a non-interactive state that is geared to displaying output
77+
await qwebrExecuteCode(`${cellCode}`, qwebrCounter, entry.options);
78+
break;
79+
case 'setup':
80+
const activeDiv = document.getElementById(`qwebr-noninteractive-setup-area-${qwebrCounter}`);
81+
// Run the code in a non-interactive state with all output thrown away
82+
await mainWebR.evalRVoid(`${cellCode}`);
83+
break;
84+
default:
85+
break;
86+
}
87+
88+
if (['output', 'setup'].includes(evalType)) {
89+
// Disable further global status updates
90+
const activeContainer = document.getElementById(`qwebr-non-interactive-loading-container-${qwebrCounter}`);
91+
// Disable visibility
92+
activeContainer.style.visibility = 'hidden';
93+
activeContainer.style.display = 'none';
94+
}
95+
}
96+
}
97+
).then(
98+
() => {
99+
// Release document status as ready
100+
101+
if (qwebrShowStartupMessage) {
102+
qwebrStartupMessage.innerText = "🟢 Ready!"
103+
}
104+
105+
qwebrSetInteractiveButtonState(
106+
`<i class="fa-solid fa-play qwebr-icon-run-code"></i> <span>Run Code</span>`,
107+
true
108+
);
109+
}
110+
);

0 commit comments

Comments
 (0)