chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
+190
View File
@@ -0,0 +1,190 @@
// Create chat-widget div
var chatWidgetDiv = document.createElement('div');
chatWidgetDiv.className = 'chat-widget';
chatWidgetDiv.innerHTML = `
<button id="openChatBtn">Ask AI</button>
`;
document.body.appendChild(chatWidgetDiv);
// Create chat-popup div
const date = new Date();
var chatPopupDiv = document.createElement('div');
chatPopupDiv.className = 'chat-popup';
chatPopupDiv.id = 'chatPopup';
chatPopupDiv.innerHTML = `
<div class="chatHeader">
<div class="header-wrapper">
<h3 class="assistant-title">Ray Docs AI - Ask a question</h3>
</div>
<button id="closeChatBtn" class="btn">
<i class="fas fa-times"></i>
</button>
</div>
<div id="chatContainer" class="chatContentContainer">
<div id="result">
Please note that the results of this bot are automated and may be incorrect or contain inappropriate information.
<div id="anchor"></div>
</div>
<div class="input-group">
<textarea id="searchBar" class="input" rows="3" placeholder="Do not include any personal or confidential information."></textarea>
<button id="searchBtn" class="btn btn-primary">Ask AI</button>
</div>
</div>
<div class="chatFooter text-right p-2">
© Copyright ${date.getFullYear()}, The Ray Team.
</div>
`;
chatPopupDiv.messages = [];
document.body.appendChild(chatPopupDiv);
const anchorDiv = document.getElementById('anchor');
const chatContainerDiv = document.getElementById('chatContainer');
const blurDiv = document.createElement('div');
blurDiv.id = 'blurDiv';
blurDiv.classList.add('blurDiv-hidden');
document.body.appendChild(blurDiv);
// blur background when chat popup is open
document.getElementById('openChatBtn').addEventListener('click', function () {
document.querySelector('.search-button__wrapper').classList.remove('show');
document.getElementById('chatPopup').style.display = 'block';
blurDiv.classList.remove('blurDiv-hidden');
});
// un-blur background when chat popup is closed
document.getElementById('closeChatBtn').addEventListener('click', function () {
document.getElementById('chatPopup').style.display = 'none';
blurDiv.classList.add('blurDiv-hidden');
});
// set code highlighting options
marked.setOptions({
renderer: new marked.Renderer(),
highlight: function (code) {
return hljs.highlight('python', code).value;
},
});
function highlightCode() {
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightBlock(block);
});
}
function renderCopyButtons(resultDiv) {
let preElements = resultDiv.querySelectorAll('pre');
preElements.forEach((preElement, index) => {
preElement.style.position = 'relative';
let uniqueId = `button-id-${index}`;
preElement.id = uniqueId;
// Set the proper attributes to the button to make is a sphinx copy button
let copyButton = document.createElement('button');
copyButton.className = 'copybtn o-tooltip--left';
copyButton.setAttribute('data-tooltip', 'Copy');
copyButton.setAttribute('data-clipboard-target', `#${uniqueId}`);
copyButton.style.position = 'absolute';
copyButton.style.top = '10px';
copyButton.style.right = '10px';
copyButton.style.opacity = 'inherit';
let imgElement = document.createElement('img');
imgElement.src = window.data.copyIconSrc;
imgElement.alt = 'Copy to clipboard';
copyButton.appendChild(imgElement);
preElement.appendChild(copyButton);
});
}
const searchBar = document.getElementById('searchBar');
const searchBtn = document.getElementById('searchBtn');
function rayAssistant(event) {
const resultDiv = document.getElementById('result');
const searchTerm = searchBar.value;
// handle empty input
if (!searchTerm) {
return;
}
if (
event.type === 'click' ||
(event.type === 'keydown' && event.key === 'Enter')
) {
// for improved UX, we want to equate a carriage return as hitting the send button and prevent default behavior of entering a new line
if (event.type === 'keydown' && event.key === 'Enter') {
event.preventDefault();
}
// clear search bar value and change placeholder to prompt user to ask follow up question
searchBar.value = '';
searchBar.placeholder = 'Ask follow up question here';
// Add query to the list of messages
chatPopupDiv.messages.push({role: 'user', content: searchTerm});
let msgBlock = document.createElement('div');
resultDiv.insertBefore(msgBlock, anchorDiv);
let divider = document.createElement('hr');
msgBlock.appendChild(divider);
let msgHeader = document.createElement('div');
msgHeader.style.fontWeight = 'bold';
// capitalize first letter of question header
msgHeader.innerHTML =
searchTerm.charAt(0).toUpperCase() + searchTerm.slice(1);
msgBlock.appendChild(msgHeader);
async function readStream() {
try {
const response = await fetch(
'https://ray-assistant-public-bxauk.cld-kvedzwag2qa8i5bj.s.anyscaleuserdata.com/chat',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({messages: chatPopupDiv.messages}),
}
);
const reader = response.body.getReader();
let decoder = new TextDecoder('utf-8');
let msgContent = document.createElement('div');
msgBlock.appendChild(msgContent);
msgContent.innerHTML = '';
let collectChunks = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
renderCopyButtons(resultDiv);
break;
}
const chunk = decoder.decode(value, {stream: true});
collectChunks += chunk;
let html = marked.parse(collectChunks);
html = DOMPurify.sanitize(html);
msgContent.innerHTML = html;
highlightCode();
}
chatPopupDiv.messages.push({role: 'assistant', content: collectChunks});
// we want to autoscroll to the bottom of the chat container after the answer is streamed in
chatContainerDiv.scrollTo(0, chatContainerDiv.scrollHeight);
} catch (error) {
console.error('Fetch API failed:', error);
}
}
readStream().then((res) => console.log(res));
}
}
searchBtn.addEventListener('click', rayAssistant);
searchBar.addEventListener('keydown', rayAssistant);
+83
View File
@@ -0,0 +1,83 @@
// Handlers for CSAT events
/**
* Upvote or downvote the documentation via Google Analytics.
*
* @param {string} vote 'Yes' or 'No' vote to send as feedback
*/
function sendVote(vote) {
if (typeof window.dataLayer === 'undefined' || !Array.isArray(window.dataLayer)) {
console.warn('Google Tag Manager dataLayer not available - CSAT vote not tracked');
return;
}
window.dataLayer.push({
event: 'csat_vote',
vote_type: vote,
category: 'CSAT',
page_location: window.location.href,
page_title: document.title,
value: vote === 'Yes' ? 1 : 0
});
}
/**
* Documentation feedback via Google Analytics
* @param {string} text Text to send as feedback
*/
function sendFeedback(text) {
if (typeof window.dataLayer === 'undefined' || !Array.isArray(window.dataLayer)) {
console.warn('Google Tag Manager dataLayer not available - CSAT feedback not tracked');
return;
}
window.dataLayer.push({
event: 'csat_feedback',
feedback_text: text.substring(0, 500),
category: 'CSAT',
page_location: window.location.href,
page_title: document.title,
feedback_length: text.length
});
}
window.addEventListener("DOMContentLoaded", () => {
const yesButton = document.getElementById("csat-yes");
const noButton = document.getElementById("csat-no");
const yesIcon = document.getElementById("csat-yes-icon");
const noIcon = document.getElementById("csat-no-icon");
const text = document.getElementById("csat-textarea");
const submitButton = document.getElementById("csat-submit");
const csatGroup = document.getElementById("csat-textarea-group");
const csatFeedbackReceived = document.getElementById("csat-feedback-received");
const csatInputs = document.getElementById("csat-inputs")
yesButton.addEventListener("click", () => {
text.placeholder = "We're glad you found it helpful. If you have any additional feedback, please let us know.";
csatGroup.classList.remove("csat-hidden");
yesIcon.classList.remove("csat-hidden");
noIcon.classList.add("csat-hidden");
yesButton.classList.add("csat-button-active");
noButton.classList.remove("csat-button-active");
submitButton.scrollIntoView();
sendVote('Yes');
})
noButton.addEventListener("click", () => {
text.placeholder = "We value your feedback. Please let us know how we can improve our documentation.";
csatGroup.classList.remove("csat-hidden");
yesIcon.classList.add("csat-hidden");
noIcon.classList.remove("csat-hidden");
yesButton.classList.remove("csat-button-active");
noButton.classList.add("csat-button-active");
submitButton.scrollIntoView();
sendVote('No');
})
submitButton.addEventListener("click", () => {
csatGroup.classList.add("csat-hidden");
csatInputs.classList.add("csat-hidden");
csatFeedbackReceived.classList.remove("csat-hidden");
sendFeedback(text.value);
}, {once: true})
})
+95
View File
@@ -0,0 +1,95 @@
let new_termynals = [];
function createTermynals() {
const containers = document.getElementsByClassName("termynal");
Array.from(containers).forEach(addTermynal);
}
function addTermynal(container) {
const t = new Termynal(container, {
noInit: true,
});
new_termynals.push(t);
}
// Initialize Termynals that are visible on the page. Once initialized, remove
// the Termynal from terminals that remain to be loaded.
function loadVisibleTermynals() {
new_termynals = new_termynals.filter(termynal => {
if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) {
termynal.init();
return false;
}
return true;
});
}
// Store the state of the page in the browser's local storage.
// For now this includes just the sidebar scroll position.
document.addEventListener("DOMContentLoaded", () => {
const sidebar = document.getElementById("main-sidebar")
window.addEventListener("beforeunload", () => {
if (sidebar) {
localStorage.setItem("scroll", sidebar.scrollTop)
}
})
const storedScrollPosition = localStorage.getItem("scroll")
if (storedScrollPosition) {
if (sidebar) {
sidebar.scrollTop = storedScrollPosition;
}
localStorage.removeItem("scroll");
}
})
// Send GA events any time a code block is copied
document.addEventListener("DOMContentLoaded", function() {
let codeButtons = document.querySelectorAll(".copybtn");
for (let i = 0; i < codeButtons.length; i++) {
const button = codeButtons[i];
button.addEventListener("click", function() {
if (typeof window.dataLayer === 'undefined' || !Array.isArray(window.dataLayer)) {
console.warn('Google Tag Manager dataLayer not available - code copy not tracked');
return;
}
window.dataLayer.push({
event: "code_copy_click",
category: "ray_docs_copy_code",
page_location: window.location.href,
page_title: document.title,
button_target: button.getAttribute("data-clipboard-target") || "unknown",
value: 1,
});
});
}
});
document.addEventListener("DOMContentLoaded", function() {
let anyscaleButton = document.getElementById("try-anyscale")
if (anyscaleButton) {
anyscaleButton.onclick = () => {
if (typeof window.dataLayer === 'undefined' || !Array.isArray(window.dataLayer)) {
console.warn('Google Tag Manager dataLayer not available - try anyscale click not tracked');
return;
}
window.dataLayer.push({
event: "try_anyscale_click",
category: "TryAnyscale",
page_location: window.location.href,
page_title: document.title,
link_url: "https://www.anyscale.com",
value: 1,
});
window.open('https://www.anyscale.com', '_blank');
}
}
});
window.addEventListener("scroll", loadVisibleTermynals);
createTermynals();
loadVisibleTermynals();
@@ -0,0 +1,24 @@
// Dismissable banner functionality
document.addEventListener('DOMContentLoaded', function () {
const banner = document.querySelector('.bd-header-announcement');
const closeButton = document.getElementById('close-banner');
const bannerKey = 'ray-docs-banner-dismissed';
// Check if banner was previously dismissed
if (localStorage.getItem(bannerKey) === 'true') {
if (banner) {
banner.style.display = 'none';
}
return;
}
// Add click handler for close button
if (closeButton) {
closeButton.addEventListener('click', function () {
if (banner) {
banner.style.display = 'none';
localStorage.setItem(bannerKey, 'true');
}
});
}
});
+209
View File
@@ -0,0 +1,209 @@
/**
* Get the status (checked/unchecked) for each filter.
*
* @returns {Object} Arrays of the name and status of each filter, grouped together into filter
* groups.
*/
function getFilterStatuses() {
const useCases = Array.from(
document.querySelectorAll('#use-case-dropdown .checkbox-container'),
).map((label) => {
return {
name: label.textContent.toLowerCase(),
isChecked: label.querySelector('input').checked,
};
});
const libraries = Array.from(
document.querySelectorAll('#library-dropdown .checkbox-container'),
).map((label) => {
return {
name: label.textContent.toLowerCase(),
isChecked: label.querySelector('input').checked,
};
});
const frameworks = Array.from(
document.querySelectorAll('#framework-dropdown .checkbox-container'),
).map((label) => {
return {
name: label.textContent.toLowerCase(),
isChecked: label.querySelector('input').checked,
};
});
const contributor = Array.from(
document.querySelectorAll('#all-examples-dropdown .checkbox-container'),
).map((label) => {
const inputElement = label.querySelector('input');
return {
name: inputElement.id.replace('-checkbox', ''),
isChecked: inputElement.checked,
};
});
return {
useCases,
libraries,
frameworks,
contributor,
};
}
/**
* Test whether the tags of the given example panel match the requested filters.
*
* @param {any} tags Tags of the example panel
* @param {any} filters Filter statuses for all the filter groups; this should be the output of
* getFilterStatuses.
* @returns {bool} True if the example panel matches the filters, or not.
*/
function panelMatchesFilters(tags, filters) {
return Object.entries(filters).every(([group, groupTags]) => {
// If there is no selection, consider the panel to be matched
if (groupTags.filter(({isChecked}) => isChecked).length === 0) {
return true;
}
// If "Any" is checked, consider the panel to be matched
if (
groupTags.filter(({name, isChecked}) => name === 'any' && isChecked)
.length > 0
) {
return true;
}
// Otherwise show the panel if any checked item matches the tags of the panel
return groupTags
.filter(({isChecked}) => isChecked)
.some(({name}) => tags.includes(name));
});
}
/** Apply the currently selected filters to the example gallery, showing only the relevant examples. */
function applyFilter() {
const noMatchesElement = document.getElementById('no-matches');
const panels = document.querySelectorAll('.example');
const filters = getFilterStatuses();
const searchTerm = document
.getElementById('examples-search-input')
.value.toLowerCase();
// Show all panels before hiding the ones that need to be hidden.
panels.forEach((panel) => panel.classList.remove('hidden'));
// Check the title and tags of each example panel. If the tags match and the search term matches,
// show the panel.
panels.forEach((panel) => {
const title = panel
.querySelector('.example-title')
.textContent.toLowerCase();
const tags = panel
.querySelector('.example-tags')
.textContent.toLowerCase()
.concat();
const other_keywords = panel
.querySelector('.example-other-keywords')
.textContent.toLowerCase();
const keywords = `${tags} ${other_keywords}`;
const matchesSearch =
title.includes(searchTerm) || keywords.includes(searchTerm);
const matchesTags = panelMatchesFilters(keywords, filters);
// Hide panels that have no match
if (matchesSearch && matchesTags) {
panel.classList.remove('hidden');
} else {
panel.classList.add('hidden');
}
});
// If none are shown, show the "no matches" graphic.
if (document.querySelectorAll('.example:not(.hidden)').length === 0) {
noMatchesElement.classList.remove('hidden');
} else {
noMatchesElement.classList.add('hidden');
}
// Set the URL to match the active filters using query parameters.
const selectedTags = Object.entries(filters).map(([group, groupTags]) => {
return {
group,
selected: groupTags
.filter(({isChecked}) => isChecked)
.map(({name}) => name),
};
});
const queryParam = selectedTags
.filter(({group, selected}) => selected.length > 0)
.map(({group, selected}) => `${group}=${selected.join(',')}`)
.join('&');
history.replaceState(
null,
null,
queryParam.length === 0 ? location.pathname : `?${queryParam}`,
);
}
window.addEventListener('load', () => {
// Listen for filter checkbox clicks.
document.querySelectorAll('.filter-checkbox').forEach((tag) => {
tag.addEventListener('click', () => applyFilter());
});
// Add event listener for keypresses in the search bar.
document
.getElementById('examples-search-input')
.addEventListener('keyup', (event) => {
event.preventDefault();
applyFilter();
});
// Add the ability to provide URL query parameters to filter examples on page load.
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.size > 0) {
urlParams.forEach((params) => {
params.split(',').forEach((param) => {
const element = document.getElementById(`${param}-checkbox`);
if (element) {
element.checked = true;
}
});
});
}
// Apply the filter in case there are URL query parameters.
applyFilter();
const dropdowns = Array.from(
document.querySelectorAll('.dropdown-content'),
).map((dropdown) => {
return {
dropdown,
input: dropdown.parentNode.querySelector('input'),
inputContainer: dropdown.parentNode,
};
});
document.addEventListener('click', (event) => {
let targetEl = event.target; // clicked element
do {
const unclicked = dropdowns.filter(({dropdown, inputContainer}) => {
return !(targetEl == dropdown || targetEl == inputContainer);
});
if (unclicked.length !== dropdowns.length) {
// There has been a click inside one of the dropdowns. Close unclicked dropdowns and return.
unclicked.forEach(({input}) => {
input.checked = false;
});
return;
}
// Go up the DOM.
targetEl = targetEl.parentNode;
} while (targetEl);
// This is a click outside. Close all dropdowns.
dropdowns.forEach(({dropdown, input}) => {
input.checked = false;
});
});
});
+52
View File
@@ -0,0 +1,52 @@
function animateTabs() {
const tabs = Array.from(document.getElementById('v-pills-tab').children);
const contentTabs = Array.from(
document.getElementById('v-pills-tabContent').children,
);
tabs.forEach((item, index) => {
item.onclick = () => {
tabs.forEach((tab, i) => {
if (i === index) {
item.classList.add('active');
} else {
tab.classList.remove('active');
}
});
contentTabs.forEach((tab, i) => {
if (i === index) {
tab.classList.add('active', 'show');
} else {
tab.classList.remove('active', 'show');
}
});
};
});
}
function updateHighlight() {
const {theme} = document.documentElement.dataset;
['dark', 'light'].forEach((title) => {
const stylesheet = document.querySelector(`link[title="${title}"]`);
if (title === theme) {
stylesheet.removeAttribute('disabled');
} else {
stylesheet.setAttribute('disabled', 'disabled');
}
});
}
function setHighlightListener() {
const observer = new MutationObserver((mutations) => updateHighlight());
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme'],
});
}
document.addEventListener('DOMContentLoaded', animateTabs);
document.addEventListener('DOMContentLoaded', () => {
hljs.highlightAll();
updateHighlight();
});
document.addEventListener('DOMContentLoaded', setHighlightListener);
+229
View File
@@ -0,0 +1,229 @@
/**
* termynal.js
* A lightweight, modern and extensible animated terminal window, using
* async/await.
*
* @author Ines Montani <ines@ines.io>
* @version 0.0.1
* @license MIT
*/
'use strict';
/** Generate a terminal widget. */
class Termynal {
/**
* Construct the widget's settings.
* @param {(string|Node)=} container - Query selector or container element.
* @param {{noInit: boolean}} options - Custom settings.
* @param {string} options.prefix - Prefix to use for data attributes.
* @param {number} options.startDelay - Delay before animation, in ms.
* @param {number} options.typeDelay - Delay between each typed character, in ms.
* @param {number} options.lineDelay - Delay between each line, in ms.
* @param {number} options.progressLength - Number of characters displayed as progress bar.
* @param {string} options.progressChar Character to use for progress bar, defaults to █.
* @param {number} options.progressPercent - Max percent of progress.
* @param {string} options.cursor Character to use for cursor, defaults to ▋.
* @param {Object[]} lineData - Dynamically loaded line data objects.
* @param {boolean} options.noInit - Don't initialise the animation.
*/
constructor(container = '#termynal', options = {}) {
this.container = (typeof container === 'string') ? document.querySelector(container) : container;
this.pfx = `data-${options.prefix || 'ty'}`;
this.startDelay = options.startDelay
|| parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600;
this.typeDelay = options.typeDelay
|| parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90;
this.lineDelay = options.lineDelay
|| parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500;
this.progressLength = options.progressLength
|| parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40;
this.progressChar = options.progressChar
|| this.container.getAttribute(`${this.pfx}-progressChar`) || '█';
this.progressPercent = options.progressPercent
|| parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100;
this.cursor = options.cursor
|| this.container.getAttribute(`${this.pfx}-cursor`) || '▋';
this.lineData = this.lineDataToElements(options.lineData || []);
this.loadLines()
if (!options.noInit) this.init()
}
loadLines() {
// Load all the lines and create the container so that the size is fixed
// Otherwise it would be changing and the user viewport would be constantly
// moving as she/he scrolls
// Appends dynamically loaded lines to existing line elements.
this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData);
for (let line of this.lines) {
line.style.visibility = 'hidden'
this.container.appendChild(line)
}
const restart = this.generateRestart()
restart.style.visibility = 'hidden'
this.container.appendChild(restart)
this.container.setAttribute('data-termynal', '');
}
/**
* Initialise the widget, get lines, clear container and start animation.
*/
init() {
// Appends dynamically loaded lines to existing line elements.
//this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData);
/**
* Calculates width and height of Termynal container.
* If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS.
*/
const containerStyle = getComputedStyle(this.container);
this.container.style.width = containerStyle.width !== '0px' ?
containerStyle.width : undefined;
this.container.style.minHeight = containerStyle.height !== '0px' ?
containerStyle.height : undefined;
this.container.setAttribute('data-termynal', '');
this.container.innerHTML = '';
for (let line of this.lines) {
line.style.visibility = 'visible'
}
this.start();
}
/**
* Start the animation and rener the lines depending on their data attributes.
*/
async start() {
await this._wait(this.startDelay);
for (let line of this.lines) {
const type = line.getAttribute(this.pfx);
const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay;
if (type == 'input') {
line.setAttribute(`${this.pfx}-cursor`, this.cursor);
await this.type(line);
await this._wait(delay);
}
else if (type == 'progress') {
await this.progress(line);
await this._wait(delay);
}
else {
this.container.appendChild(line);
await this._wait(delay);
}
line.removeAttribute(`${this.pfx}-cursor`);
}
this.addRestart()
}
/**
* Animate a typed line.
* @param {Node} line - The line element to render.
*/
async type(line) {
const chars = [...line.textContent];
const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay;
line.textContent = '';
this.container.appendChild(line);
for (let char of chars) {
await this._wait(delay);
line.textContent += char;
}
}
/**
* Animate a progress bar.
* @param {Node} line - The line element to render.
*/
async progress(line) {
const progressLength = line.getAttribute(`${this.pfx}-progressLength`)
|| this.progressLength;
const progressChar = line.getAttribute(`${this.pfx}-progressChar`)
|| this.progressChar;
const chars = progressChar.repeat(progressLength);
const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`)
|| this.progressPercent;
line.textContent = '';
this.container.appendChild(line);
for (let i = 1; i < chars.length + 1; i++) {
await this._wait(this.typeDelay);
const percent = Math.round(i / chars.length * 100);
line.textContent = `${chars.slice(0, i)} ${percent}%`;
if (percent > progressPercent) {
break;
}
}
}
/**
* Helper function for animation delays, called with `await`.
* @param {number} time - Timeout, in ms.
*/
_wait(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
/**
* Converts line data objects into line elements.
*
* @param {Object[]} lineData - Dynamically loaded lines.
* @param {Object} line - Line data object.
* @returns {Element[]} - Array of line elements.
*/
lineDataToElements(lineData) {
return lineData.map(line => {
let div = document.createElement('div');
div.innerHTML = `<span ${this._attributes(line)}>${line.value || ''}</span>`;
return div.firstElementChild;
});
}
/**
* Helper function for generating attributes string.
*
* @param {Object} line - Line data object.
* @returns {string} - String of attributes.
*/
_attributes(line) {
let attrs = '';
for (let prop in line) {
attrs += this.pfx;
if (prop === 'type') {
attrs += `="${line[prop]}" `
} else if (prop !== 'value') {
attrs += `-${prop}="${line[prop]}" `
}
}
return attrs;
}
// Taken from Typer Tiangolo
generateRestart() {
const restart = document.createElement('a')
restart.onclick = (e) => {
e.preventDefault()
this.container.innerHTML = ''
this.init()
}
restart.href = '#'
restart.setAttribute('data-terminal-control', '')
restart.innerHTML = "restart ↻"
return restart
}
addRestart() {
const restart = this.generateRestart()
this.container.appendChild(restart)
}
}