Compare commits

...

5 Commits

6 changed files with 324 additions and 89 deletions

View File

@ -7,18 +7,21 @@ module.exports = grunt => {
default: ["dist/"],
},
copy: {
css: {
files: [{expand: true, cwd: "src/main/", src: "**/*.css", dest: "dist/", flatten: true}],
},
html: {
files: [{expand: true, cwd: "src/main/", src: "**/*.html", dest: "dist/", flatten: true}]
files: [{expand: true, cwd: "src/main/", src: "**/*.html", dest: "dist/", flatten: true}],
},
},
focus: {
dev: {
include: ["html", "js"],
include: ["css", "html", "js"],
},
},
replace: {
dev: {
src: ["./dist/*.html", "./dist/*.js"],
src: ["./dist/**/*.html", "./dist/**/*.js"],
replacements: [
{
from: "%%VERSION_NUMBER%%",
@ -28,7 +31,7 @@ module.exports = grunt => {
overwrite: true
},
deploy: {
src: ["./dist/*.html", "./dist/*.js"],
src: ["./dist/**/*.html", "./dist/**/*.js"],
replacements: [
{
from: "%%VERSION_NUMBER%%",
@ -39,6 +42,10 @@ module.exports = grunt => {
},
},
watch: {
css: {
files: ["src/main/**/*.css"],
tasks: ["copy:css"],
},
html: {
files: ["src/main/**/*.html"],
tasks: ["copy:html"],
@ -88,6 +95,7 @@ module.exports = grunt => {
// Pre
"clean",
// Copy files
"copy:css",
"copy:html",
// Compile JS
"webpack:dev",
@ -98,6 +106,7 @@ module.exports = grunt => {
// Pre
"clean",
// Copy files
"copy:css",
"copy:html",
// Compile JS
"webpack:deploy",

BIN
package-lock.json generated

Binary file not shown.

View File

@ -1,6 +1,6 @@
{
"name": "converter",
"version": "1.4.14",
"version": "1.5.0",
"description": "Convert numbers to and from various bases.",
"author": "Florine W. Dekker",
"browser": "dist/bundle.js",

21
src/main/css/main.css Normal file
View File

@ -0,0 +1,21 @@
#inputs {
display: flex;
flex-wrap: wrap;
justify-content: left;
/*noinspection CssUnresolvedCustomProperty*/
gap: var(--spacing);
}
#inputs article {
margin: 0;
/* Make `textarea`s expand automatically */
display: flex;
flex-direction: column;
}
#inputs textarea {
display: block;
width: 25em;
height: 100%;
}

View File

@ -5,18 +5,22 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="author" content="Florine W. Dekker" />
<meta name="application-name" content="Converter" />
<meta name="description" content="Convert numbers to and from various bases." />
<meta name="description" content="Convert instantly between binary, decimal, hexadecimal, base64, and ASCII." />
<meta name="theme-color" content="#0033cc" />
<meta name="fwd:auto:show-main" />
<meta name="fwd:nav:target" content="#nav" />
<meta name="fwd:nav:highlight-path" content="/Tools/Converter/" />
<meta name="fwd:footer:target" content="#footer" />
<meta name="fwd:footer:vcs-url" content="https://git.fwdekker.com/tools/converter/" />
<meta name="fwd:footer:version" content="v%%VERSION_NUMBER%%" />
<meta name="fwd:validation:load-forms" />
<title>Converter | FWDekker</title>
<link rel="stylesheet" href="https://static.fwdekker.com/lib/template/3.x.x/template.css?v=%%VERSION_NUMBER%%" />
<!--suppress HtmlUnknownTarget -->
<link rel="stylesheet" href="main.css?v=%%VERSION_NUMBER%%" />
<script async src="https://stats.fwdekker.com/count.js"
data-goatcounter="https://stats.fwdekker.com/count"></script>
</head>
@ -31,19 +35,27 @@
</p>
</noscript>
<nav id="nav"></nav>
<main class="container hidden">
<main class="hidden">
<div role="document">
<section>
<section class="container">
<header class="fwd-header">
<hgroup>
<h1><a href=".">Converter</a></h1>
<h2>Convert numbers to and from various bases.</h2>
<h2>
Convert numbers to and from various bases.
Use commas to separate multiple values.
</h2>
</hgroup>
</header>
<form id="inputs"></form>
</section>
<footer id="footer"></footer>
<section class="container-fluid">
<form id="inputs" class="grid"></form>
</section>
<section class="container">
<footer id="footer"></footer>
</section>
</div>
</main>

View File

@ -1,6 +1,8 @@
// noinspection JSUnresolvedVariable
const {$, doAfterLoad} = window.fwdekker;
import bigInt from "big-integer"
const {$, doAfterLoad, stringToHtml} = window.fwdekker;
// noinspection JSUnresolvedVariable
const {clearInputValidity, showInputInvalid} = window.fwdekker.validation;
import bigInt from "big-integer";
/**
@ -11,8 +13,9 @@ import bigInt from "big-integer"
* @param replacement the replacement to insert into the string
* @returns {string} the input string with one character replaced
*/
const stringReplaceAt = (str, index, replacement) =>
str.substring(0, index) + replacement + str.substring(index + replacement.length);
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.
@ -22,8 +25,9 @@ const stringReplaceAt = (str, index, replacement) =>
* @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);
function stringReplaceAll(str, target, replacement) {
return str.split(target).join(replacement);
}
/**
* Runs `stringReplaceAll` for each character in `targets` and `replacements`.
@ -34,87 +38,196 @@ const stringReplaceAll = (str, target, replacement) =>
* 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);
function stringReplaceAlls(str, targets, replacements) {
return Array.from(targets)
.reduce((output, target, index) => stringReplaceAll(output, target, replacements[index]), str);
}
/**
* A numeral system, expressing a conversion from decimal to this system.
*/
class NumeralSystem {
constructor(base, alphabet, caseSensitive) {
/**
* Constructs a new numeral system.
*
* @param base the base
* @param alphabet the symbols representing values in this system
* @param caseSensitive `true` if and only if capitalization affects the value of a number in this system
*/
constructor(base, alphabet, caseSensitive = false) {
this.base = base;
this.alphabet = alphabet;
this.caseSensitive = caseSensitive;
}
/**
* Converts a decimal number to a number in this system.
*
* @param decimalNumber {bigInt} the decimal number to convert
* @returns {string} the representation of `decimalNumber` in this system
*/
decimalToBase(decimalNumber) {
return decimalNumber.toString(this.base, this.alphabet);
}
/**
* Converts multiple decimal numbers to numbers in this system.
*
* @param decimalNumbers {bigInt[]} the decimal numbers to convert
* @returns {string[]} the representations of `decimalNumbers` in this system
*/
decimalsToBases(decimalNumbers) {
return decimalNumbers.map(it => this.decimalToBase(it));
}
/**
* Converts a number from this system to decimal.
*
* @param baseString {string} the number in this system to convert
* @returns {bigInt} the decimal representation of `baseString`
*/
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, "\\$&");
/**
* Converts numbers from this system to decimal.
*
* @param baseStrings {string[]} the numbers in this system to convert
* @returns {bigInt[]} the decimal representations of `baseStrings`
*/
basesToDecimals(baseStrings) {
return baseStrings.map(it => this.baseToDecimal(it));
}
return baseString.replace(new RegExp(`[^${regexSafeAlphabet}]`, "g"), "");
/**
* Checks whether `testString` is a legal substring of a number in this numerical system.
*
* @param string {string} a substring of a number in this numerical system
* @returns {boolean} the filtered base string
*/
isLegal(string) {
const alphabet = this.caseSensitive
? (this.alphabet + ",")
: (this.alphabet.toLowerCase() + this.alphabet.toUpperCase() + ",");
// Regex from https://stackoverflow.com/a/3561711/
const alphabetRegex = alphabet.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
return (new RegExp(`^[${alphabetRegex}]*$`)).test(string);
}
}
/**
* An input field that contains a value in some numeral system.
*/
class NumeralSystemInput {
/**
* Constructs a new numeral system input, including HTML elements.
*
* @param name {string} the human-readable name of the system
* @param numeralSystem {NumeralSystem} the numeral system in which values in this input are expressed
*/
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.wrapper = document.createElement("article");
const base = this.numeralSystem.base;
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;
const label = stringToHtml(`<label for="b${base}-input">${this.name} <small>(base ${base})</small></label>`);
this.wrapper.appendChild(label);
this.textarea.value = this.numeralSystem.filterBaseString(this.textarea.value);
updateAllInputs(this, this.numeralSystem.baseToDecimal(this.textarea.value));
};
this.textarea = stringToHtml(`<textarea id="b${base}-input" class="number-input" rows="4"></textarea>`);
this.textarea.addEventListener("beforeinput", event => {
if (
(
event.data == null && event.inputType === "insertLineBreak" &&
!this.numeralSystem.alphabet.includes("\n")
) ||
(
event.data != null && !this.numeralSystem.isLegal(event.data)
)
) {
event.preventDefault();
}
});
this.textarea.addEventListener("input", () => {
clearInputValidity(this.textarea);
try {
updateAllInputs(this, this.numeralSystem.basesToDecimals(this.getValues()));
} catch (error) {
showInputInvalid(this.textarea, error.message);
}
});
this.wrapper.appendChild(this.textarea);
const hint = stringToHtml(`<small id="b${base}-input-hint" data-hint-for="b${base}-input"></small>`);
this.wrapper.appendChild(hint);
}
/**
* Appends this input's elements to `parent.`
*
* @param parent {HTMLElement} the element to add this input to
*/
addToParent(parent) {
parent.appendChild(this.label);
parent.appendChild(this.textarea);
parent.appendChild(this.wrapper);
}
update(decimalNumber) {
this.textarea.value = this.numeralSystem.decimalToBase(decimalNumber);
/**
* Returns the input's values.
*
* @returns {string[]} the input's values expressed in the associated numeral system
*/
getValues() {
return this.textarea.value.split(",");
}
/**
* Updates the input's value to contain this input's numeral system's representation of `decimalNumbers`.
*
* @param decimalNumbers {bigInt[]} the decimal numbers to represent in the input
*/
setValues(decimalNumbers) {
clearInputValidity(this.textarea);
this.textarea.value = this.numeralSystem.decimalsToBases(decimalNumbers).join(",");
}
}
/**
* The base-64 `NumeralSystem`.
*/
class Base64NumeralSystem extends NumeralSystem {
// TODO Convert static methods to static properties once supported by Firefox
/**
* @returns {string} the default base-64 alphabet, including padding
*/
static defaultAlphabet() {
return "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
}
/**
* Constructs a new base 64 numeral system.
* Constructs a new base-64 numeral system.
*
* @param alphabet the 64 characters to encode numbers with, and the padding character at the end
* @param alphabet {string} the 64 characters to encode numbers with, and the padding character at the end
*/
constructor(alphabet) {
super(64, alphabet, true);
}
/**
* Converts a decimal number to base-64.
*
* @param decimalNumber {bigInt} the decimal number to convert to base-64
* @returns {string} the base-64 representation of `decimalNumber`
*/
decimalToBase(decimalNumber) {
const hex = decimalNumber.toString(16);
const b64 = Array.from(hex.padStart(hex.length + hex.length % 2))
@ -128,71 +241,148 @@ class Base64NumeralSystem extends NumeralSystem {
return stringReplaceAlls(btoa(b64), Base64NumeralSystem.defaultAlphabet(), this.alphabet);
}
/**
* Converts a base-64 number to decimal.
*
* @param baseString {string} the base-64 number to convert to decimal
* @returns {bigInt} the decimal representation of `baseString`
*/
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);
try {
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);
} catch (error) {
throw new Error("Invalid base64 string. Maybe the padding is wrong?");
}
}
}
/**
* An input field for base-64 numbers.
*/
class Base64NumeralSystemInput extends NumeralSystemInput {
/**
* @returns {Object.<string, [string, string]>} the variants for the last two characters in the base-64 alphabet
*/
// TODO Convert static methods to static properties once supported by Firefox
static dropdownOptions() {
return {"Standard": ['+', '/'], "Filename": ['-', '_'], "IMAP": ['+', ',']};
}
/**
* Constructs a new base-64 input, including HTML elements.
*
* @param name {string} the human-readable name of the system
*/
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];
const dropdown = stringToHtml(`<select id="${this.numeralSystem.base}-dropdown"></select>`);
dropdown.addEventListener("change", () => {
const selectedOption = Base64NumeralSystemInput.dropdownOptions()[dropdown.value];
this.setLastDigits(selectedOption[0], selectedOption[1]);
};
});
this.wrapper.appendChild(dropdown);
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;
});
Object
.keys(Base64NumeralSystemInput.dropdownOptions())
.map(key => {
const text = key + ": " + Base64NumeralSystemInput.dropdownOptions()[key].join("");
return stringToHtml(`<option value="${key}">${text}</option>`);
})
.forEach(option => dropdown.appendChild(option));
}
/**
* Changes the last two symbols of the alphabet.
*
* @param c62 the new 62nd (0-indexed) symbol of the alphabet
* @param c63 the new 63rd (0-indexed) symbol of the alphabet
*/
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));
parent.appendChild(this.label);
parent.appendChild(this.dropdown);
parent.appendChild(this.textarea);
this.numeralSystem.alphabet = stringReplaceAt(stringReplaceAt(this.numeralSystem.alphabet, 62, c62), 63, c63);
this.textarea.value = stringReplaceAll(stringReplaceAll(this.textarea.value, oc62, c62), oc63, c63);
}
}
/**
* An input field that contains a value in some numeral system, and can optionally interpret the separator symbol
* literally as part of its alphabet.
*
* Useful for numeral systems with separators in their alphabet. Lets the user decide whether to interpret those values
* as literals or as separators.
*/
class NumeralSystemInputWithToggleableSeparator extends NumeralSystemInput {
/**
* Constructs a new numeral system input, including HTML elements.
*
* @param name {string} the human-readable name of the system
* @param numeralSystem {NumeralSystem} the numeral system in which values in this input are expressed
*/
constructor(name, numeralSystem) {
if (!numeralSystem.alphabet.includes(","))
console.warn("Toggleable separator input incorrectly used on numeral system without comma in alphabet.");
super(name, numeralSystem);
const id = `b${this.numeralSystem.base}-separator-toggle`;
const checkboxContainer = document.createElement("div");
this.wrapper.appendChild(checkboxContainer);
this.separatorCheckbox = stringToHtml(`<input type="checkbox" role="switch" id="${id}" checked />`);
this.separatorCheckbox.addEventListener("change", () => this.textarea.dispatchEvent(new InputEvent("input")));
checkboxContainer.appendChild(this.separatorCheckbox);
const label = stringToHtml(`<label for="${id}">Comma separates values</label>`);
checkboxContainer.appendChild(label);
}
getValues() {
if (this.separatorCheckbox.checked)
return super.getValues();
else
return [this.textarea.value];
}
setValues(decimalNumbers) {
clearInputValidity(this.textarea);
const baseStrings = this.numeralSystem.decimalsToBases(decimalNumbers);
if (baseStrings.length === 1) {
this.separatorCheckbox.checked = !baseStrings[0].includes(",");
this.textarea.value = baseStrings[0];
} else {
if (baseStrings.some(it => it.includes(",")))
showInputInvalid(this.textarea, "Contains a mixture of literal commas and separator commas.", false);
this.textarea.value = baseStrings.join(",");
}
}
}
/**
* All the inputs to display.
*
* @type {NumeralSystemInput[]} all the inputs to display
*/
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 NumeralSystemInput("Duodecimal", new NumeralSystem(12, "0123456789ab")),
new NumeralSystemInput("Hexadecimal", new NumeralSystem(16, "0123456789abcdef")),
new Base64NumeralSystemInput("Base64"),
new NumeralSystemInput(
new NumeralSystemInputWithToggleableSeparator(
"ASCII",
new NumeralSystem(
256,
@ -202,21 +392,24 @@ const inputs = [
),
];
const updateAllInputs = (source, newValue) => {
/**
* Updates the values of all inputs to represent `newValue`.
*
* @param source {NumeralSystemInput} the input that triggered the update
* @param newDecimalValues {bigInt[]} the decimal representation of the new value
*/
function updateAllInputs(source, newDecimalValues) {
for (const input of inputs)
if (input !== source)
input.update(newValue);
};
input.setValues(newDecimalValues);
}
doAfterLoad(() => {
const inputParent = $("#inputs");
const form = $("#inputs");
for (const input of inputs)
input.addToParent(inputParent);
input.addToParent(form);
updateAllInputs(undefined, bigInt(42));
updateAllInputs(undefined, [bigInt(42), bigInt(17)]);
inputs[0].textarea.focus();
$("main").classList.remove("hidden");
});