Oliveboard Plus

Enhance Oliveboard UI and functionality.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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 ? "![" + alt + "](" + src + titlePart + ")" : "";
		}
	};
	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();
})();