Skip to content

Instantly share code, notes, and snippets.

@q00u
Last active November 6, 2023 04:07
Show Gist options
  • Save q00u/91bb69eef74e140005bd776608af8c5f to your computer and use it in GitHub Desktop.
Save q00u/91bb69eef74e140005bd776608af8c5f to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Textidtor
// @namespace https://gist.github.com/q00u
// @downloadURL https://gist.github.com/q00u/91bb69eef74e140005bd776608af8c5f/raw/Textidtor.user.js
// @updateURL https://gist.github.com/q00u/91bb69eef74e140005bd776608af8c5f/raw/Textidtor.user.js
// @supportURL https://gist.github.com/q00u/91bb69eef74e140005bd776608af8c5f
// @version 0.11
// @description Keyboard shortcuts for editing/correcting AI transcriptions
// @author Phoenix G
// @match http://otranscribe.com/classic/
// @match https://otranscribe.com/
// @icon https://www.google.com/s2/favicons?sz=64&domain=otranscribe.com
// @grant none
// ==/UserScript==
// alt-" Turn into quote (TODO: Same, but with ' )
// alt-, Replace punctuation with comma and join two sentences (lowercase)
// alt-; Replace punctuation with colon and join sentences (lowercase)
// alt-. Replace punctuation with period and split two sentences (uppercase)
// alt-/ Replace punctuation with question mark and split two sentences (uppercase)
// alt-m Remove punctuation
// alt-_ _Italicise_ (markdown style)
// alt-= **Bold** (markdown style)
// alt-1 Lowercase
// alt-2 "Sentence case"
// alt-3 _Title Text_
// alt-4 UPPERCASE
// alt-w Sentence case alternative (no quotes)
// alt-t Title Text alternative (no italics)
// alt-q > Markdown quote
// alt-7 Insert [Macro1]
// alt-8 Insert [Macro2]
// alt-9 Insert [Macro3]
// alt-b Insert [Macro4] (for breaks; alt-0 doesn't seem to work)
// alt-g Insert [Macro5] (for guests)
// TODO: alt-. Replace SPACE with period and split two sentences (if only one space, and no punctuation)
const MACROS = ['[Thomas]\n', '[Emily]\n', '[Shep]\n', '\n\n[**Break**]\n\n[Thomas]\n', '[]\n'];
/* eslint-disable no-console */
// eslint-disable-next-line wrap-iife, func-names
(function () {
// eslint-disable-next-line strict, lines-around-directive
'use strict';
function fixSelection() {
if (window.getSelection) {
const selection = window.getSelection();
//console.log('selected text (via getSelection):', selection);
if (selection.anchorNode !== selection.extentNode) {
// Which direction did we select? latter = start is after end
// eslint-disable-next-line no-bitwise
const latter = !(selection.anchorNode.compareDocumentPosition(selection.extentNode) & Node.DOCUMENT_POSITION_FOLLOWING);
// console.log('latter:', latter);
const topNode = latter ? selection.extentNode : selection.anchorNode;
const topOffset = latter ? selection.extentOffset : selection.anchorOffset;
const bottomNode = latter ? selection.anchorNode : selection.extentNode;
const bottomOffset = latter ? selection.anchorOffset : selection.extentOffset;
//console.log('topNode:', topNode, 'topOffset:', topOffset, 'bottomNode:', bottomNode, 'bottomOffset:', bottomOffset);
if (bottomOffset === 0) {
//console.log('Nothing in end node selected');
let cur = bottomNode.previousSibling;
while (cur && cur !== topNode && cur.textContent.trim().length === 0) {
//console.log('previous cur:', cur);
cur = cur.previousSibling;
}
//console.log('cur:', cur);
if (cur) {
const newRange = new Range();
newRange.selectNodeContents(cur);
newRange.setStart(topNode, topOffset);
// Move selection to trimmed range
selection.removeAllRanges();
selection.addRange(newRange);
}
}
}
}
}
function getSelectionText() {
let text = '';
if (window.getSelection) {
fixSelection();
const selection = window.getSelection();
// console.log('selected text (via getSelection):', selection);
// // Which direction did we select? latter = start is after end
// const latter = !(selection.anchorNode.compareDocumentPosition(selection.extentNode) & Node.DOCUMENT_POSITION_FOLLOWING);
// // console.log('latter:', latter);
// //? selection.anchorOffset : selection.extentOffset;
// if ((latter ? selection.anchorOffset : selection.extentOffset) === 0) {
// console.log('Nothing in end node selected');
// let cur = (latter ? selection.anchorNode : selection.extentNode).previousSibling;
// while (cur && cur !== (latter ? selection.extendNode : selection.anchorNode) && cur.textContent.trim().length === 0) {
// console.log('previous cur:', cur);
// cur = cur.previousSibling;
// }
// console.log('cur:', cur);
// const newRange = new Range();
// newRange.selectNodeContents(cur);
// newRange.setStart(selection.baseNode, selection.baseOffset);
// // Move selection to trimmed range
// selection.removeAllRanges();
// selection.addRange(newRange);
// }
text = selection.toString();
} else if (document.selection && document.selection.type !== 'Control') {
const range = document.selection.createRange();
// console.log('selected text (via createRange):', range);
text = range.text;
}
// console.log('text:', text);
return text;
}
function pasteHtmlAtCaret(html, selectPastedContent) {
// console.log('pasting: `'+html+'`');
let sel;
let range;
if (window.getSelection) {
// IE9 and non-IE
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
// console.log('range (before paste):', range);
if (document.queryCommandSupported('insertText')) { // Supports browser undo
document.execCommand('insertText', false, html);
if (selectPastedContent) {
// Try setting new range
const newPos = window.getSelection().getRangeAt(0);
// console.log('newPos:', newPos);
const offset = (newPos.endOffset > html.length)
? newPos.endOffset - html.length
: 0;
range.setStart(newPos.endContainer, offset);
range.setEnd(newPos.endContainer, newPos.endOffset);
sel.removeAllRanges();
sel.addRange(range);
}
} else {
range.deleteContents();
// Range.createContextualFragment() would be useful here but is
// only relatively recently standardized and is not supported in
// some browsers (IE9, for one)
const el = document.createElement('div');
el.innerHTML = html;
const frag = document.createDocumentFragment();
let node;
let lastNode;
// eslint-disable-next-line no-cond-assign
while ((node = el.firstChild)) {
lastNode = frag.appendChild(node);
}
const firstNode = frag.firstChild;
range.insertNode(frag);
// Preserve the selection
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
if (selectPastedContent) {
range.setStartBefore(firstNode);
} else {
range.collapse(true);
}
sel.removeAllRanges();
sel.addRange(range);
}
}
}
// eslint-disable-next-line no-cond-assign
} else if ((sel = document.selection) && sel.type !== 'Control') {
// IE < 9
const originalRange = sel.createRange();
originalRange.collapse(true);
sel.createRange().pasteHTML(html);
if (selectPastedContent) {
range = sel.createRange();
range.setEndPoint('StartToStart', originalRange);
range.select();
}
}
}
function SelectionToMarkdownQuote(str) {
function getNodesInRange(range) {
function getNextNode(node) {
if (node.firstChild) { return node.firstChild; }
while (node) {
if (node.nextSibling) { return node.nextSibling; }
node = node.parentNode;
}
return null; // no siblings or parent
}
const start = range.startContainer;
const end = range.endContainer;
const commonAncestor = range.commonAncestorContainer;
const nodes = [];
let node;
// walk parent nodes from start to common ancestor
for (node = start.parentNode; node; node = node.parentNode) {
nodes.push(node);
if (node === commonAncestor) { break; }
}
nodes.reverse();
// walk children and siblings from start until end is found
for (node = start; node; node = getNextNode(node)) {
nodes.push(node);
if (node === end) { break; }
}
return nodes;
}
if (window.getSelection) {
fixSelection();
const selection = window.getSelection();
// For all ranges in selection
for (let i = 0; i < selection.rangeCount; i += 1) {
//console.log('Processing range', i, 'of', selection.rangeCount);
const range = selection.getRangeAt(i);
//console.log('range:', range);
const selectedNodes = getNodesInRange(range);
//console.log('selectedNodes:', selectedNodes);
// For all text nodes in selectedNodes
for (let j = 0; j < selectedNodes.length; j += 1) {
const node = selectedNodes[j];
if (node.nodeType === Node.TEXT_NODE) {
//console.log('Processing node', j, 'of', selectedNodes.length);
node.textContent = `> ${node.textContent}`;
}
}
}
}
}
const UNIMPORTANT_WORDS = [
'a',
'an',
'and',
'as',
'at',
'but',
'by',
'en',
'for',
'from',
'how',
'if',
'in',
'neither',
'nor',
'of',
'on',
'only',
'onto',
'out',
'or',
'per',
'so',
'than',
'that',
'the',
'to',
'until',
'up',
'upon',
'v',
'v.',
'versus',
'vs',
'vs.',
'via',
'when',
'with',
'without',
'yet',
];
const IMPORTANT_WORDS = [
'i',
'i\'m',
'i\'ve',
'i\'ll',
'i\'d',
'mr.',
'mrs.',
'ms.',
'dr.',
];
// JUST capitalize first letter of passed string
function capitalize(str) {
const res = str.charAt(0).toUpperCase() + str.slice(1);
// console.log('capitalize:', str, '->', res);
return res;
}
function toTitleWord(lstr) {
if (UNIMPORTANT_WORDS.includes(lstr)) {
// console.log('Unimportant:', lstr);
return lstr;
}
// console.log('toTitleWord', lstr, '->', capitalize(lstr));
return capitalize(lstr);
}
function toTitleString(str) {
const sstr = str.toLowerCase().split(' ');
// console.log('sstr:', sstr);
const res = capitalize(sstr[0]) + ((sstr.length > 1) ? ` ${sstr.slice(1).map(toTitleWord).join(' ')}` : '');
// console.log('toTitleString:', str, '->', res);
return res;
}
function toSentenceWord(lstr) {
if (IMPORTANT_WORDS.includes(lstr)) {
// console.log('Important:', lstr);
return capitalize(lstr);
}
// console.log('toSentenceWord', lstr, '->', lstr);
return lstr;
}
const PUNCTUATION = [
'. ', '! ', '? ',
];
function toSentenceString(str) {
// First split on punctuation, then capitalize each sentence
for (let i = 0; i < PUNCTUATION.length; i++) {
const punc = PUNCTUATION[i];
if (str.includes(punc)) {
const sstr = str.split(punc);
// console.log('toSentenceString, splitting on', punc, 'sstr:', sstr);
const res = sstr.map(toSentenceString).join(punc);
// console.log('toSentenceString:', str, '->', res);
return res;
}
}
const sstr = str.toLowerCase().split(' ');
// console.log('sstr:', sstr);
const res = capitalize(sstr[0]) + ((sstr.length > 1) ? ` ${sstr.slice(1).map(toSentenceWord).join(' ')}` : '');
// console.log('toSentenceString:', str, '->', res);
return res;
}
function toLowercaseString(str) {
const words = str.toLowerCase().split(' ');
return words.map(toSentenceWord).join(' ');
}
// document.getElementById("paste").onclick = function() {
// document.getElementById('test').focus();
// var selectPastedContent = document.getElementById('selectPasted').checked;
// pasteHtmlAtCaret('<b>INSERTED</b>', selectPastedContent);
// return false;
// };
// function changePunctuation(str, punc) {
// const firstChar = str.length > 1 ? str.slice(0,1) : '';
// const newText = text.replace...
// }
function onKeydown(evt) {
// use https://keycode.info/ to get keys
if (evt.altKey) {
const text = getSelectionText();
// TODO: Convert "&nbsp;" to space before processing
switch (evt.keyCode) {
case 222: // alt-" -> turn into quote
// TODO: Punctuation at end! If none, add ,
// TODO: Space at end? Move selection back out of whitespace
pasteHtmlAtCaret(`"${text}"`, true); break;
case 189: // alt-_ -> italicise (markdown)
pasteHtmlAtCaret(`_${text}_`, true); break;
case 187: // alt-+ -> bold (markdown)
pasteHtmlAtCaret(`**${text}**`, true); break;
case 188: // alt-, -> join two sentences (lower case, replace punctuation with comma)
{
// TODO: BUG: (For this and alt-.) If the first character is the target punctuation, it doesn't get replaced
const firstchar = text.length > 1 ? text.slice(0,1) : '';
const newText = text.length > 1
? toSentenceString(text.replace(/[-.?!]/g, ',')).slice(1)
: text.replace(/[-.?!]/g, ',');
pasteHtmlAtCaret(`${firstchar}${newText}`, true);
} break;
case 190: // alt-. -> replace puncuation with period, capitalize next word
{
// Count punctuation
const punc = text.match(/[-,.?!]/);
//console.log('punc:', punc);
if (punc) {
// Punctuation exists
const firstchar = text.length > 1 ? text.slice(0,1) : '';
const newText = text.length > 1
? toSentenceString(text.replace(/[-,?!]/g, '.')).slice(1)
: text.replace(/[-,?!]/g, '.');
pasteHtmlAtCaret(`${firstchar}${newText}`, true);
} else {
// No punctuation, replace first space
const words = text.split(' ');
const first = `${words[0]}.`;
const rest = toSentenceString(words.slice(1).join(' '));
pasteHtmlAtCaret(`${first} ${rest}`, true);
}
} break;
case 191: // alt-/ -> replace punctuation with question mark, capitalize next word
{
const firstchar = text.length > 1 ? text.slice(0,1) : '';
const newText = text.length > 1
? toSentenceString(text.replace(/[-,.!]/g, '?')).slice(1)
: text.replace(/[-,.!]/g, '?');
pasteHtmlAtCaret(`${firstchar}${newText}`, true);
} break;
case 186: // alt-; -> replace punctuation with :, lower case next word
{
const firstchar = text.length > 1 ? text.slice(0,1) : '';
const newText = text.length > 1
? toSentenceString(text.replace(/[-.?!]/g, ':')).slice(1)
: text.replace(/-.?!/g, ':');
pasteHtmlAtCaret(`${firstchar}${newText}`, true);
} break;
case 77: // alt-m -> remove punctuation
{
// TODO: BUG: If an IMPORTANT_WORD is in the mix, it gets lowercased anyway (fixed?)
// const newText = text.replace(/,/g, '');
const newText = text.replace(/[-,?!.]/g, '');
pasteHtmlAtCaret(toLowercaseString(newText), true);
} break;
case 49: // alt-1 -> lowercase
evt.preventDefault();
// case 76: // alt-l -> lowercase
pasteHtmlAtCaret(text.toLowerCase(), true); break;
case 50: // alt-2 -> "Sentence case"
evt.preventDefault();
{
const newText = toSentenceString(text);
pasteHtmlAtCaret(`"${newText}"`, true);
} break;
case 87: // alt-w -> Sentence case (alt-s not available)
{
// console.log('Trying to sentence');
const newText = toSentenceString(text);
pasteHtmlAtCaret(newText, true);
} break;
case 51: // alt-3 -> _Title Text_
evt.preventDefault();
{
const newText = toTitleString(text);
pasteHtmlAtCaret(`_${newText}_`, true);
} break;
case 84: // alt-t -> Title Text
{
const newText = toTitleString(text);
pasteHtmlAtCaret(newText, true);
} break;
case 52: // alt-4 -> UPPERCASE
evt.preventDefault();
// case 85: // alt-u -> UPPERCASE
pasteHtmlAtCaret(text.toUpperCase(), true); break;
case 81: // alt-q -> markdown quotes
SelectionToMarkdownQuote();
break;
// {
// const newText = toMarkdownQuote(text);
// pasteHtmlAtCaret(newText, true);
// } break;
case 55: // alt-7 -> [Macro1]\n
evt.preventDefault();
pasteHtmlAtCaret(`${MACROS[0]}`, true); break;
case 56: // alt-8 -> [Macro2]\n
evt.preventDefault();
pasteHtmlAtCaret(`${MACROS[1]}`, true); break;
case 57: // alt-9 -> [Macro3]\n
evt.preventDefault();
pasteHtmlAtCaret(`${MACROS[2]}`, true); break;
case 66: // alt-b -> [Macro4]\n
evt.preventDefault();
pasteHtmlAtCaret(`${MACROS[3]}`, true); break;
case 71: // alt-g -> [Macro5]\n
evt.preventDefault();
pasteHtmlAtCaret(`${MACROS[4]}`, true); break;
default:
}
}
}
document.addEventListener('keydown', onKeydown, true);
})();
@q00u
Copy link
Author

q00u commented Jan 15, 2023

CHANGELOG:

0.1 Initial code

0.2 Linting, add keyboard shortcuts comment near top, add sentence case processing for multiple sentences

0.3 Add downloadURL

0.4 Add updateURL and supportURL

0.5 Fix leading-space bug in new sentence case processing for multiple sentences

0.6 Trim selection of whitespace on alt keypress, before any action

  • Fixes bug where alt-2 would put the end " on a blank line below

0.7 Add 'alt-q' to convert selected text to > Markdown quote

  • Warning, not currently undoable (WIP)

0.8 Fix bug in fixSelection

0.9 Add Macros, modify some commands

  • alt-. now replaces punctuation with period and splits sentences (eg, if it were a comma). If no punctuation, replace first space
  • alt-, now replaces punctuation with comma and joins sentences
  • alt-m (new) removes punctuation (similar to previous alt-,
  • alt-/ (new) replaces punctuation with question mark and splits sentences
  • alt-b (new) Inserts BREAK macro
  • alt-7 thru alt-9 (new) Host names macros

0.10 Bug fixes

  • Fix bug in range selection
  • Add additional important strings (titles)
  • Add separate toLowercaseString function

0.11 Additional shortcuts

  • Add shortcut alt-g -> Insert [Macro5] (for guests)
  • Add shortcut alt-; -> Change punctuation to colon

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment