Skip to content

Commit b28b99d

Browse files
committed
Update to latest extension version
1 parent 72fe3d7 commit b28b99d

14 files changed

+1016
-181
lines changed

.github/workflows/publish-quarto.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
shell: bash
5151
run: |
5252
quarto add --no-prompt quarto-ext/shinylive
53-
quarto add --no-prompt coatless/quarto-webr@0.4.0
53+
quarto add --no-prompt coatless/quarto-webr@0.4.2
5454
5555
# Render the Quarto file
5656
- name: "Render working directory"
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
name: webr
22
title: Embedded webr code cells
33
author: James Joseph Balamuta
4-
version: 0.4.1-dev.1
5-
quarto-required: ">=1.2.198"
4+
version: 0.4.2
5+
quarto-required: ">=1.4.554"
66
contributes:
77
filters:
88
- webr.lua

_extensions/coatless/webr/qwebr-cell-elements.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,35 @@ globalThis.EvalTypes = Object.freeze({
55
Output: 'output',
66
});
77

8+
// Function that obtains the font size for a given element
9+
globalThis.qwebrCurrentFontSizeOnElement = function(element, cssProperty = 'font-size') {
10+
11+
const currentFontSize = parseFloat(
12+
window
13+
.getComputedStyle(element)
14+
.getPropertyValue(cssProperty)
15+
);
16+
17+
return currentFontSize;
18+
}
19+
20+
// Function to determine font scaling
21+
globalThis.qwebrScaledFontSize = function(div, qwebrOptions) {
22+
// Determine if we should compute font-size using RevealJS's `--r-main-font-size`
23+
// or if we can directly use the document's `font-size`.
24+
const cssProperty = document.body.classList.contains('reveal') ?
25+
"--r-main-font-size" : "font-size";
26+
27+
// Get the current font size on the div element
28+
const elementFontSize = qwebrCurrentFontSizeOnElement(div, cssProperty);
29+
30+
// Determine the scaled font size value
31+
const scaledFontSize = ((qwebrOptions['editor-font-scale'] ?? 1) * elementFontSize) ?? 17.5;
32+
33+
return scaledFontSize;
34+
}
35+
36+
837
// Function that dispatches the creation request
938
globalThis.qwebrCreateHTMLElement = function (
1039
cellData

_extensions/coatless/webr/qwebr-cell-initialization.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ qwebrInstance.then(
7878
break;
7979
case 'setup':
8080
const activeDiv = document.getElementById(`qwebr-noninteractive-setup-area-${qwebrCounter}`);
81+
82+
// Store code in history
83+
qwebrLogCodeToHistory(cellCode, entry.options);
84+
8185
// Run the code in a non-interactive state with all output thrown away
8286
await mainWebR.evalRVoid(`${cellCode}`);
8387
break;

_extensions/coatless/webr/qwebr-compute-engine.js

Lines changed: 107 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,34 @@ globalThis.qwebrPrefixComment = function(x, comment) {
2424
return `${comment}${x}`;
2525
};
2626

27+
// Function to store the code in the history
28+
globalThis.qwebrLogCodeToHistory = function(codeToRun, options) {
29+
qwebrRCommandHistory.push(
30+
`# Ran code in ${options.label} at ${new Date().toLocaleString()} ----\n${codeToRun}`
31+
);
32+
}
33+
34+
// Function to attach a download button onto the canvas
35+
// allowing the user to download the image.
36+
function qwebrImageCanvasDownloadButton(canvas, canvasContainer) {
37+
38+
// Create the download button
39+
const downloadButton = document.createElement('button');
40+
downloadButton.className = 'qwebr-canvas-image-download-btn';
41+
downloadButton.textContent = 'Download Image';
42+
canvasContainer.appendChild(downloadButton);
43+
44+
// Trigger a download of the image when the button is clicked
45+
downloadButton.addEventListener('click', function() {
46+
const image = canvas.toDataURL('image/png');
47+
const link = document.createElement('a');
48+
link.href = image;
49+
link.download = 'qwebr-canvas-image.png';
50+
link.click();
51+
});
52+
}
53+
54+
2755
// Function to parse the pager results
2856
globalThis.qwebrParseTypePager = async function (msg) {
2957

@@ -64,10 +92,8 @@ globalThis.qwebrComputeEngine = async function(
6492
// 1. We setup a canvas device to write to by making a namespace call into the {webr} package
6593
// 2. We use values inside of the options array to set the figure size.
6694
// 3. We capture the output stream information (STDOUT and STERR)
67-
// 4. While parsing the results, we disable image creation.
68-
69-
// Create a canvas variable for graphics
70-
let canvas = undefined;
95+
// 4. We disable the current device's image creation.
96+
// 5. Piece-wise parse the results into the different output areas
7197

7298
// Create a pager variable for help/file contents
7399
let pager = [];
@@ -91,23 +117,45 @@ globalThis.qwebrComputeEngine = async function(
91117
// Initialize webR
92118
await mainWebR.init();
93119

94-
// Setup a webR canvas by making a namespace call into the {webr} package
95-
await mainWebR.evalRVoid(`webr::canvas(width=${fig_width}, height=${fig_height})`);
96-
97-
const result = await mainWebRCodeShelter.captureR(codeToRun, {
120+
// Configure capture output
121+
let captureOutputOptions = {
98122
withAutoprint: true,
99123
captureStreams: true,
100-
captureConditions: false//,
124+
captureConditions: false,
101125
// env: webR.objs.emptyEnv, // maintain a global environment for webR v0.2.0
102-
});
126+
};
127+
128+
// Determine if the browser supports OffScreen
129+
if (qwebrOffScreenCanvasSupport()) {
130+
// Mirror default options of webr::canvas()
131+
// with changes to figure height and width.
132+
captureOutputOptions.captureGraphics = {
133+
width: fig_width,
134+
height: fig_height,
135+
bg: "white", // default: transparent
136+
pointsize: 12,
137+
capture: true
138+
};
139+
} else {
140+
// Disable generating graphics
141+
captureOutputOptions.captureGraphics = false;
142+
}
143+
144+
// Store the code to run in history
145+
qwebrLogCodeToHistory(codeToRun, options);
146+
147+
// Setup a webR canvas by making a namespace call into the {webr} package
148+
// Evaluate the R code
149+
// Remove the active canvas silently
150+
const result = await mainWebRCodeShelter.captureR(
151+
`${codeToRun}`,
152+
captureOutputOptions
153+
);
103154

104155
// -----
105156

106157
// Start attempting to parse the result data
107158
processResultOutput:try {
108-
109-
// Stop creating images
110-
await mainWebR.evalRVoid("dev.off()");
111159

112160
// Avoid running through output processing
113161
if (options.results === "hide" || options.output === "false") {
@@ -130,34 +178,11 @@ globalThis.qwebrComputeEngine = async function(
130178

131179

132180
// Clean the state
133-
// We're now able to process both graphics and pager events.
181+
// We're now able to process pager events.
134182
// As a result, we cannot maintain a true 1-to-1 output order
135183
// without individually feeding each line
136184
const msgs = await mainWebR.flush();
137185

138-
// Output each image event stored
139-
msgs.forEach((msg) => {
140-
// Determine if old canvas can be used or a new canvas is required.
141-
if (msg.type === 'canvas'){
142-
// Add image to the current canvas
143-
if (msg.data.event === 'canvasImage') {
144-
canvas.getContext('2d').drawImage(msg.data.image, 0, 0);
145-
} else if (msg.data.event === 'canvasNewPage') {
146-
147-
// Generate a new canvas element
148-
canvas = document.createElement("canvas");
149-
canvas.setAttribute("width", 2 * fig_width);
150-
canvas.setAttribute("height", 2 * fig_height);
151-
canvas.style.width = options["out-width"] ? options["out-width"] : `${fig_width}px`;
152-
if (options["out-height"]) {
153-
canvas.style.height = options["out-height"];
154-
}
155-
canvas.style.display = "block";
156-
canvas.style.margin = "auto";
157-
}
158-
}
159-
});
160-
161186
// Use `map` to process the filtered "pager" events asynchronously
162187
const pager = await Promise.all(
163188
msgs.filter(msg => msg.type === 'pager').map(
@@ -177,6 +202,13 @@ globalThis.qwebrComputeEngine = async function(
177202
// Display results as HTML elements to retain output styling
178203
const div = document.createElement("div");
179204
div.innerHTML = out;
205+
206+
// Calculate a scaled font-size value
207+
const scaledFontSize = qwebrScaledFontSize(
208+
elements.outputCodeDiv, options);
209+
210+
// Override output code cell size
211+
pre.style.fontSize = `${scaledFontSize}px`;
180212
pre.appendChild(div);
181213
} else {
182214
// If nothing is present, hide the element.
@@ -185,23 +217,55 @@ globalThis.qwebrComputeEngine = async function(
185217

186218
elements.outputCodeDiv.appendChild(pre);
187219

188-
// Place the graphics on the canvas
189-
if (canvas) {
220+
// Determine if we have graphs to display
221+
if (result.images.length > 0) {
222+
190223
// Create figure element
191-
const figureElement = document.createElement('figure');
224+
const figureElement = document.createElement("figure");
225+
figureElement.className = "qwebr-canvas-image";
226+
227+
// Place each rendered graphic onto a canvas element
228+
result.images.forEach((img) => {
229+
230+
// Construct canvas for object
231+
const canvas = document.createElement("canvas");
232+
233+
// Add an image download button
234+
qwebrImageCanvasDownloadButton(canvas, figureElement);
235+
236+
// Set canvas size to image
237+
canvas.width = img.width;
238+
canvas.height = img.height;
239+
240+
// Apply output truncations
241+
canvas.style.width = options["out-width"] ? options["out-width"] : `${fig_width}px`;
242+
if (options["out-height"]) {
243+
canvas.style.height = options["out-height"];
244+
}
245+
246+
// Apply styling
247+
canvas.style.display = "block";
248+
canvas.style.margin = "auto";
192249

193-
// Append canvas to figure
194-
figureElement.appendChild(canvas);
250+
// Draw image onto Canvas
251+
const ctx = canvas.getContext("2d");
252+
ctx.drawImage(img, 0, 0, img.width, img.height);
253+
254+
// Append canvas to figure output area
255+
figureElement.appendChild(canvas);
195256

257+
});
258+
196259
if (options['fig-cap']) {
197260
// Create figcaption element
198261
const figcaptionElement = document.createElement('figcaption');
199262
figcaptionElement.innerText = options['fig-cap'];
200263
// Append figcaption to figure
201264
figureElement.appendChild(figcaptionElement);
202265
}
203-
266+
204267
elements.outputGraphDiv.appendChild(figureElement);
268+
205269
}
206270

207271
// Display the pager data
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Define a global storage and retrieval solution ----
2+
3+
// Store commands executed in R
4+
globalThis.qwebrRCommandHistory = [];
5+
6+
// Function to retrieve the command history
7+
globalThis.qwebrFormatRHistory = function() {
8+
return qwebrRCommandHistory.join("\n\n");
9+
}
10+
11+
// Retrieve HTML Elements ----
12+
13+
// Get the command modal
14+
const command_history_modal = document.getElementById("qwebr-history-modal");
15+
16+
// Get the button that opens the command modal
17+
const command_history_btn = document.getElementById("qwebrRHistoryButton");
18+
19+
// Get the <span> element that closes the command modal
20+
const command_history_close_span = document.getElementById("qwebr-command-history-close-btn");
21+
22+
// Get the download button for r history information
23+
const command_history_download_btn = document.getElementById("qwebr-download-history-btn");
24+
25+
// Plug in command history into modal/download button ----
26+
27+
// Function to populate the modal with command history
28+
function populateCommandHistoryModal() {
29+
document.getElementById("qwebr-command-history-contents").innerHTML = qwebrFormatRHistory() || "No commands have been executed yet.";
30+
}
31+
32+
// Function to format the current date and time to
33+
// a string with the format YYYY-MM-DD-HH-MM-SS
34+
function formatDateTime() {
35+
const now = new Date();
36+
37+
const year = now.getFullYear();
38+
const day = String(now.getDate()).padStart(2, '0');
39+
const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are zero-based
40+
const hours = String(now.getHours()).padStart(2, '0');
41+
const minutes = String(now.getMinutes()).padStart(2, '0');
42+
const seconds = String(now.getSeconds()).padStart(2, '0');
43+
44+
return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`;
45+
}
46+
47+
48+
// Function to convert document title with datetime to a safe filename
49+
function safeFileName() {
50+
// Get the current page title
51+
let pageTitle = document.title;
52+
53+
// Combine the current page title with the current date and time
54+
let pageNameWithDateTime = `Rhistory-${pageTitle}-${formatDateTime()}`;
55+
56+
// Replace unsafe characters with safe alternatives
57+
let safeFilename = pageNameWithDateTime.replace(/[\\/:\*\?! "<>\|]/g, '-');
58+
59+
return safeFilename;
60+
}
61+
62+
63+
// Function to download list contents as text file
64+
function downloadRHistory() {
65+
// Get the current page title + datetime and use it as the filename
66+
const filename = `${safeFileName()}.R`;
67+
68+
// Get the text contents of the R History list
69+
const text = qwebrFormatRHistory();
70+
71+
// Create a new Blob object with the text contents
72+
const blob = new Blob([text], { type: 'text/plain' });
73+
74+
// Create a new anchor element for the download
75+
const a = document.createElement('a');
76+
a.style.display = 'none';
77+
a.href = URL.createObjectURL(blob);
78+
a.download = filename;
79+
80+
// Append the anchor to the body, click it, and remove it
81+
document.body.appendChild(a);
82+
a.click();
83+
document.body.removeChild(a);
84+
}
85+
86+
// Register event handlers ----
87+
88+
// When the user clicks the View R History button, open the command modal
89+
command_history_btn.onclick = function() {
90+
populateCommandHistoryModal();
91+
command_history_modal.style.display = "block";
92+
}
93+
94+
// When the user clicks on <span> (x), close the command modal
95+
command_history_close_span.onclick = function() {
96+
command_history_modal.style.display = "none";
97+
}
98+
99+
// When the user clicks anywhere outside of the command modal, close it
100+
window.onclick = function(event) {
101+
if (event.target == command_history_modal) {
102+
command_history_modal.style.display = "none";
103+
}
104+
}
105+
106+
// Add an onclick event listener to the download button so that
107+
// the user can download the R history as a text file
108+
command_history_download_btn.onclick = function() {
109+
downloadRHistory();
110+
};

0 commit comments

Comments
 (0)