Aloso / Rust wiki editor shortcuts

// ==UserScript==
// @namespace     mediawiki
// @name          Rust wiki editor shortcuts
// @description   Configurable keyboard shortcuts for the Rust wiki editor
// @version       1.2.2
// @copyright     2020, Aloso (https://openuserjs.org/users/Aloso)
// @license       MIT
// @include       https://runrust.miraheze.org/w/index.php?*
// @grant         none
// @updateURL     https://openuserjs.org/meta/Aloso/Rust_wiki_editor_shortcuts.meta.js
// @downloadURL   https://openuserjs.org/install/Aloso/Rust_wiki_editor_shortcuts.user.js
// ==/UserScript==

// ==OpenUserJS==
// @author Aloso
// ==/OpenUserJS==



// Shortcuts. Each shortcut is a key combination with
// meta, ctrl, alt, shift and another key, e.g.
//
// meta+ctrl+alt+shift+H
//
// The keys must be written exactly in this order!
// Note that these shortcuts override the default browser behavior.
//
// Don't modify the shortcuts in this file if possible.
// Instead, open the browser console (F12) and follow the instructions!
//
// If I release a new version of this script and you update it,
// modifications of this file will be lost. If you use the exported
// functions in the browser console, they're persisted in the local storage.

let shortcuts = {
  'alt+c':         ['wrap', { before: '<code>', after: '</code>' }],
  'alt+r':         ['wrap', { before: '{{rs|', after: '}}' }],
  'alt+h':         ['wrap', { before: '<syntaxhighlight lang="rust">\n', after: '\n</syntaxhighlight>' }],
  'ctrl+i':        ['wrap', { before: "''", after: "''" }],
  'ctrl+b':        ['wrap', { before: "'''", after: "'''" }],
  'alt+t':         ['wrap', { before: "{{trait|", after: "}}" }],
  'alt+s':         ['wrap', { before: "{{struct|", after: "}}" }],
  'alt+e':         ['wrap', { before: "{{enum|", after: "}}" }],
  'alt+m':         ['wrap', { before: "{{macro|", after: "}}" }],

  'alt+y':         ['wrap', { before: "{{yes}}", after: "" }],
  'alt+n':         ['wrap', { before: "{{no}}", after: "" }],
  'alt+u':         ['wrap', { before: "{{unknown}}", after: "" }],

  // the above shortcuts can be reversed with the shift key:
  'alt+shift+C':   ['unwrap', { before: '<code>', after: '</code>' }],
  'alt+shift+R':   ['unwrap', { before: '{{rs|', after: '}}' }],
  'alt+shift+S':   ['unwrap', { before: '<syntaxhighlight lang="rust">\n', after: '\n</syntaxhighlight>' }],
  'ctrl+shift+I':  ['unwrap', { before: "''", after: "''" }],
  'ctrl+shift+B':  ['unwrap', { before: "'''", after: "'''" }],
  'alt+shift+T':   ['unwrap', { before: "{{trait|", after: "}}" }],
  'alt+shift+S':   ['unwrap', { before: "{{struct|", after: "}}" }],
  'alt+shift+E':   ['unwrap', { before: "{{enum|", after: "}}" }],
  'alt+shift+M':   ['unwrap', { before: "{{macro|", after: "}}" }],

  'alt+shift+Y':   ['unwrap', { before: "{{yes}}", after: "" }],
  'alt+shift+N':   ['unwrap', { before: "{{no}}", after: "" }],
  'alt+shift+U':   ['unwrap', { before: "{{unknown}}", after: "" }],

  // add remove an external link:
  'alt+l':         ['add_external_link', { default_url: '' }],
  'alt+shift+L':   ['remove_external_link', {}],
}



// DO NOT modify this:

window.addEventListener('load', function() {
  'use strict'

  const ta = document.getElementById('wpTextbox1')
  if (ta == null) return

  const actions = {
    wrap: function(options) {
      const val = ta.value
      const start = ta.selectionStart
      const end = ta.selectionEnd

      const newVal = val.slice(0, start) + options.before + val.slice(start, end) + options.after + val.slice(end)
      const newStart = start + options.before.length
      const newEnd = end + options.before.length

      ta.value = newVal
      selectRange(newStart, newEnd)
    },

    unwrap: function(options) {
      const val = ta.value
      const start = ta.selectionStart
      const end = ta.selectionEnd

      let p0 = val.slice(0, start)
      const p1 = val.slice(start, end)
      let p2 = val.slice(end)
      if (p0.endsWith(options.before)) p0 = p0.slice(0, start - options.before.length)
      if (p2.startsWith(options.after)) p2 = p2.slice(options.after.length)

      const newVal = p0 + p1 + p2
      const newStart = p0.length
      const newEnd = p0.length + p1.length

      ta.value = newVal
      selectRange(newStart, newEnd)
    },

    add_external_link: function(options) {
      const val = ta.value
      const start = ta.selectionStart
      const end = ta.selectionEnd

      const url = prompt("External URL:", options.default_url)
      if (url != null) {
      	actions.wrap({ before: '[' + url + ' ', after: ']' })
      }
    },

    remove_external_link: function(options) {
      const val = ta.value
      const start = ta.selectionStart
      const end = ta.selectionEnd

      const before = val.slice(0, start).match(/\[[^\[\]\s]*?\s?$/)[0]
      const after = val.slice(end)[0]
      if (before != null && after === ']') {
      	actions.unwrap({ before: before, after: after })
      }
    },
  }


  function selectRange(start, end) {
    ta.focus()
    if (ta.setSelectionRange) {
      ta.setSelectionRange(start, end);
    } else if (ta.createTextRange) {
      const r = ta.createTextRange();
      r.collapse(true);
      r.moveEnd('character', end);
      r.moveStart('character', start);
      r.select();
    }
  }


  const unsafeWin = typeof unsafeWindow === "undefined" ? window : unsafeWindow
  if (typeof exportFunction === "undefined") {
    window.exportFunction = function(f) { return f }
  }

  unsafeWin.KEY_DEBUG = false

  if (unsafeWin.localStorage.gm_editor_shortcuts != null) {
    shortcuts = JSON.parse(unsafeWin.localStorage.gm_editor_shortcuts)
  } else {
    unsafeWin.localStorage.gm_editor_shortcuts = JSON.stringify(shortcuts)
  }

  function setWrapShortcut(shortcut, before, after) {
    const existed = shortcut in shortcuts

    let obj
    let add_shift_shortcut = false
    if (typeof before === 'string' && typeof after === 'string') {
      obj = ['wrap', { before: before, after: after }]
      if (!existed) add_shift_shortcut = true
    } else {
      obj = before
    }

    shortcuts[shortcut] = obj
    console.log('added %c' + shortcut + '%c (' + obj[0] + ')', 'color: #11aaff;', 'color: inherit;')

    if (add_shift_shortcut) {
      const keys = shortcut.split('+')
      if (!keys.includes('shift')) {
        const last = keys.pop()
        keys.push('shift', last)

        shortcuts[keys.join('+')] = ['unwrap', obj[1]]
    		console.log('added %c' + keys.join('+') + '%c (unwrap)', 'color: #11aaff;', 'color: inherit;')
      }
    }

    unsafeWin.localStorage.gm_editor_shortcuts = JSON.stringify(shortcuts)
  }

  function removeWrapShortcut() {
    for (const shortcut of arguments) {
      delete shortcuts[shortcut]
      console.log('deleted %c' + shortcut + '%c', 'color: #11aaff;', 'color: inherit;')
    }

    unsafeWin.localStorage.gm_editor_shortcuts = JSON.stringify(shortcuts)
  }

  function printAllShortcuts() {
    console.group('%cAvailable keyboard shortcuts:', 'font-family: Hack, Consolas, monospace; font-weight: bold; font-size: 115%;')
    for (const s of Object.keys(shortcuts)) {
      const spaces = '               '.slice(s.length) + ' '

      if (shortcuts[s][0] === 'wrap' || shortcuts[s][0] === 'unwrap') {
        let before = shortcuts[s][1].before
        let after = shortcuts[s][1].after

        const indent = ' '.repeat(s.length + spaces.length)
        before = before.replaceAll("\n", "\n" + indent)
        after = after.replaceAll("\n", "\n" + indent)

        const color = shortcuts[s][0] === 'wrap' ? '#00ff00' : 'red'
        console.log(s + spaces + '%c' + before + '%cā”Š%c' + after, 'color: ' + color, 'color: inherit', 'color: ' + color)
      } else {
        console.log(s + spaces + shortcuts[s][0])
      }
    }
    console.groupEnd()
    console.info('Note: Green means added text, red means removed text.')
    console.info('More information: https://openuserjs.org/scripts/Aloso/Rust_wiki_editor_shortcuts')
  }

  function printHelpInfo() {
    console.group('%cRust wiki editor shortcuts ā€“ā€“ Help', 'font-family: Hack, Consolas, monospace; font-weight: bold; font-size: 115%;')
    console.log('Enter this to display pressed key shortcuts:\n' +
                '  %cKEY_DEBUG = true', 'color: #11aaff; font-size: 120%')
    console.log("To print this help info:\n" +
                "  %chelp()%c", 'color: #11aaff; font-size: 120%', '')
    console.log('Enter this to list all available shortcuts:\n' +
                '  %cshowShort()', 'color: #11aaff; font-size: 120%')
    console.log("To add keyboard shortcuts:\n" +
                "  %csetShort('alt+m', ['unwrap', { before: '{{', after: '}}' }])%c\n" +
                "  > added %calt+m%c (unwrap)\n" +
                "  %csetShort('ctrl+alt+x', 'prefix', 'postfix')%c\n" +
                "  > added %cctrl+alt+x%c (wrap)\n" +
                "  > added %cctrl+alt+shift+x%c (unwrap)",
                'color: #11aaff; font-size: 120%', '', 'color: #11aaff;', '', 'color: #11aaff; font-size: 120%', '', 'color: #11aaff;', '', 'color: #11aaff;', '')
    console.log("To delete keyboard shortcuts:\n" +
                "  %cdelShort('ctrl+alt+x', 'alt+m')%c\n" +
                "  > removed %cctrl+alt+x%c (wrap)\n" +
                "  > removed %calt+m%c (unwrap)",
                'color: #11aaff; font-size: 120%', '', 'color: #11aaff;', '', 'color: #11aaff;', '')
    console.info('More information: https://openuserjs.org/scripts/Aloso/Rust_wiki_editor_shortcuts')
    console.groupEnd()
  }

  unsafeWin.setShort = exportFunction(setWrapShortcut, unsafeWin)
  unsafeWin.delShort = exportFunction(removeWrapShortcut, unsafeWin)
  unsafeWin.showShort = exportFunction(printAllShortcuts, unsafeWin)
  unsafeWin.help = exportFunction(printHelpInfo, unsafeWin)

  ta.addEventListener('keydown', function(e) {
    if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) {
      if (e.key === 'Meta' || e.key === 'Control' || e.key === 'Alt' || e.key === 'Shift') return

      const evt = []
      if (e.metaKey) evt.push('meta')
      if (e.ctrlKey) evt.push('ctrl')
      if (e.altKey) evt.push('alt')
      if (e.shiftKey) evt.push('shift')
      evt.push(e.key)

      const shortcut = shortcuts[evt.join('+')]
      if (unsafeWin.KEY_DEBUG) {
        console.log('pressed %c' + evt.join('+'), 'color: #11aaff;')
      }
      if (shortcut != null) {
        const fn = actions[shortcut[0]]
        fn(shortcut[1])

        e.preventDefault()
        e.stopPropagation()
        return false
      }
    }
  }, false)

  setTimeout(function() {
    console.clear()
  	printHelpInfo()
  }, 1600)

})