Skip to content

Commit 8ad65f0

Browse files
dipterixcoatless
andauthored
Supporting browse type so htmlwidgets can be viewed from webR (#223)
* Supporting `browse` type so htmlwidgets can be viewed from webR * Added `webr::viewer_install()` during initialization * Added css style to the iframe browse * Add release note * Bump version * Add CSS class to list of classes. --------- Co-authored-by: James J Balamuta <[email protected]>
1 parent f56e640 commit 8ad65f0

6 files changed

+126
-54
lines changed

_extensions/webr/_extension.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: webr
22
title: Embedded webr code cells
33
author: James Joseph Balamuta
4-
version: 0.4.3-dev.1
4+
version: 0.4.3-dev.2
55
quarto-required: ">=1.4.554"
66
contributes:
77
filters:

_extensions/webr/qwebr-compute-engine.js

+100-48
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ globalThis.qwebrIsObjectEmpty = function (arr) {
33
return Object.keys(arr).length === 0;
44
}
55

6-
// Global version of the Escape HTML function that converts HTML
6+
// Global version of the Escape HTML function that converts HTML
77
// characters to their HTML entities.
88
globalThis.qwebrEscapeHTMLCharacters = function(unsafe) {
99
return unsafe
@@ -12,7 +12,7 @@ globalThis.qwebrEscapeHTMLCharacters = function(unsafe) {
1212
.replace(/>/g, "&gt;")
1313
.replace(/"/g, "&quot;")
1414
.replace(/'/g, "&#039;");
15-
};
15+
};
1616

1717
// Passthrough results
1818
globalThis.qwebrIdentity = function(x) {
@@ -29,7 +29,7 @@ globalThis.qwebrLogCodeToHistory = function(codeToRun, options) {
2929
qwebrRCommandHistory.push(
3030
`# Ran code in ${options.label} at ${new Date().toLocaleString()} ----\n${codeToRun}`
3131
);
32-
}
32+
};
3333

3434
// Function to attach a download button onto the canvas
3535
// allowing the user to download the image.
@@ -49,14 +49,14 @@ function qwebrImageCanvasDownloadButton(canvas, canvasContainer) {
4949
link.download = 'qwebr-canvas-image.png';
5050
link.click();
5151
});
52-
}
53-
52+
}
53+
5454

5555
// Function to parse the pager results
56-
globalThis.qwebrParseTypePager = async function (msg) {
56+
globalThis.qwebrParseTypePager = async function (msg) {
5757

5858
// Split out the event data
59-
const { path, title, deleteFile } = msg.data;
59+
const { path, title, deleteFile } = msg.data;
6060

6161
// Process the pager data by reading the information from disk
6262
const paged_data = await mainWebR.FS.readFile(path).then((data) => {
@@ -65,30 +65,49 @@ globalThis.qwebrParseTypePager = async function (msg) {
6565

6666
// Remove excessive backspace characters until none remain
6767
while(content.match(/.[\b]/)){
68-
content = content.replace(/.[\b]/g, '');
68+
content = content.replace(/.[\b]/g, '');
6969
}
7070

7171
// Returned cleaned data
7272
return content;
7373
});
7474

7575
// Unlink file if needed
76-
if (deleteFile) {
77-
await mainWebR.FS.unlink(path);
78-
}
76+
if (deleteFile) {
77+
await mainWebR.FS.unlink(path);
78+
}
7979

8080
// Return extracted data with spaces
8181
return paged_data;
82-
}
82+
};
83+
84+
85+
// Function to parse the browse results
86+
globalThis.qwebrParseTypeBrowse = async function (msg) {
87+
88+
// msg.type === "browse"
89+
const path = msg.data.url;
90+
91+
// Process the browse data by reading the information from disk
92+
const browse_data = await mainWebR.FS.readFile(path).then((data) => {
93+
// Obtain the file content
94+
let content = new TextDecoder().decode(data);
95+
96+
return content;
97+
});
98+
99+
// Return extracted data as-is
100+
return browse_data;
101+
};
83102

84103
// Function to run the code using webR and parse the output
85104
globalThis.qwebrComputeEngine = async function(
86-
codeToRun,
87-
elements,
105+
codeToRun,
106+
elements,
88107
options) {
89108

90109
// Call into the R compute engine that persists within the document scope.
91-
// To be prepared for all scenarios, the following happens:
110+
// To be prepared for all scenarios, the following happens:
92111
// 1. We setup a canvas device to write to by making a namespace call into the {webr} package
93112
// 2. We use values inside of the options array to set the figure size.
94113
// 3. We capture the output stream information (STDOUT and STERR)
@@ -108,11 +127,11 @@ globalThis.qwebrComputeEngine = async function(
108127
processOutput = qwebrIdentity;
109128
}
110129

111-
// ----
130+
// ----
112131
// Convert from Inches to Pixels by using DPI (dots per inch)
113132
// for bitmap devices (dpi * inches = pixels)
114-
let fig_width = options["fig-width"] * options["dpi"]
115-
let fig_height = options["fig-height"] * options["dpi"]
133+
let fig_width = options["fig-width"] * options["dpi"];
134+
let fig_height = options["fig-height"] * options["dpi"];
116135

117136
// Initialize webR
118137
await mainWebR.init();
@@ -124,7 +143,7 @@ globalThis.qwebrComputeEngine = async function(
124143
captureConditions: false,
125144
// env: webR.objs.emptyEnv, // maintain a global environment for webR v0.2.0
126145
};
127-
146+
128147
// Determine if the browser supports OffScreen
129148
if (qwebrOffScreenCanvasSupport()) {
130149
// Mirror default options of webr::canvas()
@@ -156,18 +175,18 @@ globalThis.qwebrComputeEngine = async function(
156175

157176
// Start attempting to parse the result data
158177
processResultOutput:try {
159-
178+
160179
// Avoid running through output processing
161-
if (options.results === "hide" || options.output === "false") {
162-
break processResultOutput;
180+
if (options.results === "hide" || options.output === "false") {
181+
break processResultOutput;
163182
}
164183

165184
// Merge output streams of STDOUT and STDErr (messages and errors are combined.)
166-
// Require both `warning` and `message` to be true to display `STDErr`.
185+
// Require both `warning` and `message` to be true to display `STDErr`.
167186
const out = result.output
168187
.filter(
169-
evt => evt.type === "stdout" ||
170-
( evt.type === "stderr" && (options.warning === "true" && options.message === "true"))
188+
evt => evt.type === "stdout" ||
189+
( evt.type === "stderr" && (options.warning === "true" && options.message === "true"))
171190
)
172191
.map((evt, index) => {
173192
const className = `qwebr-output-code-${evt.type}`;
@@ -179,15 +198,31 @@ globalThis.qwebrComputeEngine = async function(
179198

180199
// Clean the state
181200
// We're now able to process pager events.
182-
// As a result, we cannot maintain a true 1-to-1 output order
201+
// As a result, we cannot maintain a true 1-to-1 output order
183202
// without individually feeding each line
184203
const msgs = await mainWebR.flush();
185204

186205
// Use `map` to process the filtered "pager" events asynchronously
187-
const pager = await Promise.all(
188-
msgs.filter(msg => msg.type === 'pager').map(
206+
const pager = [];
207+
const browse = [];
208+
209+
await Promise.all(
210+
msgs.map(
189211
async (msg) => {
190-
return await qwebrParseTypePager(msg);
212+
213+
const msgType = msg.type || "unknown";
214+
215+
switch(msgType) {
216+
case 'pager':
217+
const pager_data = await qwebrParseTypePager(msg);
218+
pager.push(pager_data);
219+
break;
220+
case 'browse':
221+
const browse_data = await qwebrParseTypeBrowse(msg);
222+
browse.push(browse_data);
223+
break;
224+
}
225+
return;
191226
}
192227
)
193228
);
@@ -250,40 +285,57 @@ globalThis.qwebrComputeEngine = async function(
250285
// Draw image onto Canvas
251286
const ctx = canvas.getContext("2d");
252287
ctx.drawImage(img, 0, 0, img.width, img.height);
253-
288+
254289
// Append canvas to figure output area
255290
figureElement.appendChild(canvas);
256291

257292
});
258-
293+
259294
if (options['fig-cap']) {
260295
// Create figcaption element
261296
const figcaptionElement = document.createElement('figcaption');
262297
figcaptionElement.innerText = options['fig-cap'];
263298
// Append figcaption to figure
264-
figureElement.appendChild(figcaptionElement);
299+
figureElement.appendChild(figcaptionElement);
265300
}
266-
301+
267302
elements.outputGraphDiv.appendChild(figureElement);
268303

269304
}
270305

271306
// Display the pager data
272-
if (pager) {
273-
// Use the `pre` element to preserve whitespace.
274-
pager.forEach((paged_data, index) => {
275-
let pre_pager = document.createElement("pre");
276-
pre_pager.innerText = paged_data;
277-
pre_pager.classList.add("qwebr-output-code-pager");
278-
pre_pager.setAttribute("id", `qwebr-output-code-pager-editor-${elements.id}-result-${index + 1}`);
279-
elements.outputCodeDiv.appendChild(pre_pager);
280-
});
307+
if (pager.length > 0) {
308+
// Use the `pre` element to preserve whitespace.
309+
pager.forEach((paged_data, index) => {
310+
const pre_pager = document.createElement("pre");
311+
pre_pager.innerText = paged_data;
312+
pre_pager.classList.add("qwebr-output-code-pager");
313+
pre_pager.setAttribute("id", `qwebr-output-code-pager-editor-${elements.id}-result-${index + 1}`);
314+
elements.outputCodeDiv.appendChild(pre_pager);
315+
});
316+
}
317+
318+
// Display the browse data
319+
if (browse.length > 0) {
320+
// Use the `pre` element to preserve whitespace.
321+
browse.forEach((browse_data, index) => {
322+
const iframe_browse = document.createElement('iframe');
323+
iframe_browse.classList.add("qwebr-output-code-browse");
324+
iframe_browse.setAttribute("id", `qwebr-output-code-browse-editor-${elements.id}-result-${index + 1}`);
325+
iframe_browse.style.width = "100%";
326+
iframe_browse.style.minHeight = "500px";
327+
elements.outputCodeDiv.appendChild(iframe_browse);
328+
329+
iframe_browse.contentWindow.document.open();
330+
iframe_browse.contentWindow.document.write(browse_data);
331+
iframe_browse.contentWindow.document.close();
332+
});
281333
}
282334
} finally {
283335
// Clean up the remaining code
284336
mainWebRCodeShelter.purge();
285337
}
286-
}
338+
};
287339

288340
// Function to execute the code (accepts code as an argument)
289341
globalThis.qwebrExecuteCode = async function (
@@ -293,12 +345,12 @@ globalThis.qwebrExecuteCode = async function (
293345

294346
// If options are not passed, we fall back on the bare minimum to handle the computation
295347
if (qwebrIsObjectEmpty(options)) {
296-
options = {
297-
"context": "interactive",
298-
"fig-width": 7, "fig-height": 5,
299-
"out-width": "700px", "out-height": "",
348+
options = {
349+
"context": "interactive",
350+
"fig-width": 7, "fig-height": 5,
351+
"out-width": "700px", "out-height": "",
300352
"dpi": 72,
301-
"results": "markup",
353+
"results": "markup",
302354
"warning": "true", "message": "true",
303355
};
304356
}

_extensions/webr/qwebr-document-engine-initialization.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,15 @@ globalThis.qwebrInstance = import(qwebrCustomizedWebROptions.baseURL + "webr.mjs
5858
// Setup a shelter
5959
globalThis.mainWebRCodeShelter = await new mainWebR.Shelter();
6060

61-
// Setup a pager to allow processing help documentation
62-
await mainWebR.evalRVoid('webr::pager_install()');
61+
// Setup a pager to allow processing help documentation
62+
await mainWebR.evalRVoid('webr::pager_install()');
63+
64+
// Setup a viewer to allow processing htmlwidgets.
65+
// This might not be available in old webr version
66+
await mainWebR.evalRVoid('try({ webr::viewer_install() })');
6367

6468
// Override the existing install.packages() to use webr::install()
65-
await mainWebR.evalRVoid('webr::shim_install()');
69+
await mainWebR.evalRVoid('webr::shim_install()');
6670

6771
// Specify the repositories to pull from
6872
// Note: webR does not use the `repos` option, but instead uses `webr_pkg_repos`

_extensions/webr/qwebr-styling.css

+11-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ figure:hover .qwebr-canvas-image-download-btn {
212212
max-height: 100%;
213213
margin: 0;
214214
padding: 0;
215-
}
215+
}
216216

217217
/* Provide space to entries */
218218
.reveal div.qwebr-output-code-area pre div {
@@ -256,3 +256,13 @@ body.reveal.quarto-dark div.qwebr-console-area {
256256
max-height: 400px;
257257
overflow: scroll;
258258
}
259+
260+
iframe.qwebr-output-code-browse {
261+
width: 100%;
262+
263+
/*
264+
TODO: How to make the height automatic according to the widget size,
265+
or respect the quarto code block options?
266+
*/
267+
min-height: 500px;
268+
}

docs/qwebr-release-notes.qmd

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ format:
1212
Features listed under the `-dev` version have not yet been solidified and may change at any point.
1313
:::
1414

15-
# 0.4.3-dev: ???? (??-??-????)
15+
# 0.4.3-dev.2: ???? (??-??-????)
1616

1717
## Features
1818

1919
- Upgraded the embedded version of webR to v0.4.0. ([#219](https://github.com/coatless/quarto-webr/pulls/219))
20+
- Added support for the `browse` event ([#223](https://github.com/coatless/quarto-webr/pulls/223), credit to [@dipterix](https://github.com/dipterix))
2021

2122
# 0.4.2: A Change Is Gonna Come (06-24-2024)
2223

docs/qwebr-theming.qmd

+5
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ Across both contexts, you'll find shared output element classes and names:
7373
- `id`: `#qwebr-output-graph-area-{{ID}}`
7474
- `class`: `.qwebr-output-graph-area`
7575

76+
- **Browse Output Area**: Any HTML widgets generated by your R code is displayed in this area.
77+
- `id`: `#qwebr-output-code-browse-{{ID}}`
78+
- `class`: `.qwebr-output-code-browse`
79+
80+
7681
The `{{ID}}` in these identifiers represents the instance of the element on the page, whether it's the first, second, or nth occurrence. You can customize their appearance by targeting the specific `id` or `class` attributes in your document's CSS, or you can include a separate CSS file to align with your design preferences.
7782

7883
## CSS & HTML Structure

0 commit comments

Comments
 (0)