reveal.js-copycode
Version:
A simple plugin for Reveal.js that automatically shows a copy button in code blocks
467 lines (464 loc) • 19.5 kB
JavaScript
/*****************************************************************
*
* reveal.js-copycode for Reveal.js
* Version 1.3.2
*
* @link
* https://github.com/martinomagnifico/reveal.js-copycode
*
* @author: Martijn De Jongh (Martino), martijn.de.jongh@gmail.com
* https://github.com/martinomagnifico
*
* @license
* MIT
*
* Copyright (C) 2025 Martijn De Jongh (Martino)
*
******************************************************************/
const S = {
button: "always",
display: "text",
text: {
copy: "Copy",
copied: "Copied!"
},
plaintextonly: !0,
timeout: 1e3,
style: {
copybg: "orange",
copiedbg: "green",
copycolor: "black",
copiedcolor: "white",
copyborder: "",
copiedborder: "",
scale: 1,
offset: 0,
radius: 0
},
tooltip: !0,
iconsvg: {
copy: "",
// User can paste <svg>…</svg> code here
copied: ""
// User can paste <svg>…</svg> code here
},
cssautoload: !0,
csspath: ""
};
function L(r) {
return r && r.__esModule && Object.prototype.hasOwnProperty.call(r, "default") ? r.default : r;
}
var j, A;
function P() {
if (A) return j;
A = 1;
var r = function(i) {
return e(i) && !t(i);
};
function e(n) {
return !!n && typeof n == "object";
}
function t(n) {
var i = Object.prototype.toString.call(n);
return i === "[object RegExp]" || i === "[object Date]" || l(n);
}
var o = typeof Symbol == "function" && Symbol.for, s = o ? Symbol.for("react.element") : 60103;
function l(n) {
return n.$$typeof === s;
}
function p(n) {
return Array.isArray(n) ? [] : {};
}
function a(n, i) {
return i.clone !== !1 && i.isMergeableObject(n) ? C(p(n), n, i) : n;
}
function y(n, i, c) {
return n.concat(i).map(function(m) {
return a(m, c);
});
}
function u(n, i) {
if (!i.customMerge)
return C;
var c = i.customMerge(n);
return typeof c == "function" ? c : C;
}
function d(n) {
return Object.getOwnPropertySymbols ? Object.getOwnPropertySymbols(n).filter(function(i) {
return Object.propertyIsEnumerable.call(n, i);
}) : [];
}
function f(n) {
return Object.keys(n).concat(d(n));
}
function b(n, i) {
try {
return i in n;
} catch {
return !1;
}
}
function v(n, i) {
return b(n, i) && !(Object.hasOwnProperty.call(n, i) && Object.propertyIsEnumerable.call(n, i));
}
function x(n, i, c) {
var m = {};
return c.isMergeableObject(n) && f(n).forEach(function(h) {
m[h] = a(n[h], c);
}), f(i).forEach(function(h) {
v(n, h) || (b(n, h) && c.isMergeableObject(i[h]) ? m[h] = u(h, c)(n[h], i[h], c) : m[h] = a(i[h], c));
}), m;
}
function C(n, i, c) {
c = c || {}, c.arrayMerge = c.arrayMerge || y, c.isMergeableObject = c.isMergeableObject || r, c.cloneUnlessOtherwiseSpecified = a;
var m = Array.isArray(i), h = Array.isArray(n), I = m === h;
return I ? m ? c.arrayMerge(n, i, c) : x(n, i, c) : a(i, c);
}
C.all = function(i, c) {
if (!Array.isArray(i))
throw new Error("first argument should be an array");
return i.reduce(function(m, h) {
return C(m, h, c);
}, {});
};
var w = C;
return j = w, j;
}
var q = P();
const H = /* @__PURE__ */ L(q);
var B = Object.defineProperty, N = (r, e, t) => e in r ? B(r, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : r[e] = t, g = (r, e, t) => N(r, typeof e != "symbol" ? e + "" : e, t);
const R = () => {
const r = typeof window < "u", e = typeof document < "u", t = r && typeof location < "u" && /localhost|127\.0\.0\.1/.test(location.hostname);
let o = !1;
try {
o = new Function('return typeof module !== "undefined" && !!module.hot')();
} catch {
}
let s = !1;
try {
s = new Function('return typeof import.meta !== "undefined" && typeof import.meta.env !== "undefined" && import.meta.env.DEV === true')();
} catch {
}
const l = r && typeof navigator < "u" && /vite|localhost|127\.0\.0\.1/.test(location.origin) && /AppleWebKit|Chrome|Vite/.test(navigator.userAgent), p = e && !!document.querySelector('script[type="module"]');
let a = !1;
try {
a = new Function('return typeof process !== "undefined" && process.env && (process.env.ROLLUP_WATCH === "true" || process.env.NODE_ENV === "development")')();
} catch {
}
let y = !1;
try {
y = new Function('return typeof define === "function" && !!define.amd')();
} catch {
}
return {
isDevServer: t,
isWebpackHMR: o,
isVite: s,
isVitePreview: l,
hasModuleScripts: p,
isModuleBundler: a,
isAMD: y,
isBundlerEnvironment: o || s || l || p || a || y || t
};
};
class _ {
// Create a new plugin instance
constructor(e, t, o) {
g(this, "defaultConfig"), g(this, "pluginInit"), g(this, "pluginId"), g(this, "mergedConfig", null), g(this, "userConfigData", null), g(this, "data", {}), g(this, "getEnvironmentInfo", () => R()), typeof e == "string" ? (this.pluginId = e, this.pluginInit = t, this.defaultConfig = o || {}) : (this.pluginId = e.id, this.pluginInit = e.init, this.defaultConfig = e.defaultConfig || {});
}
// Initialize plugin configuration by merging default and user settings
initializeConfig(e) {
const t = this.defaultConfig, o = e.getConfig()[this.pluginId] || {};
this.userConfigData = o, this.mergedConfig = H(t, o, {
arrayMerge: (s, l) => l,
clone: !0
});
}
// Get the current plugin configuration
getCurrentConfig() {
if (!this.mergedConfig)
throw new Error("Plugin configuration has not been initialized");
return this.mergedConfig;
}
// Get plugin data if any exists
getData() {
return Object.keys(this.data).length > 0 ? this.data : void 0;
}
get userConfig() {
return this.userConfigData || {};
}
// Initialize the plugin
init(e) {
if (this.initializeConfig(e), this.pluginInit)
return this.pluginInit(this, e, this.getCurrentConfig());
}
// Create the plugin interface containing all exports
createInterface(e = {}) {
return {
id: this.pluginId,
init: (t) => this.init(t),
getConfig: () => this.getCurrentConfig(),
getData: () => this.getData(),
...e
};
}
}
const F = (r) => {
const e = document.querySelector(
`script[src$="${r}.js"], script[src$="${r}.min.js"], script[src$="${r}.mjs"]`
);
if (e != null && e.src) {
const t = e.getAttribute("src") || "", o = t.lastIndexOf("/");
if (o !== -1)
return t.substring(0, o + 1);
}
try {
if (typeof import.meta < "u" && import.meta.url)
return import.meta.url.slice(0, import.meta.url.lastIndexOf("/") + 1);
} catch {
}
return `plugin/${r}/`;
}, T = "data-css-id", U = (r, e) => new Promise((t, o) => {
const s = document.createElement("link");
s.rel = "stylesheet", s.href = e, s.setAttribute(T, r);
const l = setTimeout(() => {
s.parentNode && s.parentNode.removeChild(s), o(new Error(`[${r}] Timeout loading CSS from: ${e}`));
}, 5e3);
s.onload = () => {
clearTimeout(l), t();
}, s.onerror = () => {
clearTimeout(l), s.parentNode && s.parentNode.removeChild(s), o(new Error(`[${r}] Failed to load CSS from: ${e}`));
}, document.head.appendChild(s);
}), O = (r) => document.querySelectorAll(`[${T}="${r}"]`).length > 0, z = (r) => new Promise((e) => {
if (t())
return e(!0);
setTimeout(() => {
e(t());
}, 50);
function t() {
if (O(r)) return !0;
try {
return window.getComputedStyle(document.documentElement).getPropertyValue(`--cssimported-${r}`).trim() !== "";
} catch {
return !1;
}
}
}), M = async (r) => {
const {
id: e,
cssautoload: t = !0,
csspath: o = "",
debug: s = !1
} = r;
if (t === !1 || o === !1) return;
if (O(e)) {
s && console.log(`[${e}] CSS already loaded, skipping`);
return;
}
const l = [];
typeof o == "string" && o.trim() !== "" && l.push(o);
const p = F(e);
if (p) {
const y = `${p}${e}.css`;
l.push(y);
}
const a = `plugin/${e}/${e}.css`;
l.push(a);
for (const y of l)
try {
await U(e, y);
let u = "CSS";
o && y === o ? u = "user-specified CSS" : p && y === `${p}${e}.css` ? u = "CSS (auto-detected from script location)" : u = "CSS (standard fallback)", s && console.log(`[${e}] ${u} loaded successfully from: ${y}`);
return;
} catch {
s && console.log(`[${e}] Failed to load CSS from: ${y}`);
}
console.warn(`[${e}] Could not load CSS from any location`);
};
async function V(r, e) {
if ("getEnvironmentInfo" in r && e) {
const t = r, o = t.getEnvironmentInfo();
if (await z(t.pluginId)) {
e.debug && console.log(`[${t.pluginId}] CSS already imported, skipping`);
return;
}
if ("cssautoload" in t.userConfig ? e.cssautoload : !o.isBundlerEnvironment)
return M({
id: t.pluginId,
cssautoload: !0,
csspath: e.csspath,
debug: e.debug
});
o.isBundlerEnvironment && console.warn(`[${t.pluginId}] CSS autoloading is disabled in bundler environments. Please import the CSS manually, using import.`);
return;
}
return M(r);
}
class k {
constructor() {
g(this, "debugMode", !1), g(this, "label", "DEBUG"), g(this, "groupDepth", 0), g(this, "group", (...e) => {
this.debugLog("group", ...e), this.groupDepth++;
}), g(this, "groupCollapsed", (...e) => {
this.debugLog("groupCollapsed", ...e), this.groupDepth++;
}), g(this, "groupEnd", () => {
this.groupDepth > 0 && (this.groupDepth--, this.debugLog("groupEnd"));
}), g(this, "error", (...e) => {
const t = this.debugMode;
this.debugMode = !0, this.formatAndLog(console.error, e), this.debugMode = t;
}), g(this, "table", (e, t, o) => {
if (this.debugMode)
try {
typeof e == "string" && t !== void 0 && typeof t != "string" ? (this.groupDepth === 0 ? console.log(`[${this.label}]: ${e}`) : console.log(e), o ? console.table(t, o) : console.table(t)) : (this.groupDepth === 0 && console.log(`[${this.label}]: Table data`), typeof t == "object" && Array.isArray(t) ? console.table(e, t) : console.table(e));
} catch (s) {
console.error(`[${this.label}]: Error showing table:`, s), console.log(`[${this.label}]: Raw data:`, e);
}
}), g(this, "formatAndLog", (e, t) => {
if (this.debugMode)
try {
this.groupDepth > 0 ? e.call(console, ...t) : t.length > 0 && typeof t[0] == "string" ? e.call(console, `[${this.label}]: ${t[0]}`, ...t.slice(1)) : e.call(console, `[${this.label}]:`, ...t);
} catch (o) {
console.error(`[${this.label}]: Error in logging:`, o), console.log(`[${this.label}]: Original log data:`, ...t);
}
});
}
// Initializes the debug utility with custom settings.
initialize(e, t = "DEBUG") {
this.debugMode = e, this.label = t;
}
// Core method that handles calling console methods with proper formatting.
// - Adds label prefix to messages outside of groups
// - Skips label prefix for messages inside groups to avoid redundancy
// - Always adds label prefix to group headers
// - Error messages are always shown regardless of debug mode
// @param methodName - Name of the console method to call
// @param args - Arguments to pass to the console method
debugLog(e, ...t) {
const o = console[e];
if (!this.debugMode && e !== "error" || typeof o != "function") return;
const s = o;
if (e === "group" || e === "groupCollapsed") {
t.length > 0 && typeof t[0] == "string" ? s.call(console, `[${this.label}]: ${t[0]}`, ...t.slice(1)) : s.call(console, `[${this.label}]:`, ...t);
return;
}
if (e === "groupEnd") {
s.call(console);
return;
}
if (e === "table") {
t.length === 1 ? this.table(t[0]) : t.length === 2 ? typeof t[0] == "string" ? this.table(t[0], t[1]) : this.table(t[0], t[1]) : t.length >= 3 && this.table(
t[0],
t[1],
t[2]
);
return;
}
this.groupDepth > 0 ? s.call(console, ...t) : t.length > 0 && typeof t[0] == "string" ? s.call(console, `[${this.label}]: ${t[0]}`, ...t.slice(1)) : s.call(console, `[${this.label}]:`, ...t);
}
}
const G = (r) => new Proxy(r, {
get: (e, t) => {
if (t in e)
return e[t];
const o = t.toString();
if (typeof console[o] == "function")
return (...s) => {
e.debugLog(o, ...s);
};
}
}), $ = G(new k()), W = (r, e) => {
const t = (o, s) => o === void 0 || o === "" && String(s) === "0" ? !1 : String(o) !== String(s);
t(e.style.copybg, S.style.copybg) && r.style.setProperty("--cc-copy-bg", e.style.copybg), t(e.style.copiedbg, S.style.copiedbg) && r.style.setProperty("--cc-copied-bg", e.style.copiedbg), t(e.style.copycolor, S.style.copycolor) && r.style.setProperty("--cc-copy-color", e.style.copycolor), t(e.style.copiedcolor, S.style.copiedcolor) && r.style.setProperty("--cc-copied-color", e.style.copiedcolor), t(e.style.scale, S.style.scale) && r.style.setProperty("--cc-scale", String(e.style.scale)), t(e.style.offset, S.style.offset) && r.style.setProperty("--cc-offset", String(e.style.offset)), t(e.style.radius, S.style.radius) && r.style.setProperty("--cc-radius", String(e.style.radius)), t(e.style.copyborder, S.style.copyborder) && e.style.copyborder !== "" && r.style.setProperty("--cc-copyborder", e.style.copyborder), t(e.style.copiedborder, S.style.copiedborder) && e.style.copiedborder !== "" && r.style.setProperty("--cc-copiedborder", e.style.copiedborder);
}, E = {
copy: '<svg aria-hidden="true" height="16" width="16" viewBox="0 0 16 16" version="1.1"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg>',
copied: '<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16"><path d="M15.7,2.8c0.4,0.4,0.4,1,0,1.4L6,13.9c-0.4,0.4-1,0.4-1.4,0L0.3,9.6c-0.4-0.4-0.4-1,0-1.4c0.4-0.4,1-0.4,1.4,0l3.6,3.6l9-9C14.7,2.4,15.3,2.4,15.7,2.8z"/></svg>'
}, Z = (r, e) => {
var l, p, a;
const t = (l = e.iconsvg) != null && l.copy && e.iconsvg.copy !== "" ? e.iconsvg.copy : E.copy, o = (p = e.iconsvg) != null && p.copied && e.iconsvg.copied !== "" ? e.iconsvg.copied : E.copied, s = r.dataset.ccDisplay || e.display;
s === "icons" || s === "both" ? (r.innerHTML = "<span></span>", r.textholder = r.querySelector("span"), r.insertAdjacentHTML("afterbegin", o), r.insertAdjacentHTML("afterbegin", t), r.dataset.ccDisplay === "icons" && e.tooltip && (r.textholder.style.display = "flex")) : (r.innerHTML = "<span></span>", r.textholder = r.querySelector("span")), r.textholder && (r.textholder.textContent = r.dataset.ccCopy ? r.dataset.ccCopy : ((a = e.text) == null ? void 0 : a.copy) || "Copy");
}, K = async (r, e) => {
if (e.plaintextonly) {
let t;
const o = r.querySelector("table.hljs-ln");
o ? t = Array.from(o.querySelectorAll("td.hljs-ln-code")).map((s) => s.textContent).join(`
`) : t = r.innerText.replace(/^\s+|\s+$/g, ""), await navigator.clipboard.writeText(t);
} else
try {
const t = r.cloneNode(!0);
let o;
const s = t.querySelector("table.hljs-ln");
if (s) {
const d = s.querySelectorAll("td.hljs-ln-numbers");
for (const f of d)
f.style.display = "none";
o = Array.from(s.querySelectorAll("td.hljs-ln-code")).map((f) => f.textContent).join(`
`);
} else
o = t.innerText;
const p = `
<style>${Array.from(document.styleSheets).flatMap((d) => {
try {
return Array.from(d.cssRules);
} catch {
return [];
}
}).filter((d) => d.cssText.includes(".hljs") || d.cssText.includes("pre code") || d.cssText.includes("code[class") || d.cssText.includes(".language-") || d.cssText.includes("pre") || d.cssText.includes("code")).map((d) => d.cssText).join(`
`)}pre, code, .hljs, .hljs-ln-code { white-space: pre !important; font-family: monospace !important;}</style>
<div>${t.outerHTML}</div>
`, a = new Blob([p], { type: "text/html" }), y = new Blob([o], { type: "text/plain" }), u = new ClipboardItem({
"text/html": a,
"text/plain": y
});
await navigator.clipboard.write([u]);
} catch (t) {
console.error("Rich text clipboard error:", t), await navigator.clipboard.writeText(r.innerText);
}
}, Y = (r, e) => {
const t = r.querySelectorAll(".codeblock > button:not(.code-copy-button)");
for (const o of t)
o.addEventListener("click", async () => {
const s = o, l = s.nextElementSibling;
if (!l || !(l instanceof HTMLElement))
return;
const p = l.querySelector("code");
if (!p || !(p instanceof HTMLElement)) {
$.error("Could not find code element");
return;
}
try {
await K(p, e), J(s, e);
} catch (a) {
$.error("Error copying code:", a);
}
});
}, J = (r, e) => {
r.textholder && (r.dataset.textOriginal = r.textholder.innerHTML, r.textholder.innerHTML = r.dataset.ccCopied || e.text.copied), r.setAttribute("disabled", "true"), setTimeout(() => {
r.textholder && (r.dataset.ccDisplay !== "icons" || !r.dataset.ccDisplay) && (r.textholder.innerHTML = r.dataset.textOriginal || ""), delete r.dataset.textOriginal, r.removeAttribute("disabled");
}, e.timeout);
}, Q = async (r, e, t) => {
var y;
const o = e.getRevealElement(), s = 'pre:not([data-cc="false"]) > code', l = document.querySelector("[name=generator]"), p = ((y = l == null ? void 0 : l.getAttribute("content")) == null ? void 0 : y.includes("quarto")) ?? !1;
let a = [];
if (o && (W(o, t), a = Array.from(o.querySelectorAll(s)).map((u) => u.parentElement).filter((u) => u instanceof HTMLPreElement)), a.length > 0 && o) {
$.log(`${a.length} code blocks found`, a);
for (const u of a) {
let d = null, f = null;
const b = u.parentElement;
if (p && (b != null && b.matches(".sourceCode")) ? (d = b, f = d, b.dataset.did = "quartoblock") : (f = u, b != null && b.classList.contains("codeblock") || (d = document.createElement("div"), b == null || b.insertBefore(d, u))), d && f) {
d.classList.add("codeblock"), d.appendChild(u), (t.display === "icon" || t.display === "icons" || t.display === "both") && f && (f.dataset.ccDisplay = t.display), u.classList.contains("fragment") && (d.classList.add("fragment"), u.classList.remove("fragment"));
const v = document.createElement("button");
v.dataset.cc = "true", v.title = "Copy to Clipboard", t.button !== "always" && (v.dataset.cc = t.button);
const x = ["cc", "ccCopy", "ccCopied", "ccDisplay"];
for (const w of x)
f.dataset[w] && (v.dataset[w] = f.dataset[w], delete f.dataset[w]);
const C = u.querySelectorAll("code")[0];
C != null && C.innerText && (Z(v, t), d.insertBefore(v, u));
}
}
Y(o, t);
}
}, D = "copycode", X = async (r, e, t) => {
$ && t.debug && $.initialize(!0, D), await V(r, t), await Q(r, e, t);
}, ee = () => new _(D, X, S).createInterface();
export {
ee as default
};