converter/src/main/js/main.js

238 lines
8.0 KiB
JavaScript

// noinspection JSUnresolvedVariable
const {$, doAfterLoad, footer, header, nav} = window.fwdekker;
import bigInt from "big-integer"
/**
* Replaces the character at the given index with the given replacement.
*
* @param str the string to replace in
* @param index the index in the given string to replace at
* @param replacement the replacement to insert into the string
* @returns {string} the input string with one character replaced
*/
const stringReplaceAt = (str, index, replacement) =>
str.substr(0, index) + replacement + str.substr(index + replacement.length);
/**
* Replaces all instances of the target with the replacement.
*
* @param str the string to replace in
* @param target the character to replace
* @param replacement the replacement to insert into the string
* @returns {string} the input string with all instances of the target replaced
*/
const stringReplaceAll = (str, target, replacement) =>
str.split(target).join(replacement);
/**
* Runs `stringReplaceAll` for each character in `targets` and `replacements`.
*
* @param str the string to replace in
* @param targets the characters to replace
* @param replacements the replacements to insert into the string; each character here corresponds to a character
* in the targets string
* @returns {string} the input string with all instances of the targets replaced
*/
const stringReplaceAlls = (str, targets, replacements) =>
Array.from(targets).reduce((output, target, index) =>
stringReplaceAll(output, target, replacements[index]), str);
class NumeralSystem {
constructor(base, alphabet, caseSensitive) {
this.base = base;
this.alphabet = alphabet;
this.caseSensitive = caseSensitive;
}
decimalToBase(decimalNumber) {
return decimalNumber.toString(this.base, this.alphabet);
}
baseToDecimal(baseString) {
return bigInt(baseString, this.base, this.alphabet, this.caseSensitive);
}
filterBaseString(baseString) {
// Regex from https://stackoverflow.com/a/3561711/
const alphabet = this.caseSensitive
? this.alphabet
: this.alphabet.toLowerCase() + this.alphabet.toUpperCase();
const regexSafeAlphabet = alphabet.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
return baseString.replace(new RegExp(`[^${regexSafeAlphabet}]`, "g"), "");
}
}
class NumeralSystemInput {
constructor(name, numeralSystem) {
this.name = name;
this.numeralSystem = numeralSystem;
this.label = document.createElement("label");
this.label.setAttribute("for", `${this.name}Input`);
this.label.innerHTML = this.name;
this.textarea = document.createElement("textarea");
this.textarea.id = `${this.name}Input`;
this.textarea.className = "numberInput";
this.textarea.oninput = () => {
if (this.textarea.value === undefined || this.textarea.value === null || this.textarea.value === "")
return;
this.textarea.value = this.numeralSystem.filterBaseString(this.textarea.value);
updateAllInputs(this, this.numeralSystem.baseToDecimal(this.textarea.value));
};
}
addToParent(parent) {
parent.appendChild(this.label);
parent.appendChild(this.textarea);
}
update(decimalNumber) {
this.textarea.value = this.numeralSystem.decimalToBase(decimalNumber);
}
}
class Base64NumeralSystem extends NumeralSystem {
// TODO Convert static methods to static properties once supported by Firefox
static defaultAlphabet() {
return "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
}
/**
* Constructs a new base 64 numeral system.
*
* @param alphabet the 64 characters to encode numbers with, and the padding character at the end
*/
constructor(alphabet) {
super(64, alphabet, true);
}
decimalToBase(decimalNumber) {
const hex = decimalNumber.toString(16);
const b64 = Array.from(hex.padStart(hex.length + hex.length % 2))
.reduce((result, value, index, array) => {
if (index % 2 === 0) result.push(array.slice(index, index + 2));
return result;
}, [])
.map(pair => String.fromCharCode(parseInt(pair.join(""), 16)))
.join("");
return stringReplaceAlls(btoa(b64), Base64NumeralSystem.defaultAlphabet(), this.alphabet);
}
baseToDecimal(baseString) {
if (baseString.length % 4 === 1) throw new Error("Invalid input string length.");
const normalBaseString = stringReplaceAlls(baseString, this.alphabet, Base64NumeralSystem.defaultAlphabet());
const hex = Array.from(atob(normalBaseString))
.map(char => char.charCodeAt(0).toString(16).padStart(2, "0")).join("");
return bigInt(hex, 16);
}
}
class Base64NumeralSystemInput extends NumeralSystemInput {
// TODO Convert static methods to static properties once supported by Firefox
static dropdownOptions() {
return {"Standard": ['+', '/'], "Filename": ['-', '_'], "IMAP": ['+', ',']};
}
constructor(name) {
super(name, new Base64NumeralSystem(Base64NumeralSystem.defaultAlphabet()));
this.dropdown = document.createElement("select");
this.dropdown.id = `${this.name}Dropdown`;
this.dropdown.onchange = () => {
const selectedOption = Base64NumeralSystemInput.dropdownOptions()[this.dropdown.value];
this.setLastDigits(selectedOption[0], selectedOption[1]);
};
this.dropdownDiv = document.createElement("div");
this.dropdownDiv.classList.add("float-right");
this.options =
Object.keys(Base64NumeralSystemInput.dropdownOptions()).map(key => {
const option = document.createElement("option");
option.value = key;
option.text = key + ": " + Base64NumeralSystemInput.dropdownOptions()[key].join("");
return option;
});
}
setLastDigits(c62, c63) {
const oc62 = this.numeralSystem.alphabet[62];
const oc63 = this.numeralSystem.alphabet[63];
this.numeralSystem.alphabet =
stringReplaceAt(stringReplaceAt(this.numeralSystem.alphabet, 62, c62), 63, c63);
this.textarea.value =
stringReplaceAll(stringReplaceAll(this.textarea.value, oc62, c62), oc63, c63);
}
addToParent(parent) {
this.options.forEach(option => this.dropdown.appendChild(option));
this.dropdownDiv.appendChild(this.dropdown);
parent.appendChild(this.dropdownDiv);
parent.appendChild(this.label);
parent.appendChild(this.textarea);
}
}
const inputs = [
new NumeralSystemInput("Binary", new NumeralSystem(2, "01")),
new NumeralSystemInput("Octal", new NumeralSystem(8, "01234567")),
new NumeralSystemInput("Decimal", new NumeralSystem(10, "0123456789")),
new NumeralSystemInput("Duodecimal", new NumeralSystem(12, "0123456789ab", false)),
new NumeralSystemInput("Hexadecimal", new NumeralSystem(16, "0123456789abcdef", false)),
new Base64NumeralSystemInput("Base64"),
new NumeralSystemInput(
"ASCII",
new NumeralSystem(
256,
new Array(256).fill(0).map((_, it) => String.fromCharCode(it)).join(""),
true
)
),
];
const updateAllInputs = (source, newValue) => {
for (const input of inputs)
if (input !== source)
input.update(newValue);
};
doAfterLoad(() => {
$("#nav").appendChild(nav("/Tools/Converter/"));
$("#header").appendChild(header({
title: "Converter",
description: "Convert numbers to and from various bases"
}));
$("#footer").appendChild(footer({
vcsURL: "https://git.fwdekker.com/FWDekker/converter/",
version: "v%%VERSION_NUMBER%%"
}));
$("main").classList.remove("hidden");
});
doAfterLoad(() => {
const inputParent = $("#inputs");
for (const input of inputs)
input.addToParent(inputParent);
updateAllInputs(undefined, bigInt(42));
inputs[0].textarea.focus();
});