diff --git a/altdoc/_extensions/coatless-quarto/panelize/_extension.yml b/altdoc/_extensions/coatless-quarto/panelize/_extension.yml index 2c18e88..fee6ea6 100644 --- a/altdoc/_extensions/coatless-quarto/panelize/_extension.yml +++ b/altdoc/_extensions/coatless-quarto/panelize/_extension.yml @@ -1,7 +1,7 @@ name: panelize title: Panelize code cells -author: Carlos Scheidegger and James Joseph Balamuta -version: 0.0.0-dev.1 +author: James Joseph Balamuta +version: 0.0.2 quarto-required: ">=1.4.554" contributes: filters: diff --git a/altdoc/_extensions/coatless-quarto/panelize/panelize.lua b/altdoc/_extensions/coatless-quarto/panelize/panelize.lua index a8e64ab..c831189 100644 --- a/altdoc/_extensions/coatless-quarto/panelize/panelize.lua +++ b/altdoc/_extensions/coatless-quarto/panelize/panelize.lua @@ -1,105 +1,208 @@ --- Function to process code blocks -local function clean_code_block(el, language) - - -- Check if the code block contains R code - if el.text:match("^```{{"..language) then - -- Remove the ```{{}} and ``` lines - local cleaned_text = el.text:gsub("```{{".. language .."}}\n", ""):gsub("\n```", "") - - -- Remove lines starting with #| (options) - cleaned_text = cleaned_text:gsub("#|.-\n", "") +-- Define helper types for clarity +---@class Block +---@field t string The type of the block +---@field attr table Block attributes +---@field content? table Block content +---@field classes table List of classes +---@field text? string Block text if CodeBlock + +---@class Cell +---@field code_blocks Block[] List of code blocks +---@field outputs Block[] List of output blocks +---@field language string Programming language + +---@class DocumentMetadata +---@field panelize table Configuration options +---@field handler_added boolean Flag for handler addition + +-- Store metadata at module level +---@type DocumentMetadata +local document_metadata = { + panelize = {}, + handler_added = false +} + +-- Helper function to detect language from a code block +---@param block Block The code block to analyze +---@return string|nil language The detected language +local function detect_language(block) + if block.attr.classes:includes("r") then + return "r" + elseif block.attr.classes:includes("python") then + return "python" + elseif block.text:match("^```{{r") then + return "r" + elseif block.text:match("^```{{python") then + return "python" + end + return nil +end - -- Add 'language' to the class list if not already present - if not el.attr.classes:includes(language) then - table.insert(el.attr.classes, 1, language) +-- Helper function to clean code block text +---@param block Block The code block to clean +---@param language string The programming language +---@param remove_fences boolean Whether to remove code fences and options +---@return string cleaned_text The processed text +local function clean_code_text(block, language, remove_fences) + local text = block.text + if remove_fences then + if text:match("^```{{" .. language .. "}") then + text = text:gsub("```{{" .. language .. "}}\n", ""):gsub("\n```", "") + end + text = text:gsub("#|.-\n", "") end + return text +end - -- Return the modified code block - return pandoc.CodeBlock(cleaned_text, el.attr) - end +-- Helper function to extract cell content +---@param cell_div Block The cell div block +---@return Cell cell The processed cell content +local function extract_cell_content(cell_div) + local cell = { + blocks = {}, -- Will store alternating code blocks and their outputs + language = nil + } + + -- Process blocks in sequence + for _, block in ipairs(cell_div.content) do + if block.t == "CodeBlock" and block.classes:includes("cell-code") then + table.insert(cell.blocks, {type = "code", content = block}) + -- Detect language from first code block if not already set + if not cell.language then + cell.language = detect_language(block) + end + elseif block.t == "Div" and ( + block.classes:includes("cell-output") or + block.classes:includes("cell-output-stdout") or + block.classes:includes("cell-output-display") + ) then + table.insert(cell.blocks, {type = "output", content = block}) + end + end + + return cell +end - -- If not an R code block, return unchanged - return el +-- Helper function to create tab content +---@param cell Cell The cell content +---@param tab_type string The type of tab ("result", "source", or "interactive") +---@return pandoc.List content The tab content +local function create_tab_content(cell, tab_type) + local content = pandoc.List() + + if tab_type == "interactive" then + -- For interactive tab, combine all code blocks into one + local combined_code = table.concat( + pandoc.List(cell.blocks) + :filter(function(block) return block.type == "code" end) + :map(function(block) return clean_code_text(block.content, cell.language, true) end), + "\n" + ) + + -- Create single code block with appropriate classes + local classes = cell.language == "r" and {"{webr-r}", "cell-code"} or {"{pyodide-python}", "cell-code"} + local attr = pandoc.Attr("", classes, {}) + content:insert(pandoc.CodeBlock(combined_code, attr)) + else + -- For result and source tabs, process blocks in sequence + for _, block in ipairs(cell.blocks) do + if block.type == "code" then + if tab_type == "result" then + -- For result tab, clean code but keep language class + local new_attr = block.content.attr:clone() + new_attr.classes = pandoc.List({cell.language}) + local cleaned_text = clean_code_text(block.content, cell.language, true) + content:insert(pandoc.CodeBlock(cleaned_text, new_attr)) + else + -- For source tab, use original code block + content:insert(block.content) + end + else -- output block + content:insert(block.content) + end + end + end + + return content end --- Helper function to clone and update code block attributes -local function clone_and_update_code_block(code_block, new_classes) - local new_attr = code_block.attr:clone() - new_attr.classes = pandoc.List(new_classes) - return pandoc.CodeBlock(code_block.text, new_attr) +-- Process metadata +function Meta(meta) + if meta and meta.panelize then + for key, value in pairs(meta.panelize) do + document_metadata.panelize[key] = pandoc.utils.stringify(value) + end + end + return meta end +-- Main processing function for divs function Div(div) - local to_webr = div.classes:includes("to-webr") - local to_pyodide = div.classes:includes("to-pyodide") - - -- Check if the `div` has the class "to-source"/"to-webr"/"to-pyodide" - if not (div.classes:includes("to-source") or to_webr or to_pyodide) then - return - end - - -- Initialize local variables for code block, cell output, and language - local code_block = nil - local cell_output = nil - local language = nil - - -- Walk through the content of the `div` to find `CodeBlock` and `Div` elements - div:walk({ - CodeBlock = function(code) - -- If a `CodeBlock` with the class "cell-code" is found, assign it to `code_block` - if code.classes:includes("cell-code") then - code_block = code - -- Determine the language of the code block - if code.classes:includes("r") or code.text:match("^```{{r") then - language = "r" - elseif code.classes:includes("python") or code.text:match("^```{{python") then - language = "python" - else - quarto.log.error("Please only specify either R or Python code cells inside of the `to-panel` div.") + -- Check for required classes + local to_webr = div.classes:includes("to-webr") + local to_pyodide = div.classes:includes("to-pyodide") + local to_source = div.classes:includes("to-source") + + if not (to_source or to_webr or to_pyodide) then + return div + end + + -- Find cell div + local cell_div = nil + for _, block in ipairs(div.content) do + if block.t == "Div" and block.classes:includes("cell") then + cell_div = block + break end - end - end, - Div = function(div) - -- If a `Div` with the class "cell-output" is found, assign it to `cell_output` - if div.classes:includes("cell-output") then - cell_output = div - end end - }) - - local cleaned_code_cell = clean_code_block(code_block, language) - - -- Determine the type of Tab to use - local tabs = nil - - -- Check if the language matches the required condition - if to_webr or to_pyodide then - -- Create a tab for the Result - local result_tab = quarto.Tab({ title = "Result", content = pandoc.List({code_block, cell_output}) }) - - -- Pick attribute classes - local code_block_attr_classes = to_webr and {"{webr-r}", "cell-code"} or {"{pyodide-python}", "cell-code"} - - -- Create a tab for the Source - local interactive_tab = quarto.Tab({ title = "Interactive", content = clone_and_update_code_block(code_block, code_block_attr_classes) }) - - -- Combine the tabs into a list - tabs = pandoc.List({ result_tab, interactive_tab }) - else - -- Create a tab for the Rendered - local rendered_tab = quarto.Tab({ title = "Result", content = pandoc.List({cleaned_code_cell, cell_output}) }) - -- Create a tab for the Source - local source_tab = quarto.Tab({ title = "Source", content = clone_and_update_code_block(code_block, {"md", "cell-code"}) }) - - -- Combine the tabs into a list - tabs = pandoc.List({ rendered_tab, source_tab }) - end + if not cell_div then + return div + end + + -- Extract cell content + local cell = extract_cell_content(cell_div) + + if not cell.language then + quarto.log.error("Please specify either R or Python code cells inside of the .to-* div.") + return div + end + + -- Create tabs + local tabs = pandoc.List() + + if to_webr or to_pyodide then + -- Interactive environment tabs + tabs:insert(quarto.Tab({ + title = "Result", + content = pandoc.Blocks(create_tab_content(cell, "result")) + })) + tabs:insert(quarto.Tab({ + title = "Interactive", + content = pandoc.Blocks(create_tab_content(cell, "interactive")) + })) + else + -- Source code tabs + tabs:insert(quarto.Tab({ + title = "Result", + content = pandoc.Blocks(create_tab_content(cell, "result")) + })) + tabs:insert(quarto.Tab({ + title = "Source", + content = pandoc.Blocks(create_tab_content(cell, "source")) + })) + end + + -- Return just the tabset, replacing the original div + return quarto.Tabset({ + level = 3, + tabs = tabs, + attr = pandoc.Attr("", {"panel-tabset"}, {}) + }) +end - -- Return a `quarto.Tabset` with the created tabs and specific attributes - return quarto.Tabset({ - level = 3, - tabs = tabs, - attr = pandoc.Attr("", {"panel-tabset"}) -- This attribute assignment shouldn't be necessary but addresses a known issue. Remove when using Quarto 1.5 or greater as required version. - }) -end \ No newline at end of file +-- Return the list of functions to register +return { + {Meta = Meta}, + {Div = Div} +} \ No newline at end of file diff --git a/altdoc/_extensions/coatless/webr/_extension.yml b/altdoc/_extensions/coatless/webr/_extension.yml index 8900e83..206c449 100644 --- a/altdoc/_extensions/coatless/webr/_extension.yml +++ b/altdoc/_extensions/coatless/webr/_extension.yml @@ -1,7 +1,7 @@ name: webr title: Embedded webr code cells author: James Joseph Balamuta -version: 0.4.2-dev.6 +version: 0.4.3-dev.2 quarto-required: ">=1.4.554" contributes: filters: diff --git a/altdoc/_extensions/coatless/webr/qwebr-cell-initialization.js b/altdoc/_extensions/coatless/webr/qwebr-cell-initialization.js index 828bc94..548172a 100644 --- a/altdoc/_extensions/coatless/webr/qwebr-cell-initialization.js +++ b/altdoc/_extensions/coatless/webr/qwebr-cell-initialization.js @@ -78,6 +78,10 @@ qwebrInstance.then( break; case 'setup': const activeDiv = document.getElementById(`qwebr-noninteractive-setup-area-${qwebrCounter}`); + + // Store code in history + qwebrLogCodeToHistory(cellCode, entry.options); + // Run the code in a non-interactive state with all output thrown away await mainWebR.evalRVoid(`${cellCode}`); break; diff --git a/altdoc/_extensions/coatless/webr/qwebr-compute-engine.js b/altdoc/_extensions/coatless/webr/qwebr-compute-engine.js index d35bc85..a35ea11 100644 --- a/altdoc/_extensions/coatless/webr/qwebr-compute-engine.js +++ b/altdoc/_extensions/coatless/webr/qwebr-compute-engine.js @@ -3,7 +3,7 @@ globalThis.qwebrIsObjectEmpty = function (arr) { return Object.keys(arr).length === 0; } -// Global version of the Escape HTML function that converts HTML +// Global version of the Escape HTML function that converts HTML // characters to their HTML entities. globalThis.qwebrEscapeHTMLCharacters = function(unsafe) { return unsafe @@ -12,7 +12,7 @@ globalThis.qwebrEscapeHTMLCharacters = function(unsafe) { .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); - }; +}; // Passthrough results globalThis.qwebrIdentity = function(x) { @@ -24,11 +24,39 @@ globalThis.qwebrPrefixComment = function(x, comment) { return `${comment}${x}`; }; +// Function to store the code in the history +globalThis.qwebrLogCodeToHistory = function(codeToRun, options) { + qwebrRCommandHistory.push( + `# Ran code in ${options.label} at ${new Date().toLocaleString()} ----\n${codeToRun}` + ); +}; + +// Function to attach a download button onto the canvas +// allowing the user to download the image. +function qwebrImageCanvasDownloadButton(canvas, canvasContainer) { + + // Create the download button + const downloadButton = document.createElement('button'); + downloadButton.className = 'qwebr-canvas-image-download-btn'; + downloadButton.textContent = 'Download Image'; + canvasContainer.appendChild(downloadButton); + + // Trigger a download of the image when the button is clicked + downloadButton.addEventListener('click', function() { + const image = canvas.toDataURL('image/png'); + const link = document.createElement('a'); + link.href = image; + link.download = 'qwebr-canvas-image.png'; + link.click(); + }); +} + + // Function to parse the pager results -globalThis.qwebrParseTypePager = async function (msg) { +globalThis.qwebrParseTypePager = async function (msg) { // Split out the event data - const { path, title, deleteFile } = msg.data; + const { path, title, deleteFile } = msg.data; // Process the pager data by reading the information from disk const paged_data = await mainWebR.FS.readFile(path).then((data) => { @@ -37,7 +65,7 @@ globalThis.qwebrParseTypePager = async function (msg) { // Remove excessive backspace characters until none remain while(content.match(/.[\b]/)){ - content = content.replace(/.[\b]/g, ''); + content = content.replace(/.[\b]/g, ''); } // Returned cleaned data @@ -45,22 +73,41 @@ globalThis.qwebrParseTypePager = async function (msg) { }); // Unlink file if needed - if (deleteFile) { - await mainWebR.FS.unlink(path); - } + if (deleteFile) { + await mainWebR.FS.unlink(path); + } // Return extracted data with spaces return paged_data; -} +}; + + +// Function to parse the browse results +globalThis.qwebrParseTypeBrowse = async function (msg) { + + // msg.type === "browse" + const path = msg.data.url; + + // Process the browse data by reading the information from disk + const browse_data = await mainWebR.FS.readFile(path).then((data) => { + // Obtain the file content + let content = new TextDecoder().decode(data); + + return content; + }); + + // Return extracted data as-is + return browse_data; +}; // Function to run the code using webR and parse the output globalThis.qwebrComputeEngine = async function( - codeToRun, - elements, + codeToRun, + elements, options) { // Call into the R compute engine that persists within the document scope. - // To be prepared for all scenarios, the following happens: + // To be prepared for all scenarios, the following happens: // 1. We setup a canvas device to write to by making a namespace call into the {webr} package // 2. We use values inside of the options array to set the figure size. // 3. We capture the output stream information (STDOUT and STERR) @@ -80,11 +127,11 @@ globalThis.qwebrComputeEngine = async function( 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"] + let fig_width = options["fig-width"] * options["dpi"]; + let fig_height = options["fig-height"] * options["dpi"]; // Initialize webR await mainWebR.init(); @@ -96,7 +143,7 @@ globalThis.qwebrComputeEngine = async function( captureConditions: false, // env: webR.objs.emptyEnv, // maintain a global environment for webR v0.2.0 }; - + // Determine if the browser supports OffScreen if (qwebrOffScreenCanvasSupport()) { // Mirror default options of webr::canvas() @@ -113,6 +160,9 @@ globalThis.qwebrComputeEngine = async function( captureOutputOptions.captureGraphics = false; } + // Store the code to run in history + qwebrLogCodeToHistory(codeToRun, options); + // Setup a webR canvas by making a namespace call into the {webr} package // Evaluate the R code // Remove the active canvas silently @@ -125,18 +175,18 @@ globalThis.qwebrComputeEngine = async function( // Start attempting to parse the result data processResultOutput:try { - + // Avoid running through output processing - if (options.results === "hide" || options.output === "false") { - break processResultOutput; + if (options.results === "hide" || options.output === "false") { + 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`. + // Require both `warning` and `message` to be true to display `STDErr`. const out = result.output .filter( - evt => evt.type === "stdout" || - ( evt.type === "stderr" && (options.warning === "true" && options.message === "true")) + evt => evt.type === "stdout" || + ( evt.type === "stderr" && (options.warning === "true" && options.message === "true")) ) .map((evt, index) => { const className = `qwebr-output-code-${evt.type}`; @@ -148,15 +198,31 @@ globalThis.qwebrComputeEngine = async function( // Clean the state // We're now able to process pager events. - // As a result, we cannot maintain a true 1-to-1 output order + // As a result, we cannot maintain a true 1-to-1 output order // without individually feeding each line const msgs = await mainWebR.flush(); // Use `map` to process the filtered "pager" events asynchronously - const pager = await Promise.all( - msgs.filter(msg => msg.type === 'pager').map( + const pager = []; + const browse = []; + + await Promise.all( + msgs.map( async (msg) => { - return await qwebrParseTypePager(msg); + + const msgType = msg.type || "unknown"; + + switch(msgType) { + case 'pager': + const pager_data = await qwebrParseTypePager(msg); + pager.push(pager_data); + break; + case 'browse': + const browse_data = await qwebrParseTypeBrowse(msg); + browse.push(browse_data); + break; + } + return; } ) ); @@ -188,14 +254,20 @@ globalThis.qwebrComputeEngine = async function( // Determine if we have graphs to display if (result.images.length > 0) { + // Create figure element - const figureElement = document.createElement('figure'); + const figureElement = document.createElement("figure"); + figureElement.className = "qwebr-canvas-image"; // Place each rendered graphic onto a canvas element result.images.forEach((img) => { + // Construct canvas for object const canvas = document.createElement("canvas"); + // Add an image download button + qwebrImageCanvasDownloadButton(canvas, figureElement); + // Set canvas size to image canvas.width = img.width; canvas.height = img.height; @@ -213,9 +285,10 @@ globalThis.qwebrComputeEngine = async function( // Draw image onto Canvas const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, img.width, img.height); - + // Append canvas to figure output area figureElement.appendChild(canvas); + }); if (options['fig-cap']) { @@ -223,28 +296,46 @@ globalThis.qwebrComputeEngine = async function( const figcaptionElement = document.createElement('figcaption'); figcaptionElement.innerText = options['fig-cap']; // Append figcaption to figure - figureElement.appendChild(figcaptionElement); + figureElement.appendChild(figcaptionElement); } elements.outputGraphDiv.appendChild(figureElement); + } // Display the pager data - if (pager) { - // Use the `pre` element to preserve whitespace. - pager.forEach((paged_data, index) => { - let pre_pager = document.createElement("pre"); - pre_pager.innerText = paged_data; - pre_pager.classList.add("qwebr-output-code-pager"); - pre_pager.setAttribute("id", `qwebr-output-code-pager-editor-${elements.id}-result-${index + 1}`); - elements.outputCodeDiv.appendChild(pre_pager); - }); + if (pager.length > 0) { + // Use the `pre` element to preserve whitespace. + pager.forEach((paged_data, index) => { + const pre_pager = document.createElement("pre"); + pre_pager.innerText = paged_data; + pre_pager.classList.add("qwebr-output-code-pager"); + pre_pager.setAttribute("id", `qwebr-output-code-pager-editor-${elements.id}-result-${index + 1}`); + elements.outputCodeDiv.appendChild(pre_pager); + }); + } + + // Display the browse data + if (browse.length > 0) { + // Use the `pre` element to preserve whitespace. + browse.forEach((browse_data, index) => { + const iframe_browse = document.createElement('iframe'); + iframe_browse.classList.add("qwebr-output-code-browse"); + iframe_browse.setAttribute("id", `qwebr-output-code-browse-editor-${elements.id}-result-${index + 1}`); + iframe_browse.style.width = "100%"; + iframe_browse.style.minHeight = "500px"; + elements.outputCodeDiv.appendChild(iframe_browse); + + iframe_browse.contentWindow.document.open(); + iframe_browse.contentWindow.document.write(browse_data); + iframe_browse.contentWindow.document.close(); + }); } } finally { // Clean up the remaining code mainWebRCodeShelter.purge(); } -} +}; // Function to execute the code (accepts code as an argument) globalThis.qwebrExecuteCode = async function ( @@ -254,12 +345,12 @@ globalThis.qwebrExecuteCode = async function ( // If options are not passed, we fall back on the bare minimum to handle the computation if (qwebrIsObjectEmpty(options)) { - options = { - "context": "interactive", - "fig-width": 7, "fig-height": 5, - "out-width": "700px", "out-height": "", + options = { + "context": "interactive", + "fig-width": 7, "fig-height": 5, + "out-width": "700px", "out-height": "", "dpi": 72, - "results": "markup", + "results": "markup", "warning": "true", "message": "true", }; } diff --git a/altdoc/_extensions/coatless/webr/qwebr-document-engine-initialization.js b/altdoc/_extensions/coatless/webr/qwebr-document-engine-initialization.js index 1d447e8..723220a 100644 --- a/altdoc/_extensions/coatless/webr/qwebr-document-engine-initialization.js +++ b/altdoc/_extensions/coatless/webr/qwebr-document-engine-initialization.js @@ -58,11 +58,15 @@ globalThis.qwebrInstance = import(qwebrCustomizedWebROptions.baseURL + "webr.mjs // Setup a shelter globalThis.mainWebRCodeShelter = await new mainWebR.Shelter(); - // Setup a pager to allow processing help documentation - await mainWebR.evalRVoid('webr::pager_install()'); + // Setup a pager to allow processing help documentation + await mainWebR.evalRVoid('webr::pager_install()'); + + // Setup a viewer to allow processing htmlwidgets. + // This might not be available in old webr version + await mainWebR.evalRVoid('try({ webr::viewer_install() })'); // Override the existing install.packages() to use webr::install() - await mainWebR.evalRVoid('webr::shim_install()'); + await mainWebR.evalRVoid('webr::shim_install()'); // Specify the repositories to pull from // Note: webR does not use the `repos` option, but instead uses `webr_pkg_repos` diff --git a/altdoc/_extensions/coatless/webr/qwebr-document-history.js b/altdoc/_extensions/coatless/webr/qwebr-document-history.js new file mode 100644 index 0000000..df00091 --- /dev/null +++ b/altdoc/_extensions/coatless/webr/qwebr-document-history.js @@ -0,0 +1,110 @@ +// Define a global storage and retrieval solution ---- + +// Store commands executed in R +globalThis.qwebrRCommandHistory = []; + +// Function to retrieve the command history +globalThis.qwebrFormatRHistory = function() { + return qwebrRCommandHistory.join("\n\n"); +} + +// Retrieve HTML Elements ---- + +// Get the command modal +const command_history_modal = document.getElementById("qwebr-history-modal"); + +// Get the button that opens the command modal +const command_history_btn = document.getElementById("qwebrRHistoryButton"); + +// Get the element that closes the command modal +const command_history_close_span = document.getElementById("qwebr-command-history-close-btn"); + +// Get the download button for r history information +const command_history_download_btn = document.getElementById("qwebr-download-history-btn"); + +// Plug in command history into modal/download button ---- + +// Function to populate the modal with command history +function populateCommandHistoryModal() { + document.getElementById("qwebr-command-history-contents").innerHTML = qwebrFormatRHistory() || "No commands have been executed yet."; +} + +// Function to format the current date and time to +// a string with the format YYYY-MM-DD-HH-MM-SS +function formatDateTime() { + const now = new Date(); + + const year = now.getFullYear(); + const day = String(now.getDate()).padStart(2, '0'); + const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are zero-based + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const seconds = String(now.getSeconds()).padStart(2, '0'); + + return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`; +} + + +// Function to convert document title with datetime to a safe filename +function safeFileName() { + // Get the current page title + let pageTitle = document.title; + + // Combine the current page title with the current date and time + let pageNameWithDateTime = `Rhistory-${pageTitle}-${formatDateTime()}`; + + // Replace unsafe characters with safe alternatives + let safeFilename = pageNameWithDateTime.replace(/[\\/:\*\?! "<>\|]/g, '-'); + + return safeFilename; +} + + +// Function to download list contents as text file +function downloadRHistory() { + // Get the current page title + datetime and use it as the filename + const filename = `${safeFileName()}.R`; + + // Get the text contents of the R History list + const text = qwebrFormatRHistory(); + + // Create a new Blob object with the text contents + const blob = new Blob([text], { type: 'text/plain' }); + + // Create a new anchor element for the download + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = URL.createObjectURL(blob); + a.download = filename; + + // Append the anchor to the body, click it, and remove it + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); +} + +// Register event handlers ---- + +// When the user clicks the View R History button, open the command modal +command_history_btn.onclick = function() { + populateCommandHistoryModal(); + command_history_modal.style.display = "block"; +} + +// When the user clicks on (x), close the command modal +command_history_close_span.onclick = function() { + command_history_modal.style.display = "none"; +} + +// When the user clicks anywhere outside of the command modal, close it +window.onclick = function(event) { + if (event.target == command_history_modal) { + command_history_modal.style.display = "none"; + } +} + +// Add an onclick event listener to the download button so that +// the user can download the R history as a text file +command_history_download_btn.onclick = function() { + downloadRHistory(); +}; \ No newline at end of file diff --git a/altdoc/_extensions/coatless/webr/qwebr-document-status.js b/altdoc/_extensions/coatless/webr/qwebr-document-status.js index b72910a..71441df 100644 --- a/altdoc/_extensions/coatless/webr/qwebr-document-status.js +++ b/altdoc/_extensions/coatless/webr/qwebr-document-status.js @@ -28,6 +28,41 @@ globalThis.qwebrUpdateStatusHeader = function(message) { ${message}`; } +// Function to return true if element is found, false if not +globalThis.qwebrCheckHTMLElementExists = function(selector) { + const element = document.querySelector(selector); + return !!element; +} + +// Function that detects whether reveal.js slides are present +globalThis.qwebrIsRevealJS = function() { + // If the '.reveal .slides' selector exists, RevealJS is likely present + return qwebrCheckHTMLElementExists('.reveal .slides'); +} + +// Initialize the Quarto sidebar element +function qwebrSetupQuartoSidebar() { + var newSideBarDiv = document.createElement('div'); + newSideBarDiv.id = 'quarto-margin-sidebar'; + newSideBarDiv.className = 'sidebar margin-sidebar'; + newSideBarDiv.style.top = '0px'; + newSideBarDiv.style.maxHeight = 'calc(0px + 100vh)'; + + return newSideBarDiv; +} + +// Position the sidebar in the document +function qwebrPlaceQuartoSidebar() { + // Get the reference to the element with id 'quarto-document-content' + var referenceNode = document.getElementById('quarto-document-content'); + + // Create the new div element + var newSideBarDiv = qwebrSetupQuartoSidebar(); + + // Insert the new div before the 'quarto-document-content' element + referenceNode.parentNode.insertBefore(newSideBarDiv, referenceNode); +} + function qwebrPlaceMessageContents(content, html_location = "title-block-header", revealjs_location = "title-slide") { // Get references to header elements @@ -49,6 +84,7 @@ function qwebrPlaceMessageContents(content, html_location = "title-block-header" } + function qwebrOffScreenCanvasSupportWarningMessage() { // Verify canvas is supported. @@ -154,5 +190,179 @@ function displayStartupMessage(showStartupMessage, showHeaderMessage) { qwebrPlaceMessageContents(quartoTitleMeta); } +function qwebrAddCommandHistoryModal() { + // Create the modal div + var modalDiv = document.createElement('div'); + modalDiv.id = 'qwebr-history-modal'; + modalDiv.className = 'qwebr-modal'; + + // Create the modal content div + var modalContentDiv = document.createElement('div'); + modalContentDiv.className = 'qwebr-modal-content'; + + // Create the span for closing the modal + var closeSpan = document.createElement('span'); + closeSpan.id = 'qwebr-command-history-close-btn'; + closeSpan.className = 'qwebr-modal-close'; + closeSpan.innerHTML = '×'; + + // Create the h1 element for the modal + var modalH1 = document.createElement('h1'); + modalH1.textContent = 'R History Command Contents'; + + // Create an anchor element for downloading the Rhistory file + var downloadLink = document.createElement('a'); + downloadLink.href = '#'; + downloadLink.id = 'qwebr-download-history-btn'; + downloadLink.className = 'qwebr-download-btn'; + + // Create an 'i' element for the icon + var icon = document.createElement('i'); + icon.className = 'bi bi-file-code'; + + // Append the icon to the anchor element + downloadLink.appendChild(icon); + + // Add the text 'Download R History' to the anchor element + downloadLink.appendChild(document.createTextNode(' Download R History File')); + + // Create the pre for command history contents + var commandContentsPre = document.createElement('pre'); + commandContentsPre.id = 'qwebr-command-history-contents'; + commandContentsPre.className = 'qwebr-modal-content-code'; + + // Append the close span, h1, and history contents pre to the modal content div + modalContentDiv.appendChild(closeSpan); + modalContentDiv.appendChild(modalH1); + modalContentDiv.appendChild(downloadLink); + modalContentDiv.appendChild(commandContentsPre); + + // Append the modal content div to the modal div + modalDiv.appendChild(modalContentDiv); + + // Append the modal div to the body + document.body.appendChild(modalDiv); +} + +function qwebrRegisterRevealJSCommandHistoryModal() { + // Select the