Skip to content

Commit

Permalink
Add Rhistory tracking (#213)
Browse files Browse the repository at this point in the history
* Add Rhistory tracking

* Redo check on sidebar after attempting to create ...

* Rewrite safeFileName to use a date string of YYYY-MM-DD-HH-MM-SS and regex cleared string to avoid special characters.
  • Loading branch information
coatless authored Jun 16, 2024
1 parent 088bbd2 commit 1634d98
Show file tree
Hide file tree
Showing 7 changed files with 390 additions and 5 deletions.
4 changes: 4 additions & 0 deletions _extensions/webr/qwebr-cell-initialization.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions _extensions/webr/qwebr-compute-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ 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 parse the pager results
globalThis.qwebrParseTypePager = async function (msg) {

Expand Down Expand Up @@ -113,6 +120,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
Expand Down
110 changes: 110 additions & 0 deletions _extensions/webr/qwebr-document-history.js
Original file line number Diff line number Diff line change
@@ -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 <span> 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 <span> (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();
};
207 changes: 207 additions & 0 deletions _extensions/webr/qwebr-document-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,41 @@ globalThis.qwebrUpdateStatusHeader = function(message) {
<span>${message}</span>`;
}

// 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
Expand All @@ -49,6 +84,7 @@ function qwebrPlaceMessageContents(content, html_location = "title-block-header"
}



function qwebrOffScreenCanvasSupportWarningMessage() {

// Verify canvas is supported.
Expand Down Expand Up @@ -154,5 +190,176 @@ 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 = '&times;';

// 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 <ul> element inside the <div> with data-panel="Custom0"
let ulElement = document.querySelector('div[data-panel="Custom0"] > ul.slide-menu-items');

// Find the last <li> element with class slide-tool-item
let lastItem = ulElement.querySelector('li.slide-tool-item:last-child');

// Calculate the next data-item value
let nextItemValue = 0;
if (lastItem) {
nextItemValue = parseInt(lastItem.dataset.item) + 1;
}

// Create a new <li> element
let newListItem = document.createElement('li');
newListItem.className = 'slide-tool-item';
newListItem.dataset.item = nextItemValue.toString(); // Set the next available data-item value

// Create the <a> element inside the <li>
let newLink = document.createElement('a');
newLink.href = '#';
newLink.id = 'qwebrRHistoryButton'; // Set the ID for the new link

// Create the <kbd> element inside the <a>
let newKbd = document.createElement('kbd');
newKbd.textContent = ' '; // Set to empty as we are not registering a keyboard shortcut

// Create text node for the link text
let newText = document.createTextNode(' View R History');

// Append <kbd> and text node to <a>
newLink.appendChild(newKbd);
newLink.appendChild(newText);

// Append <a> to <li>
newListItem.appendChild(newLink);

// Append <li> to <ul>
ulElement.appendChild(newListItem);
}

// Handle setting up the R history modal
function qwebrCodeLinks() {

if (qwebrIsRevealJS()) {
qwebrRegisterRevealJSCommandHistoryModal();
return;
}

// Create the container div
var containerDiv = document.createElement('div');
containerDiv.className = 'quarto-code-links';

// Create the h2 element
var h2 = document.createElement('h2');
h2.textContent = 'webR Code Links';

// Create the ul element
var ul = document.createElement('ul');

// Create the li element
var li = document.createElement('li');

// Create the a_history_btn element
var a_history_btn = document.createElement('a');
a_history_btn.href = 'javascript:void(0)';
a_history_btn.setAttribute('id', 'qwebrRHistoryButton');

// Create the i_history_btn element
var i_history_btn = document.createElement('i');
i_history_btn.className = 'bi bi-file-code';

// Create the text node for the link text
var text_history_btn = document.createTextNode('View R History');

// Append the icon element and link text to the a element
a_history_btn.appendChild(i_history_btn);
a_history_btn.appendChild(text_history_btn);

// Append the a element to the li element
li.appendChild(a_history_btn);

// Append the li element to the ul element
ul.appendChild(li);

// Append the h2 and ul elements to the container div
containerDiv.appendChild(h2);
containerDiv.appendChild(ul);

// Append the container div to the element with the ID 'quarto-margin-sidebar'
var sidebar = document.getElementById('quarto-margin-sidebar');

// If the sidebar element is not found, create it
if(!sidebar) {
qwebrPlaceQuartoSidebar();
}

// Re-select the sidebar element (if it was just created)
sidebar = document.getElementById('quarto-margin-sidebar');


// If the sidebar element exists, append the container div to it
if(sidebar) {
// Append the container div to the sidebar
sidebar.appendChild(containerDiv);
} else {
// Get a debugger ...
console.warn('Element with ID "quarto-margin-sidebar" not found.');
}
}

// Call the function to append the code links for qwebR into the right sidebar
qwebrCodeLinks();

// Add the command history modal
qwebrAddCommandHistoryModal();

displayStartupMessage(qwebrShowStartupMessage, qwebrShowHeaderMessage);
qwebrOffScreenCanvasSupportWarningMessage();
Loading

0 comments on commit 1634d98

Please sign in to comment.