converter/src/main/js/main.js

216 lines
7.2 KiB
JavaScript

// noinspection JSUnresolvedVariable
const {$, doAfterLoad, stringToHtml} = 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
*/
function stringReplaceAt(str, index, replacement) {
return str.substring(0, index) + replacement + str.substring(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
*/
function stringReplaceAll(str, target, replacement) {
return 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
*/
function stringReplaceAlls(str, targets, replacements) {
return 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 = stringToHtml(`<label for="${this.name}-input">${this.name}</label>`);
this.textarea = stringToHtml(`<textarea id="${this.name}-input" class="number-input"></textarea>`);
this.textarea.oninput = () => {
if (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));
};
this.wrapper = document.createElement("article");
this.wrapper.appendChild(this.label);
this.wrapper.appendChild(this.textarea);
}
addToParent(parent) {
parent.appendChild(this.wrapper);
}
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 = stringToHtml(`<select id="${this.name}-dropdown"></select>`);
this.dropdown.onchange = () => {
const selectedOption = Base64NumeralSystemInput.dropdownOptions()[this.dropdown.value];
this.setLastDigits(selectedOption[0], selectedOption[1]);
};
Object
.keys(Base64NumeralSystemInput.dropdownOptions())
.forEach(key => {
const text = key + ": " + Base64NumeralSystemInput.dropdownOptions()[key].join("");
const option = stringToHtml(`<option value="${key}">${text}</option>`);
this.dropdown.appendChild(option);
});
this.wrapper.appendChild(this.dropdown);
}
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);
}
}
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
)
),
];
function updateAllInputs(source, newValue) {
for (const input of inputs)
if (input !== source)
input.update(newValue);
}
doAfterLoad(() => {
const form = $("#inputs");
for (const input of inputs)
input.addToParent(form);
updateAllInputs(undefined, bigInt(42));
inputs[0].textarea.focus();
});