Last active
November 6, 2023 04:07
-
-
Save q00u/91bb69eef74e140005bd776608af8c5f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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 " " 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); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
andsupportURL
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
0.7 Add 'alt-q' to convert selected text to > Markdown quote
0.8 Fix bug in
fixSelection
0.9 Add Macros, modify some commands
0.10 Bug fixes
toLowercaseString
function0.11 Additional shortcuts