Migrate to npm, use template package

This commit is contained in:
Florine W. Dekker 2020-05-16 18:54:07 +02:00
parent c99d596989
commit 89083aaeb0
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
9 changed files with 561 additions and 290 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
package-lock.json binary

104
.gitignore vendored Normal file
View File

@ -0,0 +1,104 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port

112
Gruntfile.js Normal file
View File

@ -0,0 +1,112 @@
const path = require("path");
module.exports = grunt => {
grunt.initConfig({
pkg: grunt.file.readJSON("package.json"),
clean: {
default: ["dist/"],
},
copy: {
html: {
files: [{expand: true, cwd: "src/main/", src: "**/*.html", dest: "dist/"}]
},
},
focus: {
dev: {
include: ["html", "js", "link"],
},
},
replace: {
dev: {
src: ["./dist/*.html", "./dist/*.js"],
replacements: [
{
from: "%%VERSION_NUMBER%%",
to: "<%= pkg.version %>+" + new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "")
}
],
overwrite: true
},
deploy: {
src: ["./dist/*.html", "./dist/*.js"],
replacements: [
{
from: "%%VERSION_NUMBER%%",
to: "<%= pkg.version %>"
}
],
overwrite: true
},
},
watch: {
html: {
files: ["src/main/**/*.html"],
tasks: ["copy:html"],
},
js: {
files: ["src/main/**/*.js"],
tasks: ["webpack:dev", "replace:dev"],
},
link: {
files: ["node_modules/@fwdekker/*/dist/**"],
tasks: ["webpack:dev", "replace:dev"],
},
},
webpack: {
options: {
entry: "./src/main/js/index.js",
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".js"],
},
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist/"),
},
},
dev: {
mode: "development",
devtool: "inline-source-map",
},
deploy: {
mode: "production",
},
},
});
grunt.loadNpmTasks("grunt-contrib-clean");
grunt.loadNpmTasks("grunt-contrib-copy");
grunt.loadNpmTasks("grunt-contrib-watch");
grunt.loadNpmTasks("grunt-focus");
grunt.loadNpmTasks("grunt-text-replace");
grunt.loadNpmTasks("grunt-webpack");
grunt.registerTask("dev", [
// Pre
"clean",
// Copy files
"copy:html",
// Compile JS
"webpack:dev",
"replace:dev",
]);
grunt.registerTask("dev:server", ["dev", "focus:dev"]);
grunt.registerTask("deploy", [
// Pre
"clean",
// Copy files
"copy:html",
// Compile JS
"webpack:deploy",
"replace:deploy",
]);
grunt.registerTask("default", ["dev"]);
};

View File

@ -2,3 +2,23 @@
An online tool to convert values between number systems.
Quickly convert hexadecimal to binary or base64 to ASCII.
Everything happens in the browser, so nothing is submitted to any server.
## Development
### Requirements
* [npm](https://www.npmjs.com/)
### Setting up
```shell script
# Install dependencies (only needed once)
$> npm ci
```
### Building
```shell script
# Build the tool in `dist/` for development
$> npm run dev
# Same as above, but automatically rerun it whenever files are changed
$> npm run dev:server
# Build the tool in `dist/` for deployment
$> npm run deploy
```

View File

@ -1,290 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="author" content="Felix W. Dekker" />
<meta name="application-name" content="Converter" />
<meta name="description" content="Convert numbers to and from various bases." />
<meta name="theme-color" content="#0033cc" />
<title>Converter | FWDekker</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic"
crossorigin="anonymous" />
<link rel="stylesheet" href="https://static.fwdekker.com/css/milligram-bundle.min.css" crossorigin="anonymous" />
</head>
<body>
<main class="wrapper">
<!-- Header -->
<header class="header">
<section class="container">
<h1>Converter</h1>
<noscript>
<span style="color: red; font-weight: bold;">
This website does not function if JavaScript is disabled.
Please check the <a href="https://www.enable-javascript.com/">
instructions on how to enable JavaScript in your web browser</a>.
</span>
</noscript>
<blockquote>
<p><em>Convert numbers to and from various bases.</em></p>
</blockquote>
</section>
</header>
<!-- Input -->
<section class="container">
<div class="row">
<div class="column">
<form>
<fieldset id="inputs">
</fieldset>
</form>
</div>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<section class="container">
Made by <a href="https://fwdekker.com/">Felix W. Dekker</a>.
Licensed under the
<a href="https://git.fwdekker.com/FWDekker/converter/src/branch/master/LICENSE">MIT License</a>.
Source code available on <a href="https://git.fwdekker.com/FWDekker/converter/">git</a>.
<div style="float: right;">v1.4.6</div>
</section>
</footer>
</main>
<!-- Scripts -->
<script src="https://static.fwdekker.com/js/common.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/big-integer/1.6.44/BigInteger.min.js"
integrity="sha256-es+ex6Oj344uak+VnCPyaHY2nzQkqhr7ByWVQgdjATA=" crossorigin="anonymous"></script>
<script>
/**
* 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", caseSensitive = false)),
new NumeralSystemInput("Hexadecimal", new NumeralSystem(16, "0123456789abcdef", caseSensitive = false)),
new Base64NumeralSystemInput("Base64"),
new NumeralSystemInput(
"ASCII",
new NumeralSystem(
256,
new Array(256).fill(0).map((_, it) => String.fromCharCode(it)).join(""),
caseSensitive = true
)
),
];
const updateAllInputs = (source, newValue) => {
for (const input of inputs)
if (input !== source)
input.update(newValue);
};
doAfterLoad(() => {
const inputParent = $("#inputs");
for (const input of inputs)
input.addToParent(inputParent);
updateAllInputs(undefined, bigInt(42));
inputs[0].textarea.focus();
});
</script>
</body>
</html>

BIN
package-lock.json generated Normal file

Binary file not shown.

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "converter",
"version": "1.4.7",
"description": "Convert numbers to and from various bases.",
"author": "Felix W. Dekker",
"browser": "dist/bundle.js",
"repository": {
"type": "git",
"url": "git@git.fwdekker.com:FWDekker/converter.git"
},
"private": true,
"scripts": {
"clean": "grunt clean",
"dev": "grunt dev",
"dev:server": "grunt dev:server",
"deploy": "grunt deploy"
},
"dependencies": {
"@fwdekker/template": "^0.0.14",
"big-integer": "^1.6.48"
},
"devDependencies": {
"grunt": "^1.1.0",
"grunt-cli": "^1.3.2",
"grunt-contrib-clean": "^2.0.0",
"grunt-contrib-copy": "^1.0.0",
"grunt-contrib-watch": "^1.1.0",
"grunt-focus": "^1.0.0",
"grunt-text-replace": "^0.4.0",
"grunt-webpack": "^3.1.3",
"webpack": "^4.42.1",
"webpack-cli": "^3.3.11"
}
}

49
src/main/index.html Normal file
View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="author" content="Felix W. Dekker" />
<meta name="application-name" content="Converter" />
<meta name="description" content="Convert numbers to and from various bases." />
<meta name="theme-color" content="#0033cc" />
<title>Converter | FWDekker</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic"
crossorigin="anonymous" />
</head>
<body>
<noscript>
<span style="color: red; font-weight: bold;">
This website does not function if JavaScript is disabled.
Please check the <a href="https://www.enable-javascript.com/">
instructions on how to enable JavaScript in your web browser</a>.
</span>
</noscript>
<main style="display: none;">
<div id="nav"></div>
<div id="contents">
<div id="header"></div>
<!-- Input -->
<section class="container">
<div class="row">
<div class="column">
<form>
<fieldset id="inputs">
</fieldset>
</form>
</div>
</div>
</section>
</div>
<div id="footer"></div>
</main>
<!-- Scripts -->
<script src="bundle.js"></script>
</body>
</html>

241
src/main/js/index.js Normal file
View File

@ -0,0 +1,241 @@
import {$, doAfterLoad, footer, header, nav} from "@fwdekker/template";
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());
$("#header").appendChild(header({
title: "Converter",
description: "Convert numbers to and from various bases"
}));
$("#footer").appendChild(footer({
author: "Felix W. Dekker",
authorURL: "https://fwdekker.com/",
license: "MIT License",
licenseURL: "https://git.fwdekker.com/FWDekker/converter/src/branch/master/LICENSE",
vcs: "git",
vcsURL: "https://git.fwdekker.com/FWDekker/converter/",
version: "v%%VERSION_NUMBER%%"
}));
$("main").style.display = null;
});
doAfterLoad(() => {
const inputParent = $("#inputs");
for (const input of inputs)
input.addToParent(inputParent);
updateAllInputs(undefined, bigInt(42));
inputs[0].textarea.focus();
});