2022-09-30 10:43:04 -06:00
|
|
|
|
// Localization support
|
|
|
|
|
const messages = {
|
|
|
|
|
'en': {
|
|
|
|
|
'copy': 'Copy',
|
|
|
|
|
'copy_to_clipboard': 'Copy to clipboard',
|
|
|
|
|
'copy_success': 'Copied!',
|
|
|
|
|
'copy_failure': 'Failed to copy',
|
|
|
|
|
},
|
|
|
|
|
'es' : {
|
|
|
|
|
'copy': 'Copiar',
|
|
|
|
|
'copy_to_clipboard': 'Copiar al portapapeles',
|
|
|
|
|
'copy_success': '¡Copiado!',
|
|
|
|
|
'copy_failure': 'Error al copiar',
|
|
|
|
|
},
|
|
|
|
|
'de' : {
|
|
|
|
|
'copy': 'Kopieren',
|
|
|
|
|
'copy_to_clipboard': 'In die Zwischenablage kopieren',
|
|
|
|
|
'copy_success': 'Kopiert!',
|
|
|
|
|
'copy_failure': 'Fehler beim Kopieren',
|
|
|
|
|
},
|
|
|
|
|
'fr' : {
|
|
|
|
|
'copy': 'Copier',
|
2023-06-29 08:52:06 -06:00
|
|
|
|
'copy_to_clipboard': 'Copier dans le presse-papier',
|
2022-09-30 10:43:04 -06:00
|
|
|
|
'copy_success': 'Copié !',
|
|
|
|
|
'copy_failure': 'Échec de la copie',
|
|
|
|
|
},
|
|
|
|
|
'ru': {
|
|
|
|
|
'copy': 'Скопировать',
|
|
|
|
|
'copy_to_clipboard': 'Скопировать в буфер',
|
|
|
|
|
'copy_success': 'Скопировано!',
|
|
|
|
|
'copy_failure': 'Не удалось скопировать',
|
|
|
|
|
},
|
|
|
|
|
'zh-CN': {
|
|
|
|
|
'copy': '复制',
|
|
|
|
|
'copy_to_clipboard': '复制到剪贴板',
|
|
|
|
|
'copy_success': '复制成功!',
|
|
|
|
|
'copy_failure': '复制失败',
|
|
|
|
|
},
|
|
|
|
|
'it' : {
|
|
|
|
|
'copy': 'Copiare',
|
|
|
|
|
'copy_to_clipboard': 'Copiato negli appunti',
|
|
|
|
|
'copy_success': 'Copiato!',
|
|
|
|
|
'copy_failure': 'Errore durante la copia',
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let locale = 'en'
|
|
|
|
|
if( document.documentElement.lang !== undefined
|
|
|
|
|
&& messages[document.documentElement.lang] !== undefined ) {
|
|
|
|
|
locale = document.documentElement.lang
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT;
|
|
|
|
|
if (doc_url_root == '#') {
|
|
|
|
|
doc_url_root = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* SVG files for our copy buttons
|
|
|
|
|
*/
|
|
|
|
|
let iconCheck = `<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#22863a" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
|
<title>${messages[locale]['copy_success']}</title>
|
|
|
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
|
|
|
<path d="M5 12l5 5l10 -10" />
|
|
|
|
|
</svg>`
|
|
|
|
|
|
|
|
|
|
// If the user specified their own SVG use that, otherwise use the default
|
|
|
|
|
let iconCopy = ``;
|
|
|
|
|
if (!iconCopy) {
|
|
|
|
|
iconCopy = `<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-copy" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
|
<title>${messages[locale]['copy_to_clipboard']}</title>
|
|
|
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
|
|
|
<rect x="8" y="8" width="12" height="12" rx="2" />
|
|
|
|
|
<path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" />
|
|
|
|
|
</svg>`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set up copy/paste for code blocks
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const runWhenDOMLoaded = cb => {
|
|
|
|
|
if (document.readyState != 'loading') {
|
|
|
|
|
cb()
|
|
|
|
|
} else if (document.addEventListener) {
|
|
|
|
|
document.addEventListener('DOMContentLoaded', cb)
|
|
|
|
|
} else {
|
|
|
|
|
document.attachEvent('onreadystatechange', function() {
|
|
|
|
|
if (document.readyState == 'complete') cb()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const codeCellId = index => `codecell${index}`
|
|
|
|
|
|
|
|
|
|
// Clears selected text since ClipboardJS will select the text when copying
|
|
|
|
|
const clearSelection = () => {
|
|
|
|
|
if (window.getSelection) {
|
|
|
|
|
window.getSelection().removeAllRanges()
|
|
|
|
|
} else if (document.selection) {
|
|
|
|
|
document.selection.empty()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-29 08:52:06 -06:00
|
|
|
|
// Changes tooltip text for a moment, then changes it back
|
|
|
|
|
// We want the timeout of our `success` class to be a bit shorter than the
|
|
|
|
|
// tooltip and icon change, so that we can hide the icon before changing back.
|
|
|
|
|
var timeoutIcon = 2000;
|
|
|
|
|
var timeoutSuccessClass = 1500;
|
|
|
|
|
|
2022-09-30 10:43:04 -06:00
|
|
|
|
const temporarilyChangeTooltip = (el, oldText, newText) => {
|
|
|
|
|
el.setAttribute('data-tooltip', newText)
|
|
|
|
|
el.classList.add('success')
|
2023-06-29 08:52:06 -06:00
|
|
|
|
// Remove success a little bit sooner than we change the tooltip
|
|
|
|
|
// So that we can use CSS to hide the copybutton first
|
|
|
|
|
setTimeout(() => el.classList.remove('success'), timeoutSuccessClass)
|
|
|
|
|
setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon)
|
2022-09-30 10:43:04 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Changes the copy button icon for two seconds, then changes it back
|
|
|
|
|
const temporarilyChangeIcon = (el) => {
|
|
|
|
|
el.innerHTML = iconCheck;
|
2023-06-29 08:52:06 -06:00
|
|
|
|
setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon)
|
2022-09-30 10:43:04 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const addCopyButtonToCodeCells = () => {
|
|
|
|
|
// If ClipboardJS hasn't loaded, wait a bit and try again. This
|
|
|
|
|
// happens because we load ClipboardJS asynchronously.
|
|
|
|
|
if (window.ClipboardJS === undefined) {
|
|
|
|
|
setTimeout(addCopyButtonToCodeCells, 250)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add copybuttons to all of our code cells
|
2023-06-29 08:52:06 -06:00
|
|
|
|
const COPYBUTTON_SELECTOR = 'div.highlight pre';
|
|
|
|
|
const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR)
|
2022-09-30 10:43:04 -06:00
|
|
|
|
codeCells.forEach((codeCell, index) => {
|
|
|
|
|
const id = codeCellId(index)
|
|
|
|
|
codeCell.setAttribute('id', id)
|
|
|
|
|
|
|
|
|
|
const clipboardButton = id =>
|
|
|
|
|
`<button class="copybtn o-tooltip--left" data-tooltip="${messages[locale]['copy']}" data-clipboard-target="#${id}">
|
|
|
|
|
${iconCopy}
|
|
|
|
|
</button>`
|
|
|
|
|
codeCell.insertAdjacentHTML('afterend', clipboardButton(id))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function escapeRegExp(string) {
|
|
|
|
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-29 08:52:06 -06:00
|
|
|
|
/**
|
|
|
|
|
* Removes excluded text from a Node.
|
|
|
|
|
*
|
|
|
|
|
* @param {Node} target Node to filter.
|
|
|
|
|
* @param {string} exclude CSS selector of nodes to exclude.
|
|
|
|
|
* @returns {DOMString} Text from `target` with text removed.
|
|
|
|
|
*/
|
|
|
|
|
function filterText(target, exclude) {
|
|
|
|
|
const clone = target.cloneNode(true); // clone as to not modify the live DOM
|
|
|
|
|
if (exclude) {
|
|
|
|
|
// remove excluded nodes
|
|
|
|
|
clone.querySelectorAll(exclude).forEach(node => node.remove());
|
|
|
|
|
}
|
|
|
|
|
return clone.innerText;
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-30 10:43:04 -06:00
|
|
|
|
// Callback when a copy button is clicked. Will be passed the node that was clicked
|
|
|
|
|
// should then grab the text and replace pieces of text that shouldn't be used in output
|
|
|
|
|
function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") {
|
|
|
|
|
var regexp;
|
|
|
|
|
var match;
|
|
|
|
|
|
|
|
|
|
// Do we check for line continuation characters and "HERE-documents"?
|
|
|
|
|
var useLineCont = !!lineContinuationChar
|
|
|
|
|
var useHereDoc = !!hereDocDelim
|
|
|
|
|
|
|
|
|
|
// create regexp to capture prompt and remaining line
|
|
|
|
|
if (isRegexp) {
|
|
|
|
|
regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)')
|
|
|
|
|
} else {
|
|
|
|
|
regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const outputLines = [];
|
|
|
|
|
var promptFound = false;
|
|
|
|
|
var gotLineCont = false;
|
|
|
|
|
var gotHereDoc = false;
|
|
|
|
|
const lineGotPrompt = [];
|
|
|
|
|
for (const line of textContent.split('\n')) {
|
|
|
|
|
match = line.match(regexp)
|
|
|
|
|
if (match || gotLineCont || gotHereDoc) {
|
|
|
|
|
promptFound = regexp.test(line)
|
|
|
|
|
lineGotPrompt.push(promptFound)
|
|
|
|
|
if (removePrompts && promptFound) {
|
|
|
|
|
outputLines.push(match[2])
|
|
|
|
|
} else {
|
|
|
|
|
outputLines.push(line)
|
|
|
|
|
}
|
|
|
|
|
gotLineCont = line.endsWith(lineContinuationChar) & useLineCont
|
|
|
|
|
if (line.includes(hereDocDelim) & useHereDoc)
|
|
|
|
|
gotHereDoc = !gotHereDoc
|
|
|
|
|
} else if (!onlyCopyPromptLines) {
|
|
|
|
|
outputLines.push(line)
|
|
|
|
|
} else if (copyEmptyLines && line.trim() === '') {
|
|
|
|
|
outputLines.push(line)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If no lines with the prompt were found then just use original lines
|
|
|
|
|
if (lineGotPrompt.some(v => v === true)) {
|
|
|
|
|
textContent = outputLines.join('\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove a trailing newline to avoid auto-running when pasting
|
|
|
|
|
if (textContent.endsWith("\n")) {
|
|
|
|
|
textContent = textContent.slice(0, -1)
|
|
|
|
|
}
|
|
|
|
|
return textContent
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var copyTargetText = (trigger) => {
|
|
|
|
|
var target = document.querySelector(trigger.attributes['data-clipboard-target'].value);
|
2023-06-29 08:52:06 -06:00
|
|
|
|
|
|
|
|
|
// get filtered text
|
|
|
|
|
let exclude = '.linenos';
|
|
|
|
|
|
|
|
|
|
let text = filterText(target, exclude);
|
|
|
|
|
return formatCopyText(text, '', false, true, true, true, '', '')
|
2022-09-30 10:43:04 -06:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize with a callback so we can modify the text before copy
|
|
|
|
|
const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText})
|
|
|
|
|
|
|
|
|
|
// Update UI with error/success messages
|
|
|
|
|
clipboard.on('success', event => {
|
|
|
|
|
clearSelection()
|
|
|
|
|
temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success'])
|
|
|
|
|
temporarilyChangeIcon(event.trigger)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
clipboard.on('error', event => {
|
|
|
|
|
temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure'])
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
runWhenDOMLoaded(addCopyButtonToCodeCells)
|