Enhance Oliveboard UI and functionality.
// ==UserScript==
// @name Oliveboard Plus
// @namespace ob-enhancer
// @version 1.3.0
// @author quantavil
// @description Enhance Oliveboard UI and functionality.
// @license MIT
// @match *://*.oliveboard.in/*
// @run-at document-start
// ==/UserScript==
(function() {
"use strict";
var ICONS = {
idle: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 4v12m0 0l-4-4m4 4l4-4M4 20h16"/></svg>`,
loading: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle class="tb-sp" cx="12" cy="12" r="10" stroke-dasharray="32" stroke-linecap="round"/><path class="tb-x" d="M9 9l6 6M15 9l-6 6"/></svg>`,
success: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path class="tb-chk" d="M5 13l4 4L19 7"/></svg>`,
error: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>`
};
var STYLES = `
#tb-fab { position:fixed; bottom:24px; right:24px; width:44px; height:44px; border-radius:22px; background:#000; color:#fff; display:grid; place-items:center; cursor:pointer; transition:all .3s cubic-bezier(.25,.8,.25,1); box-shadow:0 4px 12px rgba(0,0,0,.15); z-index:99999; border:1px solid transparent; }
#tb-fab svg { width:20px; height:20px; transition:all .3s; }
#tb-fab[data-s="idle"] { opacity:0.3; }
#tb-fab[data-s="idle"]:hover { opacity:1; box-shadow:0 6px 16px rgba(0,0,0,.25); }
#tb-fab[data-s="idle"]:active { transform:scale(.92); }
#tb-fab[data-s="success"] { background:#fff; color:#000; border-color:#e5e5e5; pointer-events:none; transform:scale(1.1); }
#tb-fab[data-s="error"] { background:#fff; color:#000; border-color:#000; animation:sh .4s; }
#tb-fab::after { content:attr(data-t); position:absolute; right:56px; background:#000; color:#fff; padding:6px 12px; border-radius:6px; font:500 12px/1.2 -apple-system,sans-serif; opacity:0; pointer-events:none; transition:opacity .2s; white-space:nowrap; box-shadow:0 2px 8px rgba(0,0,0,.2); }
#tb-fab:hover::after, #tb-fab[data-s="loading"]::after, #tb-fab[data-s="error"]::after { opacity:1; }
#tb-fab[data-s="idle"]:not(:hover)::after { opacity:0; }
#tb-fab .tb-x { opacity:0; transition:opacity .2s; }
#tb-fab[data-s="loading"]:hover .tb-sp { opacity:0; }
#tb-fab[data-s="loading"]:hover .tb-x { opacity:1; }
#tb-fab[data-s="loading"]:hover::after { content:"Cancel"; }
@keyframes sh { 25%,75%{transform:translateX(-4px)} 50%{transform:translateX(4px)} }
#tb-fab .tb-chk { stroke-dasharray:24; stroke-dashoffset:24; animation:tb-draw .4s forwards .1s; }
@keyframes tb-draw { to { stroke-dashoffset:0; } }
`;
var DownloaderUI = class {
onStart;
onCancel;
el = document.createElement("div");
state = "idle";
spinRAF = null;
timer = null;
constructor(onStart, onCancel) {
this.onStart = onStart;
this.onCancel = onCancel;
this.el.id = "tb-fab";
this.el.onclick = () => {
if (this.state === "idle" || this.state === "error") {
this.setState("loading");
return this.onStart();
}
if (this.state === "loading") {
this.setState("idle");
return this.onCancel();
}
};
this.setState("idle");
}
startSpin() {
const el = this.el.querySelector(".tb-sp");
if (!el) return;
let last = null;
let angle = 0;
const tick = (now) => {
if (last !== null) angle = (angle + (now - last) * .36) % 360;
last = now;
el.setAttribute("transform", `rotate(${angle} 12 12)`);
this.spinRAF = requestAnimationFrame(tick);
};
this.spinRAF = requestAnimationFrame(tick);
}
stopSpin() {
if (this.spinRAF !== null) {
cancelAnimationFrame(this.spinRAF);
this.spinRAF = null;
}
}
setState(s) {
this.stopSpin();
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
this.state = s;
this.el.dataset.s = s;
this.el.innerHTML = ICONS[s];
this.el.dataset.t = {
idle: "Download",
loading: "Crawling…",
success: "Done!",
error: "Failed — retry"
}[s];
if (s === "loading") this.startSpin();
}
mount() {
if (!document.getElementById("tb-css")) document.head.insertAdjacentHTML("beforeend", `<style id="tb-css">${STYLES}</style>`);
document.body.appendChild(this.el);
}
updateStatus(m) {
if (this.state === "loading") this.el.dataset.t = m;
}
error(m) {
this.setState("error");
if (m) this.el.dataset.t = `Error: ${m}`;
this.timer = window.setTimeout(() => {
if (this.state === "error") this.setState("idle");
this.timer = null;
}, 5e3);
}
finish() {
this.setState("success");
this.timer = window.setTimeout(() => {
if (this.state === "success") this.setState("idle");
this.timer = null;
}, 2500);
}
};
function extend(destination) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) if (Object.prototype.hasOwnProperty.call(source, key)) destination[key] = source[key];
}
return destination;
}
function repeat(character, count) {
return Array(count + 1).join(character);
}
function trimLeadingNewlines(string) {
return string.replace(/^\n*/, "");
}
function trimTrailingNewlines(string) {
var indexEnd = string.length;
while (indexEnd > 0 && string[indexEnd - 1] === "\n") indexEnd--;
return string.substring(0, indexEnd);
}
function trimNewlines(string) {
return trimTrailingNewlines(trimLeadingNewlines(string));
}
var blockElements = [
"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 isBlock(node) {
return is(node, blockElements);
}
var voidElements = [
"AREA",
"BASE",
"BR",
"COL",
"COMMAND",
"EMBED",
"HR",
"IMG",
"INPUT",
"KEYGEN",
"LINK",
"META",
"PARAM",
"SOURCE",
"TRACK",
"WBR"
];
function isVoid(node) {
return is(node, voidElements);
}
function hasVoid(node) {
return has(node, voidElements);
}
var meaningfulWhenBlankElements = [
"A",
"TABLE",
"THEAD",
"TBODY",
"TFOOT",
"TH",
"TD",
"IFRAME",
"SCRIPT",
"AUDIO",
"VIDEO"
];
function isMeaningfulWhenBlank(node) {
return is(node, meaningfulWhenBlankElements);
}
function hasMeaningfulWhenBlank(node) {
return has(node, meaningfulWhenBlankElements);
}
function is(node, tagNames) {
return tagNames.indexOf(node.nodeName) >= 0;
}
function has(node, tagNames) {
return node.getElementsByTagName && tagNames.some(function(tagName) {
return node.getElementsByTagName(tagName).length;
});
}
var markdownEscapes = [
[/\\/g, "\\\\"],
[/\*/g, "\\*"],
[/^-/g, "\\-"],
[/^\+ /g, "\\+ "],
[/^(=+)/g, "\\$1"],
[/^(#{1,6}) /g, "\\$1 "],
[/`/g, "\\`"],
[/^~~~/g, "\\~~~"],
[/\[/g, "\\["],
[/\]/g, "\\]"],
[/^>/g, "\\>"],
[/_/g, "\\_"],
[/^(\d+)\. /g, "$1\\. "]
];
function escapeMarkdown(string) {
return markdownEscapes.reduce(function(accumulator, escape) {
return accumulator.replace(escape[0], escape[1]);
}, string);
}
var rules$1 = {};
rules$1.paragraph = {
filter: "p",
replacement: function(content) {
return "\n\n" + content + "\n\n";
}
};
rules$1.lineBreak = {
filter: "br",
replacement: function(content, node, options) {
return options.br + "\n";
}
};
rules$1.heading = {
filter: [
"h1",
"h2",
"h3",
"h4",
"h5",
"h6"
],
replacement: function(content, node, options) {
var hLevel = Number(node.nodeName.charAt(1));
if (options.headingStyle === "setext" && hLevel < 3) {
var underline = repeat(hLevel === 1 ? "=" : "-", content.length);
return "\n\n" + content + "\n" + underline + "\n\n";
} else return "\n\n" + repeat("#", hLevel) + " " + content + "\n\n";
}
};
rules$1.blockquote = {
filter: "blockquote",
replacement: function(content) {
content = trimNewlines(content).replace(/^/gm, "> ");
return "\n\n" + content + "\n\n";
}
};
rules$1.list = {
filter: ["ul", "ol"],
replacement: function(content, node) {
var parent = node.parentNode;
if (parent.nodeName === "LI" && parent.lastElementChild === node) return "\n" + content;
else return "\n\n" + content + "\n\n";
}
};
rules$1.listItem = {
filter: "li",
replacement: function(content, node, options) {
var prefix = options.bulletListMarker + " ";
var parent = node.parentNode;
if (parent.nodeName === "OL") {
var start = parent.getAttribute("start");
var index = Array.prototype.indexOf.call(parent.children, node);
prefix = (start ? Number(start) + index : index + 1) + ". ";
}
var isParagraph = /\n$/.test(content);
content = trimNewlines(content) + (isParagraph ? "\n" : "");
content = content.replace(/\n/gm, "\n" + " ".repeat(prefix.length));
return prefix + content + (node.nextSibling ? "\n" : "");
}
};
rules$1.indentedCodeBlock = {
filter: function(node, options) {
return options.codeBlockStyle === "indented" && node.nodeName === "PRE" && node.firstChild && node.firstChild.nodeName === "CODE";
},
replacement: function(content, node, options) {
return "\n\n " + node.firstChild.textContent.replace(/\n/g, "\n ") + "\n\n";
}
};
rules$1.fencedCodeBlock = {
filter: function(node, options) {
return options.codeBlockStyle === "fenced" && node.nodeName === "PRE" && node.firstChild && node.firstChild.nodeName === "CODE";
},
replacement: function(content, node, options) {
var language = ((node.firstChild.getAttribute("class") || "").match(/language-(\S+)/) || [null, ""])[1];
var code = node.firstChild.textContent;
var fenceChar = options.fence.charAt(0);
var fenceSize = 3;
var fenceInCodeRegex = new RegExp("^" + fenceChar + "{3,}", "gm");
var match;
while (match = fenceInCodeRegex.exec(code)) if (match[0].length >= fenceSize) fenceSize = match[0].length + 1;
var fence = repeat(fenceChar, fenceSize);
return "\n\n" + fence + language + "\n" + code.replace(/\n$/, "") + "\n" + fence + "\n\n";
}
};
rules$1.horizontalRule = {
filter: "hr",
replacement: function(content, node, options) {
return "\n\n" + options.hr + "\n\n";
}
};
rules$1.inlineLink = {
filter: function(node, options) {
return options.linkStyle === "inlined" && node.nodeName === "A" && node.getAttribute("href");
},
replacement: function(content, node) {
var href = escapeLinkDestination(node.getAttribute("href"));
var title = escapeLinkTitle(cleanAttribute(node.getAttribute("title")));
var titlePart = title ? " \"" + title + "\"" : "";
return "[" + content + "](" + href + titlePart + ")";
}
};
rules$1.referenceLink = {
filter: function(node, options) {
return options.linkStyle === "referenced" && node.nodeName === "A" && node.getAttribute("href");
},
replacement: function(content, node, options) {
var href = escapeLinkDestination(node.getAttribute("href"));
var title = cleanAttribute(node.getAttribute("title"));
if (title) title = " \"" + escapeLinkTitle(title) + "\"";
var replacement;
var reference;
switch (options.linkReferenceStyle) {
case "collapsed":
replacement = "[" + content + "][]";
reference = "[" + content + "]: " + href + title;
break;
case "shortcut":
replacement = "[" + content + "]";
reference = "[" + content + "]: " + href + title;
break;
default:
var id = this.references.length + 1;
replacement = "[" + content + "][" + id + "]";
reference = "[" + id + "]: " + href + title;
}
this.references.push(reference);
return replacement;
},
references: [],
append: function(options) {
var references = "";
if (this.references.length) {
references = "\n\n" + this.references.join("\n") + "\n\n";
this.references = [];
}
return references;
}
};
rules$1.emphasis = {
filter: ["em", "i"],
replacement: function(content, node, options) {
if (!content.trim()) return "";
return options.emDelimiter + content + options.emDelimiter;
}
};
rules$1.strong = {
filter: ["strong", "b"],
replacement: function(content, node, options) {
if (!content.trim()) return "";
return options.strongDelimiter + content + options.strongDelimiter;
}
};
rules$1.code = {
filter: function(node) {
var hasSiblings = node.previousSibling || node.nextSibling;
var isCodeBlock = node.parentNode.nodeName === "PRE" && !hasSiblings;
return node.nodeName === "CODE" && !isCodeBlock;
},
replacement: function(content) {
if (!content) return "";
content = content.replace(/\r?\n|\r/g, " ");
var extraSpace = /^`|^ .*?[^ ].* $|`$/.test(content) ? " " : "";
var delimiter = "`";
var matches = content.match(/`+/gm) || [];
while (matches.indexOf(delimiter) !== -1) delimiter = delimiter + "`";
return delimiter + extraSpace + content + extraSpace + delimiter;
}
};
rules$1.image = {
filter: "img",
replacement: function(content, node) {
var alt = escapeMarkdown(cleanAttribute(node.getAttribute("alt")));
var src = escapeLinkDestination(node.getAttribute("src") || "");
var title = cleanAttribute(node.getAttribute("title"));
var titlePart = title ? " \"" + escapeLinkTitle(title) + "\"" : "";
return src ? "" : "";
}
};
function cleanAttribute(attribute) {
return attribute ? attribute.replace(/(\n+\s*)+/g, "\n") : "";
}
function escapeLinkDestination(destination) {
var escaped = destination.replace(/([<>()])/g, "\\$1");
return escaped.indexOf(" ") >= 0 ? "<" + escaped + ">" : escaped;
}
function escapeLinkTitle(title) {
return title.replace(/"/g, "\\\"");
}
function Rules(options) {
this.options = options;
this._keep = [];
this._remove = [];
this.blankRule = { replacement: options.blankReplacement };
this.keepReplacement = options.keepReplacement;
this.defaultRule = { replacement: options.defaultReplacement };
this.array = [];
for (var key in options.rules) this.array.push(options.rules[key]);
}
Rules.prototype = {
add: function(key, rule) {
this.array.unshift(rule);
},
keep: function(filter) {
this._keep.unshift({
filter,
replacement: this.keepReplacement
});
},
remove: function(filter) {
this._remove.unshift({
filter,
replacement: function() {
return "";
}
});
},
forNode: function(node) {
if (node.isBlank) return this.blankRule;
var rule;
if (rule = findRule(this.array, node, this.options)) return rule;
if (rule = findRule(this._keep, node, this.options)) return rule;
if (rule = findRule(this._remove, node, this.options)) return rule;
return this.defaultRule;
},
forEach: function(fn) {
for (var i = 0; i < this.array.length; i++) fn(this.array[i], i);
}
};
function findRule(rules, node, options) {
for (var i = 0; i < rules.length; i++) {
var rule = rules[i];
if (filterValue(rule, node, options)) return rule;
}
}
function filterValue(rule, node, options) {
var filter = rule.filter;
if (typeof filter === "string") {
if (filter === node.nodeName.toLowerCase()) return true;
} else if (Array.isArray(filter)) {
if (filter.indexOf(node.nodeName.toLowerCase()) > -1) return true;
} else if (typeof filter === "function") {
if (filter.call(rule, node, options)) return true;
} else throw new TypeError("`filter` needs to be a string, array, or function");
}
function collapseWhitespace(options) {
var element = options.element;
var isBlock = options.isBlock;
var isVoid = options.isVoid;
var isPre = options.isPre || function(node) {
return node.nodeName === "PRE";
};
if (!element.firstChild || isPre(element)) return;
var prevText = null;
var keepLeadingWs = false;
var prev = null;
var node = next(prev, element, isPre);
while (node !== element) {
if (node.nodeType === 3 || node.nodeType === 4) {
var text = node.data.replace(/[ \r\n\t]+/g, " ");
if ((!prevText || / $/.test(prevText.data)) && !keepLeadingWs && text[0] === " ") text = text.substr(1);
if (!text) {
node = remove(node);
continue;
}
node.data = text;
prevText = node;
} else if (node.nodeType === 1) {
if (isBlock(node) || node.nodeName === "BR") {
if (prevText) prevText.data = prevText.data.replace(/ $/, "");
prevText = null;
keepLeadingWs = false;
} else if (isVoid(node) || isPre(node)) {
prevText = null;
keepLeadingWs = true;
} else if (prevText) keepLeadingWs = false;
} else {
node = remove(node);
continue;
}
var nextNode = next(prev, node, isPre);
prev = node;
node = nextNode;
}
if (prevText) {
prevText.data = prevText.data.replace(/ $/, "");
if (!prevText.data) remove(prevText);
}
}
function remove(node) {
var next = node.nextSibling || node.parentNode;
node.parentNode.removeChild(node);
return next;
}
function next(prev, current, isPre) {
if (prev && prev.parentNode === current || isPre(current)) return current.nextSibling || current.parentNode;
return current.firstChild || current.nextSibling || current.parentNode;
}
var root = typeof window !== "undefined" ? window : {};
function canParseHTMLNatively() {
var Parser = root.DOMParser;
var canParse = false;
try {
if (new Parser().parseFromString("", "text/html")) canParse = true;
} catch (e) {}
return canParse;
}
function createHTMLParser() {
var Parser = function() {};
if (shouldUseActiveX()) Parser.prototype.parseFromString = function(string) {
var doc = new window.ActiveXObject("htmlfile");
doc.designMode = "on";
doc.open();
doc.write(string);
doc.close();
return doc;
};
else Parser.prototype.parseFromString = function(string) {
var doc = document.implementation.createHTMLDocument("");
doc.open();
doc.write(string);
doc.close();
return doc;
};
return Parser;
}
function shouldUseActiveX() {
var useActiveX = false;
try {
document.implementation.createHTMLDocument("").open();
} catch (e) {
if (root.ActiveXObject) useActiveX = true;
}
return useActiveX;
}
var HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser();
function RootNode(input, options) {
var root;
if (typeof input === "string") root = htmlParser().parseFromString("<x-turndown id=\"turndown-root\">" + input + "</x-turndown>", "text/html").getElementById("turndown-root");
else root = input.cloneNode(true);
collapseWhitespace({
element: root,
isBlock,
isVoid,
isPre: options.preformattedCode ? isPreOrCode : null
});
return root;
}
var _htmlParser;
function htmlParser() {
_htmlParser = _htmlParser || new HTMLParser();
return _htmlParser;
}
function isPreOrCode(node) {
return node.nodeName === "PRE" || node.nodeName === "CODE";
}
function Node(node, options) {
node.isBlock = isBlock(node);
node.isCode = node.nodeName === "CODE" || node.parentNode.isCode;
node.isBlank = isBlank(node);
node.flankingWhitespace = flankingWhitespace(node, options);
return node;
}
function isBlank(node) {
return !isVoid(node) && !isMeaningfulWhenBlank(node) && /^\s*$/i.test(node.textContent) && !hasVoid(node) && !hasMeaningfulWhenBlank(node);
}
function flankingWhitespace(node, options) {
if (node.isBlock || options.preformattedCode && node.isCode) return {
leading: "",
trailing: ""
};
var edges = edgeWhitespace(node.textContent);
if (edges.leadingAscii && isFlankedByWhitespace("left", node, options)) edges.leading = edges.leadingNonAscii;
if (edges.trailingAscii && isFlankedByWhitespace("right", node, options)) edges.trailing = edges.trailingNonAscii;
return {
leading: edges.leading,
trailing: edges.trailing
};
}
function edgeWhitespace(string) {
var m = string.match(/^(([ \t\r\n]*)(\s*))(?:(?=\S)[\s\S]*\S)?((\s*?)([ \t\r\n]*))$/);
return {
leading: m[1],
leadingAscii: m[2],
leadingNonAscii: m[3],
trailing: m[4],
trailingNonAscii: m[5],
trailingAscii: m[6]
};
}
function isFlankedByWhitespace(side, node, options) {
var sibling;
var regExp;
var isFlanked;
if (side === "left") {
sibling = node.previousSibling;
regExp = / $/;
} else {
sibling = node.nextSibling;
regExp = /^ /;
}
if (sibling) {
if (sibling.nodeType === 3) isFlanked = regExp.test(sibling.nodeValue);
else if (options.preformattedCode && sibling.nodeName === "CODE") isFlanked = false;
else if (sibling.nodeType === 1 && !isBlock(sibling)) isFlanked = regExp.test(sibling.textContent);
}
return isFlanked;
}
var reduce = Array.prototype.reduce;
function TurndownService(options) {
if (!(this instanceof TurndownService)) return new TurndownService(options);
var defaults = {
rules: rules$1,
headingStyle: "setext",
hr: "* * *",
bulletListMarker: "*",
codeBlockStyle: "indented",
fence: "```",
emDelimiter: "_",
strongDelimiter: "**",
linkStyle: "inlined",
linkReferenceStyle: "full",
br: " ",
preformattedCode: false,
blankReplacement: function(content, node) {
return node.isBlock ? "\n\n" : "";
},
keepReplacement: function(content, node) {
return node.isBlock ? "\n\n" + node.outerHTML + "\n\n" : node.outerHTML;
},
defaultReplacement: function(content, node) {
return node.isBlock ? "\n\n" + content + "\n\n" : content;
}
};
this.options = extend({}, defaults, options);
this.rules = new Rules(this.options);
}
TurndownService.prototype = {
turndown: function(input) {
if (!canConvert(input)) throw new TypeError(input + " is not a string, or an element/document/fragment node.");
if (input === "") return "";
var output = process.call(this, new RootNode(input, this.options));
return postProcess.call(this, output);
},
use: function(plugin) {
if (Array.isArray(plugin)) for (var i = 0; i < plugin.length; i++) this.use(plugin[i]);
else if (typeof plugin === "function") plugin(this);
else throw new TypeError("plugin must be a Function or an Array of Functions");
return this;
},
addRule: function(key, rule) {
this.rules.add(key, rule);
return this;
},
keep: function(filter) {
this.rules.keep(filter);
return this;
},
remove: function(filter) {
this.rules.remove(filter);
return this;
},
escape: function(string) {
return escapeMarkdown(string);
}
};
function process(parentNode) {
var self = this;
return reduce.call(parentNode.childNodes, function(output, node) {
node = new Node(node, self.options);
var replacement = "";
if (node.nodeType === 3) replacement = node.isCode ? node.nodeValue : self.escape(node.nodeValue);
else if (node.nodeType === 1) replacement = replacementForNode.call(self, node);
return join(output, replacement);
}, "");
}
function postProcess(output) {
var self = this;
this.rules.forEach(function(rule) {
if (typeof rule.append === "function") output = join(output, rule.append(self.options));
});
return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
}
function replacementForNode(node) {
var rule = this.rules.forNode(node);
var content = process.call(this, node);
var whitespace = node.flankingWhitespace;
if (whitespace.leading || whitespace.trailing) content = content.trim();
return whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing;
}
function join(output, replacement) {
var s1 = trimTrailingNewlines(output);
var s2 = trimLeadingNewlines(replacement);
var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
return s1 + "\n\n".substring(0, nls) + s2;
}
function canConvert(input) {
return input != null && (typeof input === "string" || input.nodeType && (input.nodeType === 1 || input.nodeType === 9 || input.nodeType === 11));
}
var highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/;
function highlightedCodeBlock(turndownService) {
turndownService.addRule("highlightedCodeBlock", {
filter: function(node) {
var firstChild = node.firstChild;
return node.nodeName === "DIV" && highlightRegExp.test(node.className) && firstChild && firstChild.nodeName === "PRE";
},
replacement: function(content, node, options) {
var language = ((node.className || "").match(highlightRegExp) || [null, ""])[1];
return "\n\n" + options.fence + language + "\n" + node.firstChild.textContent + "\n" + options.fence + "\n\n";
}
});
}
function strikethrough(turndownService) {
turndownService.addRule("strikethrough", {
filter: [
"del",
"s",
"strike"
],
replacement: function(content) {
return "~" + content + "~";
}
});
}
var indexOf = Array.prototype.indexOf;
var every = Array.prototype.every;
var rules = {};
rules.tableCell = {
filter: ["th", "td"],
replacement: function(content, node) {
return cell(content, node);
}
};
rules.tableRow = {
filter: "tr",
replacement: function(content, node) {
var borderCells = "";
var alignMap = {
left: ":--",
right: "--:",
center: ":-:"
};
if (isHeadingRow(node)) for (var i = 0; i < node.childNodes.length; i++) {
var border = "---";
var align = (node.childNodes[i].getAttribute("align") || "").toLowerCase();
if (align) border = alignMap[align] || border;
borderCells += cell(border, node.childNodes[i]);
}
return "\n" + content + (borderCells ? "\n" + borderCells : "");
}
};
rules.table = {
filter: function(node) {
return node.nodeName === "TABLE" && isHeadingRow(node.rows[0]);
},
replacement: function(content) {
content = content.replace("\n\n", "\n");
return "\n\n" + content + "\n\n";
}
};
rules.tableSection = {
filter: [
"thead",
"tbody",
"tfoot"
],
replacement: function(content) {
return content;
}
};
function isHeadingRow(tr) {
var parentNode = tr.parentNode;
return parentNode.nodeName === "THEAD" || parentNode.firstChild === tr && (parentNode.nodeName === "TABLE" || isFirstTbody(parentNode)) && every.call(tr.childNodes, function(n) {
return n.nodeName === "TH";
});
}
function isFirstTbody(element) {
var previousSibling = element.previousSibling;
return element.nodeName === "TBODY" && (!previousSibling || previousSibling.nodeName === "THEAD" && /^\s*$/i.test(previousSibling.textContent));
}
function cell(content, node) {
var index = indexOf.call(node.parentNode.childNodes, node);
var prefix = " ";
if (index === 0) prefix = "| ";
return prefix + content + " |";
}
function tables(turndownService) {
turndownService.keep(function(node) {
return node.nodeName === "TABLE" && !isHeadingRow(node.rows[0]);
});
for (var key in rules) turndownService.addRule(key, rules[key]);
}
function taskListItems(turndownService) {
turndownService.addRule("taskListItems", {
filter: function(node) {
return node.type === "checkbox" && node.parentNode.nodeName === "LI";
},
replacement: function(content, node) {
return (node.checked ? "[x]" : "[ ]") + " ";
}
});
}
function gfm(turndownService) {
turndownService.use([
highlightedCodeBlock,
strikethrough,
tables,
taskListItems
]);
}
var instance = null;
function getTurndownService() {
if (!instance) {
instance = new TurndownService({
headingStyle: "atx",
bulletListMarker: "-",
codeBlockStyle: "fenced"
});
instance.use(gfm);
instance.addRule("fractions", {
filter: function(node) {
if (node.nodeName === "DIV") {
const el = node;
const style = el.getAttribute("style") || "";
if (style.includes("inline-block") && style.includes("vertical-align: middle")) {
if (Array.from(el.children).filter((n) => n.nodeName === "SPAN").length >= 2) return true;
}
}
return false;
},
replacement: function(_content, node) {
const el = node;
const spans = Array.from(el.children).filter((n) => n.nodeName === "SPAN");
return ` (${spans[0].textContent?.trim() || ""}/${spans[spans.length - 1].textContent?.trim() || ""}) `;
}
});
}
return instance;
}
function fixImageUrls(html) {
if (!html) return "";
let result = html.replace(/src=["']\/\/(?!u1\.oliveboard)/gi, "src=\"https://");
result = result.replace(/src=["'](?:\/)?(oliveimg\/[^"']+)["']/gi, "src=\"https://u1.oliveboard.in/exams/solution/$1\"");
result = result.replace(/src=["'](?:\/)?(images\/[^"']+)["']/gi, "src=\"https://u1.oliveboard.in/exams/solution/$1\"");
return result;
}
function getSectionRelativeIndex(globalDomIndex) {
if (globalDomIndex <= 0) return 0;
const boxes = document.querySelectorAll(".question-map .box");
for (const box of Array.from(boxes)) {
const qSpans = box.querySelectorAll(".map-qno");
for (let i = 0; i < qSpans.length; i++) {
const match = (qSpans[i].getAttribute("onclick") || "").match(/goToQuestion\((\d+)\)/);
if (match && parseInt(match[1], 10) === globalDomIndex) return i + 1;
}
}
return globalDomIndex;
}
function extractCurrentQuestion(fallbackIndex) {
let activeBlock = null;
const blocks = Array.from(document.querySelectorAll(".singleqid, .paneqid"));
let domIndex = 0;
for (let i = 0; i < blocks.length; i++) if (window.getComputedStyle(blocks[i]).display !== "none") {
activeBlock = blocks[i];
domIndex = i + 1;
break;
}
if (!activeBlock) return null;
const sectionName = document.querySelector(".ddn-select")?.textContent?.trim() || "Unknown Section";
try {
const paneTxt = activeBlock.querySelector(".panetxt .eqt") || activeBlock.querySelector(".panetxt");
const directionsHtml = paneTxt ? fixImageUrls(paneTxt.innerHTML.trim()) + "<br><br>" : "";
const qblock = activeBlock.querySelector(".qblock .eqt") || activeBlock.querySelector(".qblock");
const questionHtml = directionsHtml + (qblock ? fixImageUrls(qblock.innerHTML.trim()) : "");
const options = [];
activeBlock.querySelectorAll(".opt").forEach((optBlock) => {
const labelEl = optBlock.querySelector(".left");
const textEl = optBlock.querySelector(".rightopt .eqt") || optBlock.querySelector(".rightopt");
const label = labelEl ? labelEl.textContent?.trim() || "" : "";
const html = textEl ? fixImageUrls(textEl.innerHTML.trim()) : "";
const isCorrect = optBlock.classList.contains("correct");
if (label || html) options.push({
label,
html,
isCorrect
});
});
const solblock = activeBlock.querySelector(".solutiontxt .eqt") || activeBlock.querySelector(".solutiontxt");
const solutionHtml = solblock ? fixImageUrls(solblock.innerHTML.trim()) : "";
return {
index: domIndex ? getSectionRelativeIndex(domIndex) : fallbackIndex,
sectionName,
questionHtml,
options,
solutionHtml
};
} catch (err) {
console.error(`[OB+] Error parsing question block`, err);
return null;
}
}
function formatQuestion(q, displayIndex) {
const td = getTurndownService();
let markdown = `### Q${displayIndex}\n\n`;
markdown += `${td.turndown(q.questionHtml)}\n\n`;
q.options.forEach((opt) => {
const optionMark = opt.isCorrect ? `**[Correct]** ` : ``;
const optionMarkdown = td.turndown(opt.html).replace(/\n/g, " ");
markdown += `- **${opt.label}**: ${optionMark}${optionMarkdown}\n`;
});
markdown += `\n`;
if (q.solutionHtml) {
markdown += `**Solution:**\n\n`;
markdown += `${td.turndown(q.solutionHtml)}\n\n`;
}
return markdown;
}
var Crawler = class {
onUpdate;
onComplete;
onError;
isCancelled = false;
questionsData = new Map();
currentIndex = 1;
constructor(onUpdate, onComplete, onError) {
this.onUpdate = onUpdate;
this.onComplete = onComplete;
this.onError = onError;
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
cancel() {
this.isCancelled = true;
}
hashString(str) {
let hash = 5381;
for (let i = 0; i < str.length; i++) hash = (hash << 5) + hash + str.charCodeAt(i) | 0;
return hash.toString(36);
}
parseSectionMap() {
const sections = [];
document.querySelectorAll(".question-map .box").forEach((box) => {
const name = (box.querySelector("p b") || box.querySelector("p"))?.textContent?.trim() || "Unknown Section";
const qSpans = box.querySelectorAll(".map-qno");
const questionIndices = [];
qSpans.forEach((span) => {
const match = (span.getAttribute("onclick") || "").match(/goToQuestion\((\d+)\)/);
if (match) questionIndices.push(parseInt(match[1], 10));
});
if (questionIndices.length > 0) sections.push({
name,
questionIndices
});
});
return sections;
}
navigateToQuestion(globalIndex) {
const span = document.querySelector(`.map-qno.q-${globalIndex}`);
if (span) span.click();
}
async start() {
this.isCancelled = false;
this.questionsData.clear();
this.currentIndex = 1;
const sections = this.parseSectionMap();
if (sections.length === 0) {
await this.linearCrawl();
return;
}
const totalQuestions = sections.reduce((sum, s) => sum + s.questionIndices.length, 0);
let processedCount = 0;
for (const section of sections) {
if (this.isCancelled) break;
this.onUpdate(`Section: ${section.name}`);
await this.sleep(50);
if (this.isCancelled) break;
for (const globalIdx of section.questionIndices) {
if (this.isCancelled) break;
processedCount++;
this.onUpdate(`${section.name} — Q${processedCount}/${totalQuestions}`);
this.navigateToQuestion(globalIdx);
await this.sleep(10);
if (this.isCancelled) break;
const qData = extractCurrentQuestion(this.currentIndex);
if (!qData) {
await this.sleep(50);
if (this.isCancelled) break;
const retry = extractCurrentQuestion(this.currentIndex);
if (!retry) continue;
this.addQuestion(retry, section.name);
} else this.addQuestion(qData, section.name);
}
}
if (this.isCancelled) {
this.onUpdate("Cancelled.");
return;
}
if (this.questionsData.size === 0) {
this.onError("No questions found.");
return;
}
this.onUpdate("Generating Markdown...");
const md = this.generateMarkdown(sections);
this.onComplete(md);
}
getQuestionSignature(qData) {
const rawContent = qData.questionHtml + "|" + qData.options.map((o) => o.label + ":" + o.html).join("|");
return this.hashString(rawContent.trim());
}
addQuestion(qData, sectionName) {
const signature = this.getQuestionSignature(qData);
if (!this.questionsData.has(signature)) {
qData.sectionName = sectionName;
this.questionsData.set(signature, qData);
this.currentIndex++;
}
}
async linearCrawl() {
let sameQuestionCount = 0;
let consecutiveNulls = 0;
let lastSignature = "";
while (!this.isCancelled) {
this.onUpdate(`Extracting Q${this.currentIndex}...`);
await this.sleep(10);
if (this.isCancelled) break;
const qData = extractCurrentQuestion(this.currentIndex);
if (!qData) {
consecutiveNulls++;
if (consecutiveNulls > 3) {
this.onUpdate("No questions found on page.");
break;
}
const nextBtn = Array.from(document.querySelectorAll("button.btn-prenext")).find((btn) => btn.textContent?.includes("Next"));
if (nextBtn && !nextBtn.disabled && nextBtn.style.display !== "none") {
nextBtn.click();
await this.sleep(50);
} else break;
continue;
}
consecutiveNulls = 0;
const signature = this.getQuestionSignature(qData);
if (signature === lastSignature) {
sameQuestionCount++;
if (sameQuestionCount > 3) {
this.onUpdate(`Reached the end or stuck.`);
break;
}
} else {
sameQuestionCount = 0;
lastSignature = signature;
if (!this.questionsData.has(signature)) {
this.questionsData.set(signature, qData);
this.currentIndex++;
} else break;
}
const nextBtn = Array.from(document.querySelectorAll("button.btn-prenext")).find((btn) => btn.textContent?.includes("Next"));
if (nextBtn && !nextBtn.disabled && nextBtn.style.display !== "none") {
nextBtn.click();
await this.sleep(50);
} else break;
}
if (this.isCancelled) {
this.onUpdate("Cancelled.");
return;
}
if (this.questionsData.size === 0) {
this.onError("No questions found.");
return;
}
this.onUpdate("Generating Markdown...");
const md = this.generateMarkdown([]);
this.onComplete(md);
}
generateMarkdown(sections) {
let markdown = `# Oliveboard Exam\n\n`;
if (sections.length > 0) {
const grouped = new Map();
for (const section of sections) if (!grouped.has(section.name)) grouped.set(section.name, []);
for (const q of this.questionsData.values()) {
const sectionName = q.sectionName || "Unknown Section";
if (!grouped.has(sectionName)) grouped.set(sectionName, []);
grouped.get(sectionName).push(q);
}
for (const [sectionName, questions] of grouped) {
if (questions.length === 0) continue;
markdown += `## ${sectionName}\n\n`;
let sectionQNo = 1;
for (const q of questions) {
markdown += formatQuestion(q, sectionQNo) + `---\n\n`;
sectionQNo++;
}
}
} else {
let displayIndex = 1;
for (const q of this.questionsData.values()) {
markdown += formatQuestion(q, displayIndex) + `---\n\n`;
displayIndex++;
}
}
return markdown;
}
};
function downloadFile(content, filename) {
const blob = new Blob([content], { type: "text/markdown;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function enableCopyAndRightClick() {
const style = document.createElement("style");
style.textContent = `
* {
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select: text !important;
}
`;
(document.head || document.documentElement).appendChild(style);
[
"contextmenu",
"copy",
"cut",
"paste",
"selectstart"
].forEach((evt) => {
window.addEventListener(evt, (e) => {
e.stopPropagation();
}, true);
});
}
function onReady(fn) {
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", fn);
else fn();
}
function getExportFilename(urlStr = window.location.href) {
try {
const url = new URL(urlStr);
const c = url.searchParams.get("c")?.trim();
const testid = url.searchParams.get("testid")?.trim();
if (c && testid) return `Oliveboard_${c}_${testid}.md`;
else if (c) return `Oliveboard_${c}.md`;
else if (testid) return `Oliveboard_test_${testid}.md`;
} catch {}
return "Oliveboard_Questions.md";
}
var selectorsToHide = [
"#sec-update",
"#rightcol",
".obadcard",
".limenuitem:has(a[href*=\"refer\"])",
".limenuitem:has(a[href*=\"testimonials\"])",
".limenuitem:has(a[href*=\"edge/?c=1851\"])",
".limenuitem:has(a[href*=\"buypayu/validity\"])",
"[data-garden-id=\"buttons.icon_button\"][aria-label=\"Open messaging window\"]",
".sc-1w3tvxe-0",
"iframe[title=\"Close message\"]",
"iframe[title=\"Message from company\"]",
"iframe[title=\"Number of unread messages\"]",
"iframe[title=\"Messaging window\"]",
"iframe#launcher",
"iframe[title^=\"Button to launch messaging window\"]"
];
function hideElements() {
selectorsToHide.forEach((selector) => {
try {
document.querySelectorAll(selector).forEach((el) => {
el.style.display = "none";
});
} catch {}
});
try {
document.querySelectorAll("div[style*=\"z-index: 999999\"]").forEach((el) => {
if (el.style.position === "fixed" && el.querySelector("iframe")) el.style.display = "none";
});
} catch {}
}
function modifyLinks() {
document.querySelectorAll("a[onclick^=\"openwin\"]").forEach((a) => {
const match = (a.getAttribute("onclick") || "").match(/openwin\(['"]([^'"]+)['"]/);
if (match && match[1]) {
a.setAttribute("href", match[1]);
a.setAttribute("target", "_blank");
a.removeAttribute("onclick");
}
});
}
function interceptViewSolutions() {
const script = document.createElement("script");
script.textContent = `
// Poison Zendesk widget globals so it never initializes
const noop = function() { return noop; };
noop.setLocale = noop; noop.identify = noop; noop.hide = noop;
noop.show = noop; noop.activate = noop; noop.on = noop;
['zE', '$zopim', 'zEACLoaded'].forEach(key => {
Object.defineProperty(window, key, {
get: () => noop,
set: () => {}, // silently ignore page's attempts to redefine
configurable: true
});
});
// Override openwin — getter always returns our version, setter silently absorbs
const openwinOverride = function(url, name) { window.open(url, '_blank'); };
Object.defineProperty(window, 'openwin', {
get: () => openwinOverride,
set: () => {}, // silently ignore page's attempts to redefine
configurable: true
});
`;
document.documentElement.appendChild(script);
script.remove();
}
var COPY_SVG = "<rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path>";
var CHECK_SVG = "<path d=\"M20 6L9 17l-5-5\"></path>";
function ensureCopyButton() {
const headerCols = document.querySelectorAll(".solheader-col");
if (headerCols.length === 0) return;
const headerCol = headerCols[0];
if (headerCol.querySelector(".copy-md-btn")) return;
const btnWrapper = document.createElement("div");
btnWrapper.className = "copy-md-btn";
btnWrapper.title = "Copy Markdown";
btnWrapper.innerHTML = `
<button type="button" class="button btn-save-qn" style="display:flex;align-items:center;justify-content:center;margin-left:10px;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
${COPY_SVG}
</svg>
</button>
`;
btnWrapper.onclick = async () => {
const q = extractCurrentQuestion(1);
if (!q) return;
const md = formatQuestion(q, q.index);
try {
await navigator.clipboard.writeText(md);
const svg = btnWrapper.querySelector("svg");
if (svg) {
svg.innerHTML = CHECK_SVG;
setTimeout(() => {
svg.innerHTML = COPY_SVG;
}, 2e3);
}
} catch (err) {
console.error("[OB+] Clipboard write failed:", err);
}
};
const saveQs = headerCol.querySelector(".save-qs");
if (saveQs) saveQs.parentNode?.insertBefore(btnWrapper, saveQs);
else headerCol.appendChild(btnWrapper);
}
function initDownloader() {
let activeCrawler = null;
const ui = new DownloaderUI(() => {
activeCrawler = new Crawler((msg) => ui.updateStatus(msg), (md) => {
ui.finish();
downloadFile(md, getExportFilename());
}, (errMsg) => ui.error(errMsg));
activeCrawler.start();
}, () => activeCrawler?.cancel());
onReady(() => ui.mount());
}
function runMutationHandlers() {
hideElements();
modifyLinks();
ensureCopyButton();
}
function init() {
try {
enableCopyAndRightClick();
} catch (e) {
console.error("[OB+]", e);
}
try {
interceptViewSolutions();
} catch (e) {
console.error("[OB+]", e);
}
try {
initDownloader();
} catch (e) {
console.error("[OB+]", e);
}
onReady(runMutationHandlers);
let pending = false;
new MutationObserver(() => {
if (pending) return;
pending = true;
requestAnimationFrame(() => {
runMutationHandlers();
pending = false;
});
}).observe(document.documentElement, {
childList: true,
subtree: true
});
}
init();
})();