OzBargain Markdown Toolbar

Creates a Markdown toolbar for textboxes on OzBargain and ChoiceCheapies

// ==UserScript==
// @name		OzBargain Markdown Toolbar
// @namespace	nategasm
// @version		1.13
// @description	Creates a Markdown toolbar for textboxes on OzBargain and ChoiceCheapies
// @author		wOxxOm, darkred, nategasm
// @license		MIT
// @include		https://www.ozbargain.com.au/deals/submit
// @include		https://www.ozbargain.com.au/node/*
// @include		https://www.ozbargain.com.au/comment/edit/*
// @include		https://www.ozbargain.com.au/privatemsg/*
// @include		https://www.cheapies.nz/deals/submit
// @include		https://www.cheapies.nz/node/*
// @include		https://www.cheapies.nz/comment/edit/*
// @include		https://www.cheapies.nz/privatemsg/*
// @icon		https://www.ozbargain.com.au/favicon.ico
// @grant		GM_addStyle
// ==/UserScript==

//Add button styles
GM_addStyle(`
	.mdBtn {
		display: inline-block;
		cursor: pointer;
		margin: 0px;
		font-size: var(--page-font);
		line-height: 1;
		font-weight: bold;
		padding: 4px 6px;
		background: var(--input-bg);
		border: 1px solid #999;
		border-radius: 2px;
		white-space: pre;
		box-shadow: 0px 1px 0px #FFF inset, 0px -1px 2px #BBB inset;
		color: var(--page-fg);
		margin-bottom: 3px;
		user-select: none;
	}
	.mdBtn:hover {
		background: var(--shade3-bg) !important;
		color: #fff !important;
	}
	.qtBtn {
		font-weight: bold;
		position: fixed;
		display: none;
		line-height: 100%;
		padding: 3px 5px;
		border: var(--shade3-bg) solid 2px;
		opacity: 85%;
		user-select: none;
	}
	.qtBtn:hover {
		opacity: 100%;
	}
`);

//Add toolbar to preloaded textboxes
addFeatures(document.querySelector('textarea').parentNode);

//Add more features to nodes
if (location.href.indexOf('/node/') > 0) {
	//Observe and add toolbar to expanded reply boxes
	let targetNode = document.querySelectorAll(".comment.level0"); //Need All to capture pinned comments
	let config = {attributes: false, childList: true, subtree: true};
	let callback = (mutationList, observer) => {
		for (const mutation of mutationList) {
			if (mutation.addedNodes.length > 0 && mutation.addedNodes[0].className === "comment" && mutation.addedNodes[0].nodeName === "FORM") {
				addFeatures(mutation.addedNodes[0].querySelector('textarea').parentNode);
			}
		}
	};
	let observer = new MutationObserver(callback);
	for (let i = 0; i < targetNode.length; i++) {
		  observer.observe(targetNode[i], config);
	}
	//Add quote button to node content
	let textarea = document.querySelector('textarea');
	let node = document.querySelector('.node:not(.messages)');
	if (node && textarea) {
		let a = document.createElement('a');
		a.className = 'btn qtBtn';
		a.innerHTML = 'Quote Selection';
		a.addEventListener('click',
						   function(e){edPrefixTag('>', true, edInit(e.target,'>'));});
		a.addEventListener('mousedown',function(e){event.preventDefault()});
		node.textAreaNode = textarea;
		node.appendChild(a);
		['mouseup', 'touchend'].forEach(function(e) {
			node.addEventListener(e, (event) => {
				let selectedText = getSelectionText().trim();
				if (selectedText.length > 0) {
					const range = window.getSelection().getRangeAt(0);
					const rect = range.getBoundingClientRect();
					//Position the button near the selection
					a.style.top = `${event.clientY + 20}px`;
					a.style.left = `${event.clientX - 65}px`;
					a.style.display = 'inline-block';
				} else {
					a.style.display = 'none';
				}
			});
		});
		document.addEventListener("selectionchange", () => {
			//Remove button if unselected
			if (getSelectionText().length === 0) {
				a.style.display = 'none';
			}
		});
	}
}

function addFeatures(n) {
	n.textAreaNode = n.querySelector('textarea');
	//Add buttons
	btnMake(n, '<b>B</b>', 'Bold', '**');
	btnMake(n, '<i>I </i>', 'Italic', '*');
	btnMake(n, '<s>S</s>', 'Strikethrough', '~~');
	btnMake(n, 'H', 'Add Heading level on selected text','#', '', false, true, true);
	btnMake(n, '---', 'Horizontal line', '\n\n---\n\n', '', true);
	btnMake(n, '• List', 'Unordered list on selected text',
			function(e) {
		try {edList('* ', edInit(e.target,'* '));}
		catch(e) {}
	});
	btnMake(n, '# List', 'Ordered list on selected text',
			function(e) {
		try {edList('1', edInit(e.target,'1'), true);}
		catch(e) {}
	});
	btnMake(n, 'URL', 'Add URL to selected text',
			function(e) {
		try {edWrapInTag('[', ']('+prompt('URL:')+')', edInit(e.target,'['));}
		catch(e) {}
	});
	btnMake(n, 'Quote', 'Quote selected text','>', '', false, true, true);
	btnMake(n, 'Table', 'Insert table template',
			function(e) {
		try {
			const columns = parseInt(prompt('Enter number of columns:'), 10);
			const rows = parseInt(prompt('Enter number of rows:'), 10);

			if (isNaN(columns) || isNaN(rows) || columns <= 0 || rows <= 0) {
				alert('Please enter valid positive numbers');
				return;
			}
			edInsertText(createMarkdownTable(columns, rows), edInit(e.target,'|'));
		}
		catch(e) {}
	});
	btnMake(n, 'Code', 'Apply CODE markdown to selected text',
			function(e){
		let ed = edInit(e.target,'`');
		if (ed.sel.indexOf('\n') < 0) {
			edWrapInTag('`', '`', ed);
		}
		else {
			edWrapInTag(((ed.sel1==0) || (ed.text.charAt(ed.sel1-1) == '\n') ? '' : '\n') + '~~~' + (ed.sel.charAt(0) == '\n' ? '' : '\n'),
						(ed.sel.substr(-1) == '\n' ? '' : '\n') + '~~~' + (ed.text.substr(ed.sel2,1) == '\n' ? '' : '\n'),
						ed);
		}
	});
}

function btnMake(afterNode, label, title, tag1, tag2, noWrap, prefix, gap) {
	let a = document.createElement('a');
	a.className = 'mdBtn';
	a.innerHTML = label;
	a.title = title;
	a.addEventListener('click',
					   typeof(tag1) === 'function'
					   ? tag1
					   : noWrap ? function(e){ edInsertText(tag1, edInit(e.target,tag1)); }
					   : prefix ? function(e){ edPrefixTag(tag1, gap, edInit(e.target,tag1)); }
					   : function(e){ edWrapInTag(tag1, tag2, edInit(e.target,tag1)); });
	if (prefix) { a.addEventListener('mousedown',function(e){ event.preventDefault() }) }
	if (label === 'Quote') {
		document.addEventListener("selectionchange", () => {
			//Highlight quote button if text selected outside of textbox
			if (getSelectionText().length > 0 && getSelection().anchorNode.className !== 'form-item') {
				a.style.background = 'var(--shade3-bg)';
			} else {
				a.style.background = 'var(--input-bg)';
			}
		});
	}
	a.textAreaNode = afterNode.textAreaNode;
	afterNode.insertBefore(a, afterNode.textAreaNode);
}

function edInit(btn, tag) {
	let ed = { node: btn.parentNode.textAreaNode };
	ed.sel1 = ed.node.selectionStart;
	ed.sel2 = ed.node.selectionEnd;
	//Trim Spaces from start of selection, bad for some markdown
	for (let i = 1;ed.sel1 !== ed.sel2 && ed.node.value.substring(ed.sel1, ed.sel1 + 1) === ' '; i++) {
		  ed.sel1 = ed.node.selectionStart + i
	}
	//Trim Spaces from end of selection, bad for some markdown
	for (let i = 1;ed.sel1 !== ed.sel2 && ed.node.value.substring(ed.sel2 - 1, ed.sel2) === ' '; i++) {
		  ed.sel2 = ed.node.selectionEnd - i
	}
	ed.text = ed.node.value;
	ed.sel = ed.text.substring(ed.sel1, ed.sel2);
	//Check if there is a valid gap before and after selection, gaps required for some Markdown
	if (ed.sel1 === 0 || ed.sel1 - tag.length === 0 || ['\n',tag].includes(ed.text.substring(ed.sel1 - tag.length - 1, ed.sel1 - tag.length))) {ed.gapBefore = true}
	if (ed.text.substring(ed.sel2 + 1, ed.sel2 + 2) === '\n' || (!tag && ed.sel2 === ed.text.length)) {ed.gapAfter = true}
	return ed;
}

function edWrapInTag(tag1, tag2, ed) {
	if (ed.sel.startsWith(tag1) && ed.sel.endsWith(tag2?tag2:tag1)) {
		// Remove the syntax if it's already wrapped
		ed.node.value = ed.text.substr(0, ed.sel1) + ed.sel.slice(tag1.length, (tag2?-tag2.length:-tag1.length)) + ed.text.substr(ed.sel2);
		ed.node.setSelectionRange(ed.sel1, ed.sel1 + ed.sel.length - tag1.length - (tag2?tag2.length:tag1.length));
	} else {
		// Wrap syntax
		ed.node.value = ed.text.substr(0, ed.sel1) + tag1 + ed.sel + (tag2?tag2:tag1) + ed.text.substr(ed.sel2);
		ed.node.setSelectionRange(ed.sel1 + tag1.length, ed.sel1 + tag1.length + ed.sel.length);
	}
	ed.node.focus();
}

function edInsertText(text, ed) {
	//Insert at cursor
	ed.node.value = ed.text.substr(0, ed.sel2) + text + ed.text.substr(ed.sel2);
	ed.node.setSelectionRange(ed.sel2 + text.length, ed.sel2 + text.length);
	ed.node.focus();
}

function edPrefixTag(tag, gap, ed) {
	if (ed.sel.startsWith(tag)) {
		//Remove the syntax if it's already prefixed
		let selection = removeCharAtStartOfLines(ed.sel, tag);
		ed.node.value = ed.text.substr(0, ed.sel1) + selection + ed.text.substr(ed.sel2);
		ed.node.setSelectionRange(ed.sel1, ed.sel1 + selection.length);
	} else {
		//Prefix syntax - Note Firefox and Chrome handle getSelection() differently for textboxes
		if (getSelectionText().length > 0 && getSelection().anchorNode.className !== 'form-item' && tag === '>') {//Text selected on page for blockquote
			let selection = trimWhitespaceFromLines(getSelectionText()).replace(/^(?=\S)/gm, tag).trim();
			if (!ed.gapBefore && gap) {selection = '\n' + selection}; //Gap required before tag
			if (!ed.gapAfter && gap) {selection = selection + '\n\n'}; //Gap required after tag
			ed.node.value = ed.text.substr(0, ed.sel1) + ed.sel + selection + ed.text.substr(ed.sel2);
			ed.node.setSelectionRange(ed.sel1 + selection.length, ed.sel1 + selection.length);
		} else {//Text selected in textarea
			let selection;
			if (ed.sel.length > 0) {selection = trimWhitespaceFromLines(ed.sel).replace(/^(?=\S)/gm, tag)
								   }
			else {selection = tag};
			if (!ed.gapBefore && gap && selection !== tag && selection.length > 0) {selection = '\n' + selection} //Gap required before tag
			if (!ed.gapAfter && gap && selection !== tag && selection.length > 0) {selection = selection + '\n'} //Gap required after tag
			ed.node.value = ed.text.substr(0, ed.sel1) + selection + ed.text.substr(ed.sel2);
			if (selection === tag){ //Set resulting selection
				ed.node.setSelectionRange(ed.sel2 + tag.length, ed.sel2 + tag.length);
			} else {
				ed.node.setSelectionRange(ed.sel1 + tag.length, ed.sel1 + selection.length - (ed.gapAfter||!gap?0:1)); //It just works
			}
		}
	}
	ed.node.focus();
}

function edList(tag, ed, ordered) {
	//Add what to do when no selection
	if (ed.sel.startsWith(tag)) {
		// Remove the syntax if it's already prefixed
		let selection = trimWhitespaceFromLines(removeCharAtStartOfLines(ed.sel, tag, ordered));
		ed.node.value = ed.text.substr(0, ed.sel1) + selection + ed.text.substr(ed.sel2);
		ed.node.setSelectionRange(ed.sel1, ed.sel1 + selection.length);
	} else {
		// Wrap syntax
		var selection = insertAtStartOfLines(trimWhitespaceFromLines(ed.sel), {charToInsert: tag, numberLines: ordered, skipEmptyLines: true});
		if (!ed.gapBefore) {selection = '\n' + selection}; //Gap required before lists
		if (!ed.gapAfter) {selection = selection + '\n'}; //Gap required after lists
		ed.node.value = ed.text.substr(0, ed.sel1) + selection + ed.text.substr(ed.sel2);
		ed.node.setSelectionRange(ed.gapBefore?ed.sel1:ed.sel1+1, ed.sel1 + selection.length - (ed.gapAfter?0:1));
	}
	ed.node.focus();
}

function getSelectionText() {
	if (window.getSelection) {
		return window.getSelection().toString();
	}
}

function createMarkdownTable(columns, rows) {
	const cellWidth = 9; //Define uniform cell width for better alignment
	//Helper to pad text to fixed width
	function pad(text) {
		return text.padEnd(cellWidth, ' ');
	}
	//Generate rows
	const headers = Array.from({ length: columns }, (_, i) => `Head${i + 1}`);
	const headerRow = `| ${headers.join(' | ')} |`;
	const separatorRow = `| ${headers.map(() => '-'.repeat(cellWidth)).join(' | ')} |`;
	const dataRows = [];
	for (let r = 0; r < rows; r++) {
		const row = Array.from({ length: columns }, () => pad('  Cell'));
		dataRows.push(`| ${row.join(' | ')} |`);
	}
	//Combine everything
	return [headerRow, separatorRow, ...dataRows].join('\n');
}

function insertAtStartOfLines(inputString, options = {}) {
	const { charToInsert = '', numberLines = false, skipEmptyLines = true } = options;
	let lineNumber = 1;
	return inputString
		.split('\n')
		.map(line => {
		if (skipEmptyLines && line.trim() === '') {
			return line;
		}
		if (numberLines) {
			return `${lineNumber++}. ${line}`;
		} else {
			return charToInsert + line;
		}
	})
		.join('\n');
}

function removeCharAtStartOfLines(inputString, charToRemove, removeNumbers) {
	return inputString
		.split('\n')
		.map(line => {
		let newLine = line;
		//Remove the specific character if needed
		if (charToRemove && newLine.startsWith(charToRemove) && !removeNumbers) {
			newLine = newLine.slice(1);
		}
		//Remove leading numbers (and optional dot/space after) if needed
		if (removeNumbers) {
			newLine = newLine.replace(/^\d+\.?\s*/, '');
		}
		return newLine;
	})
		.join('\n');
}

function trimWhitespaceFromLines(inputString) {
	return inputString
		.split('\n')
		.map(line => line.trim())
		.join('\n');
}

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址