User:Golden/common.js
Jump to navigation
Jump to search
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
// "Here, you can see Golden's user configuration habits in action..."
// This particular script commits several henious crimes such as:
// - Assuming your browser supports ES6
// - Using the `let` keyword when defining most variables
// - Sometimes using `==` for equality checking
// - Using += on strings
// - A try...catch block
// - Promises
// - Backtick strings
// - Non-integer keys in Arrays
// - Definition of variables in control structures
// - Definition of variables through let [...] syntax
// - Actually using the `null` keyword
// - Modifying values in `window.location`
// - Having an annoying pop up window.
// - "keydown" and "keyup" event listeners
// - DOM manipulation
// - Hardcoded element attributes
// - Inconsistent code comment density
// - Inline function definitions
// - Unbracketed if statements
// - Implying object orientation, but instead being functional
// - Defining classes only to use them as functions
// - Placing class methods in Objects, and calling them
// - Making no class methods private
const visibleMatched = 64;
const input_bg = "#f0e8c0";
const input_fg = "#483018";
const window_bg = "#483018";
const window_fg = "#f0e8c0";
// Executes a command as typed in the VWN <input> element.
class VWN_Command {
constructor() {
// Maps functions to particular commands.
this.funcs = {
's': this.searchPage,
'#': this.goToSection
};
this.curFunc = null;
}
run(char, args)
{
// Get the function, then check if it exists (a valid command).
let func = this.funcs[char];
if (typeof func === "undefined")
return;
// If it's a valid command, call it with the args (and full string)
return new Promise(async (resolve, reject) => {
let ms = 5000;
let timeout = setTimeout(() => {
reject(`Command took too long (>${ms} ms)`);
}, ms);
let output = func.call(this, args, char + args);
clearTimeout();
resolve(output);
});
}
// assumes you slapped a 'g' flag on the regex
// if you don't it'll freeze the page
doActualSearching(element, regex)
{
// Get the inner text of the actual wiki body.
let text = element.innerText;
let matches;
if (regex.global) // Global regex?
{
matches = text.matchAll(regex);
if (typeof matches === "undefined")
matches = [];
}
else // Otherwise, polyfill in a matchAll with the behavior I want
{
let match; // current match
let cutoff_text = text; // a substring to make sure matches don't happen multiple times
let cutoff = 0; // the amount of text cut off.
matches = []; // Define matches actually
let iter = 0;
// Iterate matches. If the text matches,
while ((match = cutoff_text.match(regex)))
{
if (matches.length && match.index == matches[matches.length - 1].index)
{
iter += 1;
if (iter >= 100)
{
matches.tooMany = true;
break; // We're in a loop that goes for too long!
}
}
else
iter = 0;
let index = match.index; // collect the matched index for later,
match.index += cutoff; // give the match the true position,
match.input = text; // and the true text,
// and push the match to the array.
matches.push(match);
// Get the amount of text cut off from the string to make sure it doesn't match again,
let curcutoff = index + match[0].length;
// and cut the text off to just past the match to make sure it doesn't match again.
cutoff_text = cutoff_text.slice(curcutoff);
cutoff += curcutoff;
}
}
// Both formats should be identical.
return matches;
}
searchPage(query) {
let test;
// See if the regex starts correctly, if it doesnt, tell the user
if (query.slice(0, 1) != '/')
return `search: regex cannot start with '${test}'.`;
// Append a / if there wasn't another one (supporting lazy 's/regex' format regex)
if (query.slice(1).indexOf('/') == -1)
query += '/';
let regex_body = query.slice(1); // Slice off beginning slash.
regex_body = regex_body.slice(0, regex_body.indexOf('/')); // Slice off ending slash too.
// go after the regex body length (including first `/`), then skip the second `/`.
let flags = query.slice((regex_body.length + 1) + 1);
// Make sure it has global flag -- it's faster than manual iteration.
if (flags.indexOf('g') != -1)
flags += 'g';
// Doing it any other way than try...catch would be manual,
// and doing this manually would increase code complexity immensely.
// Instead, I just attempt and see if it errors.
let regex;
try { // Try compiling the regex.
regex = new RegExp(regex_body, flags);
}
catch (SyntaxError) { // If it fails, tell the user.
return `search: invalid regex: /${regex_body}/${flags}`;
}
// Conveniently, MediaWiki places the body of the wiki page under a specific class.
let elementSearched = document.body.getElementsByClassName("mw-body")[0];
let matches = this.doActualSearching(elementSearched, regex);
let count = matches.tooMany ? "too many" : matches.length;
let str = `Regex ${regex.toString()} produced ${count} results`;
if (!matches.length)
return str + '.';
else
{
str += ':';
str += '\n';
str += '\nMatches:';
for (let [index, match] of Object.entries(matches))
{
if (isNaN(parseInt(index)))
continue;
let matchText = match[0]; // only interested in full text
let matchInput = match.input; // the original text matched against
let matchStart = match.index; // the index in the original text the matched text starts
let matchEnd = matchStart + matchText.length; // where it ends
let matchCenter = Math.round(matchStart + matchText.length/2); // the index the center is closest to
// Define the window based on the visibleMatched constant.
// Math.round here make sure the center index is consistent when floor'd and ceil'd,
// The Math.floor and Math.ceil make sure the most accurate integer length is chosen.
let visibleStart = Math.floor(matchCenter - visibleMatched/2);
let visibleEnd = Math.ceil(matchCenter + visibleMatched/2);
// Bounds enforcement
visibleStart = (visibleStart < 0) ? 0 : visibleStart; // Don't go below the first character
visibleEnd = (visibleEnd > matchInput.length-1) ? matchInput.length-1 : visibleEnd; // Don't go past the last character
let n;
// Find a previous newline if one exists. If it does, and it's in the visible range, clip it.
if ((n = matchInput.lastIndexOf('\n', matchStart - 1)) != -1)
if (n >= visibleStart)
visibleStart = n + 1;
// Find a next newline if one exists. If it does, and it's in the visible range, clip it.
if ((n = matchInput.indexOf('\n', matchEnd)) != -1)
if (n <= visibleEnd)
visibleEnd = n;
let matchShown = ""; // The shown portion of the text, which includes the match.
// Add ellipsis to the beginning if there's more of this line to see.
if (visibleStart != 0 && matchInput.charAt(visibleStart - 1) != '\n')
matchShown += "...";
// Append the visible section.
matchShown += matchInput.substring(visibleStart, visibleEnd);
// Add ellipsis to the end if there's more of this line to see.
if (visibleEnd != matchInput.length - 1 && matchInput.charAt(visibleEnd) != '\n')
matchShown += "...";
// And print.
str += `\n${parseInt(index) + 1}: ${matchShown}`
}
if (matches.tooMany)
str += `\n... looks like infinite matches. Fix your regex!`
}
return str;
}
goToSection(section) {
let ret = encodeURI(section);
window.location.hash = ret;
}
}
let vimmyWikiNav;
let autoopen_keys = {
"?": "s/",
"S": "s/",
"#": "#"
}
function vwn_keyup(event)
{
if (event.key == "Escape")
return vimmyWikiNav.toggle("");
if (event.key == "Enter")
return vimmyWikiNav.closeWindow();
let char = autoopen_keys[event.key];
if (typeof char !== "undefined" && event.ctrlKey)
{
event.preventDefault();
return vimmyWikiNav.open(char);
}
}
class VimmyWikiNav {
constructor() {
this.opened = false;
this.inputElement = null;
this.windowElement = null;
this.stringToParse = "";
this.commandParser = new VWN_Command();
}
execute() {
this.stringToParse = this.inputElement.value;
this.close();
let char = this.stringToParse.slice(0, 1);
let args = this.stringToParse.slice(1);
this.commandParser.run(char, args).then((output) => {
if (typeof output !== "undefined")
{
this.constructWindowElement(output);
document.body.insertBefore(this.windowElement, document.body.firstChild);
this.windowElement.focus();
}
});
}
constructInputElement() {
if (this.inputElement)
return;
this.inputElement = document.createElement("input");
this.inputElement.parentVimmyWikiNav = this;
this.inputElement.addEventListener("keyup", function(event)
{
let element = event.target;
if (event.key == "Enter" && element.parentVimmyWikiNav)
element.parentVimmyWikiNav.execute();
})
this.inputElement.id = "vim-textbox";
this.inputElement.style.position = "fixed";
this.inputElement.style.top = "20px";
this.inputElement.style.left = "10%";
this.inputElement.style.width = "80%";
this.inputElement.style.zIndex = "1";
this.inputElement.style.backgroundColor = input_bg;
this.inputElement.style.color = input_fg;
this.inputElement.style.fontSize = "24px";
this.inputElement.style.fontFamily = "monospace";
}
constructWindowElement(text) {
if (this.windowElement)
return;
this.windowElement = document.createElement("div");
this.windowElement.parentVimmyWikiNav = this;
this.windowElement.id = "vim-window";
this.windowElement.style.backgroundColor = window_bg;
this.windowElement.style.position = "fixed";
this.windowElement.style.top = "10%";
this.windowElement.style.left = "10%";
this.windowElement.style.width = "80%";
this.windowElement.style.height = "80%";
this.windowElement.style.zIndex = "1";
this.windowElement.style.boxShadow = "10px 10px 10px grey";
this.windowElement.style.borderRadius = "12px";
this.windowElement.onblur = () => {
document.body.removeChild(this.windowElement);
this.windowElement = null;
}
let closeButton = document.createElement("button");
closeButton.style.top = "3%";
closeButton.style.left = "92%";
closeButton.style.width = "4%";
closeButton.style.height = "6%";
closeButton.style.position = "absolute";
closeButton.style.backgroundColor = window_fg;
closeButton.style.color = window_bg;
closeButton.innerText = "×";
closeButton.onclick = () => {
document.body.removeChild(this.windowElement);
this.windowElement = null;
}
closeButton.addEventListener("keyup", (event) => {
if (event.key == "Enter")
event.target.click();
});
this.windowElement.appendChild(closeButton);
closeButton.focus();
let innerDiv = document.createElement("div");
innerDiv.style.top = "5%";
innerDiv.style.height = "100%";
innerDiv.style.zIndex = "-1";
innerDiv.style.overflow = "auto";
this.windowElement.appendChild(innerDiv);
let code = document.createElement("p");
code.innerText = text;
code.style.fontFamily = "monospace";
code.style.fontSize = "16px";
code.style.margin = "8px 8px 8px 8px";
code.style.color = window_fg;
innerDiv.appendChild(code);
code.focus();
}
closeWindow()
{
if (this.windowElement)
{
document.body.removeChild(this.windowElement);
this.windowElement = null;
return;
}
}
toggle(start) {
this.closeWindow();
this.opened = !this.opened;
this.constructInputElement();
this.inputElement.value = start ? start : "";
if (this.opened)
{
document.body.insertBefore(this.inputElement, document.body.firstChild);
this.inputElement.focus();
}
else
{
document.body.removeChild(this.inputElement);
document.body.focus();
}
}
open(start) {
if (this.opened)
return;
this.toggle(start);
}
close(start) {
if (!this.opened)
return;
this.toggle(start);
}
}
vimmyWikiNav = new VimmyWikiNav();
document.body.addEventListener("keydown", vwn_keyup);