copy-llms-txt
Version:
A lightweight JavaScript library that lets you copy the content of a webpage as clean markdown with a simple keyboard shortcut.
803 lines (778 loc) • 19.9 kB
JavaScript
var B = Object.defineProperty;
var P = (e, t, n) => t in e ? B(e, t, { enumerable: !0, configurable: !0, writable: !0, value: n }) : e[t] = n;
var f = (e, t, n) => P(e, typeof t != "symbol" ? t + "" : t, n);
function x(e) {
for (var t = 1; t < arguments.length; t++) {
var n = arguments[t];
for (var r in n)
n.hasOwnProperty(r) && (e[r] = n[r]);
}
return e;
}
function y(e, t) {
return Array(t + 1).join(e);
}
function M(e) {
return e.replace(/^\n*/, "");
}
function I(e) {
for (var t = e.length; t > 0 && e[t - 1] === `
`; ) t--;
return e.substring(0, t);
}
var H = [
"ADDRESS",
"ARTICLE",
"ASIDE",
"AUDIO",
"BLOCKQUOTE",
"BODY",
"CANVAS",
"CENTER",
"DD",
"DIR",
"DIV",
"DL",
"DT",
"FIELDSET",
"FIGCAPTION",
"FIGURE",
"FOOTER",
"FORM",
"FRAMESET",
"H1",
"H2",
"H3",
"H4",
"H5",
"H6",
"HEADER",
"HGROUP",
"HR",
"HTML",
"ISINDEX",
"LI",
"MAIN",
"MENU",
"NAV",
"NOFRAMES",
"NOSCRIPT",
"OL",
"OUTPUT",
"P",
"PRE",
"SECTION",
"TABLE",
"TBODY",
"TD",
"TFOOT",
"TH",
"THEAD",
"TR",
"UL"
];
function E(e) {
return C(e, H);
}
var A = [
"AREA",
"BASE",
"BR",
"COL",
"COMMAND",
"EMBED",
"HR",
"IMG",
"INPUT",
"KEYGEN",
"LINK",
"META",
"PARAM",
"SOURCE",
"TRACK",
"WBR"
];
function k(e) {
return C(e, A);
}
function F(e) {
return R(e, A);
}
var b = [
"A",
"TABLE",
"THEAD",
"TBODY",
"TFOOT",
"TH",
"TD",
"IFRAME",
"SCRIPT",
"AUDIO",
"VIDEO"
];
function $(e) {
return C(e, b);
}
function V(e) {
return R(e, b);
}
function C(e, t) {
return t.indexOf(e.nodeName) >= 0;
}
function R(e, t) {
return e.getElementsByTagName && t.some(function(n) {
return e.getElementsByTagName(n).length;
});
}
var s = {};
s.paragraph = {
filter: "p",
replacement: function(e) {
return `
` + e + `
`;
}
};
s.lineBreak = {
filter: "br",
replacement: function(e, t, n) {
return n.br + `
`;
}
};
s.heading = {
filter: ["h1", "h2", "h3", "h4", "h5", "h6"],
replacement: function(e, t, n) {
var r = Number(t.nodeName.charAt(1));
if (n.headingStyle === "setext" && r < 3) {
var i = y(r === 1 ? "=" : "-", e.length);
return `
` + e + `
` + i + `
`;
} else
return `
` + y("#", r) + " " + e + `
`;
}
};
s.blockquote = {
filter: "blockquote",
replacement: function(e) {
return e = e.replace(/^\n+|\n+$/g, ""), e = e.replace(/^/gm, "> "), `
` + e + `
`;
}
};
s.list = {
filter: ["ul", "ol"],
replacement: function(e, t) {
var n = t.parentNode;
return n.nodeName === "LI" && n.lastElementChild === t ? `
` + e : `
` + e + `
`;
}
};
s.listItem = {
filter: "li",
replacement: function(e, t, n) {
e = e.replace(/^\n+/, "").replace(/\n+$/, `
`).replace(/\n/gm, `
`);
var r = n.bulletListMarker + " ", i = t.parentNode;
if (i.nodeName === "OL") {
var a = i.getAttribute("start"), o = Array.prototype.indexOf.call(i.children, t);
r = (a ? Number(a) + o : o + 1) + ". ";
}
return r + e + (t.nextSibling && !/\n$/.test(e) ? `
` : "");
}
};
s.indentedCodeBlock = {
filter: function(e, t) {
return t.codeBlockStyle === "indented" && e.nodeName === "PRE" && e.firstChild && e.firstChild.nodeName === "CODE";
},
replacement: function(e, t, n) {
return `
` + t.firstChild.textContent.replace(/\n/g, `
`) + `
`;
}
};
s.fencedCodeBlock = {
filter: function(e, t) {
return t.codeBlockStyle === "fenced" && e.nodeName === "PRE" && e.firstChild && e.firstChild.nodeName === "CODE";
},
replacement: function(e, t, n) {
for (var r = t.firstChild.getAttribute("class") || "", i = (r.match(/language-(\S+)/) || [null, ""])[1], a = t.firstChild.textContent, o = n.fence.charAt(0), c = 3, l = new RegExp("^" + o + "{3,}", "gm"), u; u = l.exec(a); )
u[0].length >= c && (c = u[0].length + 1);
var h = y(o, c);
return `
` + h + i + `
` + a.replace(/\n$/, "") + `
` + h + `
`;
}
};
s.horizontalRule = {
filter: "hr",
replacement: function(e, t, n) {
return `
` + n.hr + `
`;
}
};
s.inlineLink = {
filter: function(e, t) {
return t.linkStyle === "inlined" && e.nodeName === "A" && e.getAttribute("href");
},
replacement: function(e, t) {
var n = t.getAttribute("href");
n && (n = n.replace(/([()])/g, "\\$1"));
var r = m(t.getAttribute("title"));
return r && (r = ' "' + r.replace(/"/g, '\\"') + '"'), "[" + e + "](" + n + r + ")";
}
};
s.referenceLink = {
filter: function(e, t) {
return t.linkStyle === "referenced" && e.nodeName === "A" && e.getAttribute("href");
},
replacement: function(e, t, n) {
var r = t.getAttribute("href"), i = m(t.getAttribute("title"));
i && (i = ' "' + i + '"');
var a, o;
switch (n.linkReferenceStyle) {
case "collapsed":
a = "[" + e + "][]", o = "[" + e + "]: " + r + i;
break;
case "shortcut":
a = "[" + e + "]", o = "[" + e + "]: " + r + i;
break;
default:
var c = this.references.length + 1;
a = "[" + e + "][" + c + "]", o = "[" + c + "]: " + r + i;
}
return this.references.push(o), a;
},
references: [],
append: function(e) {
var t = "";
return this.references.length && (t = `
` + this.references.join(`
`) + `
`, this.references = []), t;
}
};
s.emphasis = {
filter: ["em", "i"],
replacement: function(e, t, n) {
return e.trim() ? n.emDelimiter + e + n.emDelimiter : "";
}
};
s.strong = {
filter: ["strong", "b"],
replacement: function(e, t, n) {
return e.trim() ? n.strongDelimiter + e + n.strongDelimiter : "";
}
};
s.code = {
filter: function(e) {
var t = e.previousSibling || e.nextSibling, n = e.parentNode.nodeName === "PRE" && !t;
return e.nodeName === "CODE" && !n;
},
replacement: function(e) {
if (!e) return "";
e = e.replace(/\r?\n|\r/g, " ");
for (var t = /^`|^ .*?[^ ].* $|`$/.test(e) ? " " : "", n = "`", r = e.match(/`+/gm) || []; r.indexOf(n) !== -1; ) n = n + "`";
return n + t + e + t + n;
}
};
s.image = {
filter: "img",
replacement: function(e, t) {
var n = m(t.getAttribute("alt")), r = t.getAttribute("src") || "", i = m(t.getAttribute("title")), a = i ? ' "' + i + '"' : "";
return r ? "" : "";
}
};
function m(e) {
return e ? e.replace(/(\n+\s*)+/g, `
`) : "";
}
function S(e) {
this.options = e, this._keep = [], this._remove = [], this.blankRule = {
replacement: e.blankReplacement
}, this.keepReplacement = e.keepReplacement, this.defaultRule = {
replacement: e.defaultReplacement
}, this.array = [];
for (var t in e.rules) this.array.push(e.rules[t]);
}
S.prototype = {
add: function(e, t) {
this.array.unshift(t);
},
keep: function(e) {
this._keep.unshift({
filter: e,
replacement: this.keepReplacement
});
},
remove: function(e) {
this._remove.unshift({
filter: e,
replacement: function() {
return "";
}
});
},
forNode: function(e) {
if (e.isBlank) return this.blankRule;
var t;
return (t = p(this.array, e, this.options)) || (t = p(this._keep, e, this.options)) || (t = p(this._remove, e, this.options)) ? t : this.defaultRule;
},
forEach: function(e) {
for (var t = 0; t < this.array.length; t++) e(this.array[t], t);
}
};
function p(e, t, n) {
for (var r = 0; r < e.length; r++) {
var i = e[r];
if (U(i, t, n)) return i;
}
}
function U(e, t, n) {
var r = e.filter;
if (typeof r == "string") {
if (r === t.nodeName.toLowerCase()) return !0;
} else if (Array.isArray(r)) {
if (r.indexOf(t.nodeName.toLowerCase()) > -1) return !0;
} else if (typeof r == "function") {
if (r.call(e, t, n)) return !0;
} else
throw new TypeError("`filter` needs to be a string, array, or function");
}
function W(e) {
var t = e.element, n = e.isBlock, r = e.isVoid, i = e.isPre || function(L) {
return L.nodeName === "PRE";
};
if (!(!t.firstChild || i(t))) {
for (var a = null, o = !1, c = null, l = N(c, t, i); l !== t; ) {
if (l.nodeType === 3 || l.nodeType === 4) {
var u = l.data.replace(/[ \r\n\t]+/g, " ");
if ((!a || / $/.test(a.data)) && !o && u[0] === " " && (u = u.substr(1)), !u) {
l = g(l);
continue;
}
l.data = u, a = l;
} else if (l.nodeType === 1)
n(l) || l.nodeName === "BR" ? (a && (a.data = a.data.replace(/ $/, "")), a = null, o = !1) : r(l) || i(l) ? (a = null, o = !0) : a && (o = !1);
else {
l = g(l);
continue;
}
var h = N(c, l, i);
c = l, l = h;
}
a && (a.data = a.data.replace(/ $/, ""), a.data || g(a));
}
}
function g(e) {
var t = e.nextSibling || e.parentNode;
return e.parentNode.removeChild(e), t;
}
function N(e, t, n) {
return e && e.parentNode === t || n(t) ? t.nextSibling || t.parentNode : t.firstChild || t.nextSibling || t.parentNode;
}
var w = typeof window < "u" ? window : {};
function z() {
var e = w.DOMParser, t = !1;
try {
new e().parseFromString("", "text/html") && (t = !0);
} catch {
}
return t;
}
function _() {
var e = function() {
};
return K() ? e.prototype.parseFromString = function(t) {
var n = new window.ActiveXObject("htmlfile");
return n.designMode = "on", n.open(), n.write(t), n.close(), n;
} : e.prototype.parseFromString = function(t) {
var n = document.implementation.createHTMLDocument("");
return n.open(), n.write(t), n.close(), n;
}, e;
}
function K() {
var e = !1;
try {
document.implementation.createHTMLDocument("").open();
} catch {
w.ActiveXObject && (e = !0);
}
return e;
}
var j = z() ? w.DOMParser : _();
function q(e, t) {
var n;
if (typeof e == "string") {
var r = G().parseFromString(
// DOM parsers arrange elements in the <head> and <body>.
// Wrapping in a custom element ensures elements are reliably arranged in
// a single element.
'<x-turndown id="turndown-root">' + e + "</x-turndown>",
"text/html"
);
n = r.getElementById("turndown-root");
} else
n = e.cloneNode(!0);
return W({
element: n,
isBlock: E,
isVoid: k,
isPre: t.preformattedCode ? X : null
}), n;
}
var v;
function G() {
return v = v || new j(), v;
}
function X(e) {
return e.nodeName === "PRE" || e.nodeName === "CODE";
}
function Y(e, t) {
return e.isBlock = E(e), e.isCode = e.nodeName === "CODE" || e.parentNode.isCode, e.isBlank = Q(e), e.flankingWhitespace = J(e, t), e;
}
function Q(e) {
return !k(e) && !$(e) && /^\s*$/i.test(e.textContent) && !F(e) && !V(e);
}
function J(e, t) {
if (e.isBlock || t.preformattedCode && e.isCode)
return { leading: "", trailing: "" };
var n = Z(e.textContent);
return n.leadingAscii && T("left", e, t) && (n.leading = n.leadingNonAscii), n.trailingAscii && T("right", e, t) && (n.trailing = n.trailingNonAscii), { leading: n.leading, trailing: n.trailing };
}
function Z(e) {
var t = e.match(/^(([ \t\r\n]*)(\s*))(?:(?=\S)[\s\S]*\S)?((\s*?)([ \t\r\n]*))$/);
return {
leading: t[1],
// whole string for whitespace-only strings
leadingAscii: t[2],
leadingNonAscii: t[3],
trailing: t[4],
// empty for whitespace-only strings
trailingNonAscii: t[5],
trailingAscii: t[6]
};
}
function T(e, t, n) {
var r, i, a;
return e === "left" ? (r = t.previousSibling, i = / $/) : (r = t.nextSibling, i = /^ /), r && (r.nodeType === 3 ? a = i.test(r.nodeValue) : n.preformattedCode && r.nodeName === "CODE" ? a = !1 : r.nodeType === 1 && !E(r) && (a = i.test(r.textContent))), a;
}
var ee = Array.prototype.reduce, te = [
[/\\/g, "\\\\"],
[/\*/g, "\\*"],
[/^-/g, "\\-"],
[/^\+ /g, "\\+ "],
[/^(=+)/g, "\\$1"],
[/^(#{1,6}) /g, "\\$1 "],
[/`/g, "\\`"],
[/^~~~/g, "\\~~~"],
[/\[/g, "\\["],
[/\]/g, "\\]"],
[/^>/g, "\\>"],
[/_/g, "\\_"],
[/^(\d+)\. /g, "$1\\. "]
];
function d(e) {
if (!(this instanceof d)) return new d(e);
var t = {
rules: s,
headingStyle: "setext",
hr: "* * *",
bulletListMarker: "*",
codeBlockStyle: "indented",
fence: "```",
emDelimiter: "_",
strongDelimiter: "**",
linkStyle: "inlined",
linkReferenceStyle: "full",
br: " ",
preformattedCode: !1,
blankReplacement: function(n, r) {
return r.isBlock ? `
` : "";
},
keepReplacement: function(n, r) {
return r.isBlock ? `
` + r.outerHTML + `
` : r.outerHTML;
},
defaultReplacement: function(n, r) {
return r.isBlock ? `
` + n + `
` : n;
}
};
this.options = x({}, t, e), this.rules = new S(this.options);
}
d.prototype = {
/**
* The entry point for converting a string or DOM node to Markdown
* @public
* @param {String|HTMLElement} input The string or DOM node to convert
* @returns A Markdown representation of the input
* @type String
*/
turndown: function(e) {
if (!ie(e))
throw new TypeError(
e + " is not a string, or an element/document/fragment node."
);
if (e === "") return "";
var t = O.call(this, new q(e, this.options));
return ne.call(this, t);
},
/**
* Add one or more plugins
* @public
* @param {Function|Array} plugin The plugin or array of plugins to add
* @returns The Turndown instance for chaining
* @type Object
*/
use: function(e) {
if (Array.isArray(e))
for (var t = 0; t < e.length; t++) this.use(e[t]);
else if (typeof e == "function")
e(this);
else
throw new TypeError("plugin must be a Function or an Array of Functions");
return this;
},
/**
* Adds a rule
* @public
* @param {String} key The unique key of the rule
* @param {Object} rule The rule
* @returns The Turndown instance for chaining
* @type Object
*/
addRule: function(e, t) {
return this.rules.add(e, t), this;
},
/**
* Keep a node (as HTML) that matches the filter
* @public
* @param {String|Array|Function} filter The unique key of the rule
* @returns The Turndown instance for chaining
* @type Object
*/
keep: function(e) {
return this.rules.keep(e), this;
},
/**
* Remove a node that matches the filter
* @public
* @param {String|Array|Function} filter The unique key of the rule
* @returns The Turndown instance for chaining
* @type Object
*/
remove: function(e) {
return this.rules.remove(e), this;
},
/**
* Escapes Markdown syntax
* @public
* @param {String} string The string to escape
* @returns A string with Markdown syntax escaped
* @type String
*/
escape: function(e) {
return te.reduce(function(t, n) {
return t.replace(n[0], n[1]);
}, e);
}
};
function O(e) {
var t = this;
return ee.call(e.childNodes, function(n, r) {
r = new Y(r, t.options);
var i = "";
return r.nodeType === 3 ? i = r.isCode ? r.nodeValue : t.escape(r.nodeValue) : r.nodeType === 1 && (i = re.call(t, r)), D(n, i);
}, "");
}
function ne(e) {
var t = this;
return this.rules.forEach(function(n) {
typeof n.append == "function" && (e = D(e, n.append(t.options)));
}), e.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
}
function re(e) {
var t = this.rules.forNode(e), n = O.call(this, e), r = e.flankingWhitespace;
return (r.leading || r.trailing) && (n = n.trim()), r.leading + t.replacement(n, e, this.options) + r.trailing;
}
function D(e, t) {
var n = I(e), r = M(t), i = Math.max(e.length - n.length, t.length - r.length), a = `
`.substring(0, i);
return n + a + r;
}
function ie(e) {
return e != null && (typeof e == "string" || e.nodeType && (e.nodeType === 1 || e.nodeType === 9 || e.nodeType === 11));
}
class ae {
constructor() {
// Initialize with default values to fix the "no initializer" error
f(this, "options", {
key: "c",
meta: !1,
turndownOptions: {},
selectorsToRemove: [
"nav",
"header",
"footer",
"aside",
".navigation",
".nav",
".menu",
".sidebar",
".ad",
".ads",
".advertisement",
"script",
"style",
"noscript",
"iframe"
]
});
f(this, "turndownService");
f(this, "initialized", !1);
/**
* Handle keydown events
*/
f(this, "handleKeyDown", (t) => {
(this.options.meta ? t.metaKey : t.ctrlKey) && t.key.toLowerCase() === this.options.key.toLowerCase() && (t.preventDefault(), this.copyPageContent());
});
this.turndownService = new d();
}
/**
* Determine if the current platform is macOS
*/
isMac() {
return navigator.platform.toLowerCase().includes("mac") || navigator.userAgent.toLowerCase().includes("mac");
}
/**
* Initialize the CopyLlmsTxt library
* @param options Configuration options (optional)
*/
init(t) {
if (this.initialized) {
console.warn(
"CopyLlmsTxt is already initialized. Call destroy() before reinitializing."
);
return;
}
const n = {
key: "c",
meta: this.isMac(),
// Use meta (Cmd) on Mac, Ctrl elsewhere
turndownOptions: {},
selectorsToRemove: [
"nav",
"header",
"footer",
"aside",
".navigation",
".nav",
".menu",
".sidebar",
".ad",
".ads",
".advertisement",
"script",
"style",
"noscript",
"iframe"
]
};
this.options = {
...n,
...t
}, this.turndownService = new d(this.options.turndownOptions), document.addEventListener("keydown", this.handleKeyDown), this.initialized = !0, console.log(
`CopyLlmsTxt initialized. Press ${this.options.meta ? "Meta/Cmd" : "Ctrl"}+${this.options.key} to copy content as markdown.`
);
}
/**
* Destroy the CopyLlmsTxt instance and remove event listeners
*/
destroy() {
this.initialized && (document.removeEventListener("keydown", this.handleKeyDown), this.initialized = !1, console.log("CopyLlmsTxt destroyed."));
}
/**
* Copy the current page content as markdown
*/
copyPageContent() {
try {
const t = document.body.cloneNode(!0);
this.cleanupDOM(t);
const n = this.turndownService.turndown(t);
navigator.clipboard.writeText(n).then(() => {
console.log("Page content copied as markdown!");
}).catch((r) => {
console.error("Failed to copy text: ", r);
});
} catch (t) {
console.error("Error copying page content:", t);
}
}
removeNonVisibleElements(t) {
const n = document.body.querySelectorAll("*"), r = t.querySelectorAll("*");
if (n.length !== r.length)
console.warn("Element count mismatch between original and clone");
else
for (let i = r.length - 1; i >= 0; i--) {
const a = n[i], o = r[i], c = window.getComputedStyle(a), l = a.getBoundingClientRect();
(c.display === "none" || c.visibility === "hidden" || c.opacity === "0" || l.height === 0 || l.width === 0) && o.parentNode && o.parentNode.removeChild(o);
}
}
removeElementsBySelector(t) {
[
"nav",
"header",
"footer",
"aside",
".navigation",
".nav",
".menu",
".sidebar",
".ad",
".ads",
".advertisement",
"script",
"style",
"noscript",
"iframe"
].forEach((r) => {
t.querySelectorAll(r).forEach((a) => {
var o;
return (o = a.parentNode) == null ? void 0 : o.removeChild(a);
});
});
}
/**
* Clean up the DOM element by removing unwanted elements
*/
cleanupDOM(t) {
this.removeNonVisibleElements(t), this.removeElementsBySelector(t);
}
/**
* Manually copy the current page content as markdown
*/
copy() {
this.copyPageContent();
}
}
const oe = new ae();
export {
oe as default
};