Migrate to npm, migrate to KaTeX

This commit is contained in:
Florine W. Dekker 2021-03-26 01:48:55 +01:00
parent e577289490
commit bc01737b2a
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
9 changed files with 527 additions and 226 deletions

1
.gitattributes vendored Normal file
View File

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

117
.gitignore vendored Normal file
View File

@ -0,0 +1,117 @@
## Node
# 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/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# 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
.parcel-cache
# Next.js build output
.next
out
# 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
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.pnp.*

126
Gruntfile.js Normal file
View File

@ -0,0 +1,126 @@
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/Main.js",
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
{
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
use: [{
loader: "file-loader",
options: {
name: "[name].[ext]",
outputPath: "./",
},
}],
},
],
},
resolve: {
extensions: [".css", ".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

@ -1,2 +1,22 @@
# Simplify fractions
Simplifies a fraction of integers by dividing both operands by their greatest common divisor.
## 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,226 +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="Simplify fractions" />
<meta name="description" content="Simplify fractions." />
<meta name="theme-color" content="#0033cc" />
<title>Simplify fractions | 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">
<div class="container">
<h1>Simplify fractions</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>Simplify a fraction to eliminate common factors.</em></p>
</blockquote>
</div>
</header>
<!-- Input -->
<section class="container">
<form>
<fieldset>
<label for="numerator">Numerator</label>
<input type="number" id="numerator" min="-2147483647" max="2147483647" autofocus />
<label for="denominator">Denominator</label>
<input type="number" id="denominator" min="-2147483647" max="2147483647" />
</fieldset>
</form>
</section>
<!-- Output -->
<section class="container">
<span id="out"></span>
</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/simplify-fractions/src/branch/master/LICENSE">MIT License</a>.
Source code available on <a href="https://git.fwdekker.com/FWDekker/simplify-fractions/">git</a>.
<div style="float: right;">v1.1.0</div>
</section>
</footer>
</main>
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML"
integrity="sha256-nvJJv9wWKEm88qvoQl9ekL2J+k/RWIsaSScxxlsrv8k=" crossorigin="anonymous"></script>
<script src="https://static.fwdekker.com/js/common.js" crossorigin="anonymous"></script>
<script>
/**
* A fraction that can be simplified.
*/
class Fraction {
constructor(numerator, denominator) {
if (!isInt(numerator) || !isInt(denominator))
throw new Error("Numerator and denominator must be integer-like.");
this.sign = numerator < 0 !== denominator < 0 ? -1 : 1;
this.numerator = Math.abs(+numerator);
this.denominator = Math.abs(+denominator);
}
/**
* Returns a new fraction such that the gcd of the numerator and denominator is 1.
*
* @returns {Fraction} a new fraction such that the gcd of the numerator and denominator is 1
*/
simplify() {
const common = gcd(this.numerator, this.denominator);
return new Fraction(this.sign * this.numerator / common, this.denominator / common);
}
/**
* Returns the MathJax string representation of this fraction.
*
* @returns {string} the MathJax string representation of this fraction
*/
toString() {
let frac = `\\frac{${this.numerator}}{${this.denominator}}`;
if (this.sign === -1)
frac = `-${frac}`;
return frac;
}
/**
* Returns the MathJax string representation of this fraction, or of the numerator if the denominator is 1.
*
* @returns {string} the MathJax string representation of this fraction, or of the numerator if the denominator
* is 1.
*/
toReducedString() {
if (this.numerator === 0)
return "0";
let frac;
if (this.denominator === 1)
frac = `${this.numerator}`;
else
frac = `\\frac{${this.numerator}}{${this.denominator}}`;
if (this.sign === -1)
frac = `-${frac}`;
return frac;
}
}
// noinspection EqualityComparisonWithCoercionJS
/**
* Returns `true` if and only if `n` is an integer.
*
* @param n {*} the value to check for integerness
* @returns {boolean} `true` if and only if `n` is an integer
*/
const isInt = n => n == parseInt(n);
/**
* Returns the greatest common divisor of `a` and `b`.
*
* @param a {number} the first operand
* @param b {number} the second operand
* @returns {number} the greatest common divisor of `a` and `b`
*/
const gcd = (a, b) => {
if (b > a) {
const temp = a;
a = b;
b = temp;
}
while (true) {
if (b === 0) return a;
a %= b;
if (a === 0) return b;
b %= a;
}
};
doAfterLoad(() => {
const numeratorInput = $("#numerator");
const denominatorInput = $("#denominator");
const outputField = $("#out");
/**
* Returns `undefined` if the inputs are valid, or a tuple consisting of the invalid element and an explanation
* of its invalidity otherwise.
*
* @param numerator {string} the numerator value
* @param denominator {string} the denominator value
* @returns {(HTMLElement|string)[]|undefined} `undefined` if the inputs are valid, or a tuple consisting of the
* invalid element and an explanation of its invalidity otherwise
*/
const validateInputs = (numerator, denominator) => {
if (numerator === "")
return [numeratorInput, ""];
if (denominator === "")
return [denominatorInput, ""];
if (!isInt(numerator))
return [numeratorInput, "Numerator must be an integer."];
if (!isInt(denominator))
return [denominatorInput, "Denominator must be an integer."];
if (+denominator === 0)
return [denominatorInput, "Denominator must not be 0."];
return undefined;
};
/**
* Reads the inputs and tries to output the simplified fraction.
*/
const outputSimplifiedFraction = () => {
let numerator = numeratorInput.value;
let denominator = denominatorInput.value;
const validationInfo = validateInputs(numerator, denominator);
if (validationInfo !== undefined) {
outputField.innerText = validationInfo[1];
return;
}
const fraction = new Fraction(numeratorInput.value, denominatorInput.value);
outputField.innerText = "$$" + fraction.toString() + " = " + fraction.simplify().toReducedString() + "$$";
MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
};
numeratorInput.addEventListener("input", () => outputSimplifiedFraction());
denominatorInput.addEventListener("input", () => outputSimplifiedFraction());
});
</script>
</body>
</html>

BIN
package-lock.json generated Normal file

Binary file not shown.

37
package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "simplify-fractions",
"version": "v1.2.0",
"description": "Simple web tool for simplifying fractions",
"author": "Felix W. Dekker",
"browser": "dist/bundle.js",
"repository": {
"type": "git",
"url": "git@git.fwdekker.com:FWDekker/simplify-fractions.git"
},
"private": true,
"scripts": {
"clean": "grunt clean",
"dev": "grunt dev",
"dev:server": "grunt dev:server",
"deploy": "grunt deploy"
},
"dependencies": {
"@fwdekker/template": "^0.0.22",
"katex": "^0.13.0"
},
"devDependencies": {
"css-loader": "^5.2.0",
"file-loader": "^6.2.0",
"grunt": "^1.3.0",
"grunt-cli": "^1.4.1",
"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": "^4.0.2",
"style-loader": "^2.0.0",
"webpack": "^5.28.0",
"webpack-cli": "^4.5.0"
}
}

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

@ -0,0 +1,52 @@
<!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="Simplify fractions" />
<meta name="description" content="Simplify fractions." />
<meta name="theme-color" content="#0033cc" />
<title>Simplify fractions | 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">
<form>
<fieldset>
<label for="numerator">Numerator</label>
<input type="number" id="numerator" min="-2147483647" max="2147483647" autofocus />
<label for="denominator">Denominator</label>
<input type="number" id="denominator" min="-2147483647" max="2147483647" />
</fieldset>
</form>
</section>
<!-- Output -->
<section class="container">
<span id="out"></span>
</section>
</div>
<div id="footer"></div>
</main>
<script src="bundle.js?v=%%VERSION_NUMBER%%"></script>
</body>
</html>

174
src/main/js/Main.js Normal file
View File

@ -0,0 +1,174 @@
import {$, doAfterLoad, footer, header, nav, showPage} from "@fwdekker/template";
import katex from "katex";
import "katex/dist/katex.min.css";
/**
* A fraction that can be simplified.
*/
class Fraction {
constructor(numerator, denominator) {
if (!isInt(numerator) || !isInt(denominator))
throw new Error("Numerator and denominator must be integer-like.");
this.sign = numerator < 0 !== denominator < 0 ? -1 : 1;
this.numerator = Math.abs(+numerator);
this.denominator = Math.abs(+denominator);
}
/**
* Returns a new fraction such that the gcd of the numerator and denominator is 1.
*
* @returns {Fraction} a new fraction such that the gcd of the numerator and denominator is 1
*/
simplify() {
const common = gcd(this.numerator, this.denominator);
return new Fraction(this.sign * this.numerator / common, this.denominator / common);
}
/**
* Returns the LaTeX string representation of this fraction.
*
* @returns {string} the LaTeX string representation of this fraction
*/
toString() {
let frac = `\\frac{${this.numerator}}{${this.denominator}}`;
if (this.sign === -1)
frac = `-${frac}`;
return frac;
}
/**
* Returns the LaTeX string representation of this fraction, or of the numerator if the denominator is 1.
*
* @returns {string} the LaTeX string representation of this fraction, or of the numerator if the denominator
* is 1.
*/
toReducedString() {
if (this.numerator === 0)
return "0";
let frac;
if (this.denominator === 1)
frac = `${this.numerator}`;
else
frac = `\\frac{${this.numerator}}{${this.denominator}}`;
if (this.sign === -1)
frac = `-${frac}`;
return frac;
}
}
// noinspection EqualityComparisonWithCoercionJS
/**
* Returns `true` if and only if `n` is an integer.
*
* @param n {*} the value to check for integerness
* @returns {boolean} `true` if and only if `n` is an integer
*/
const isInt = n => n == parseInt(n);
/**
* Returns the greatest common divisor of `a` and `b`.
*
* @param a {number} the first operand
* @param b {number} the second operand
* @returns {number} the greatest common divisor of `a` and `b`
*/
const gcd = (a, b) => {
if (b > a) {
const temp = a;
a = b;
b = temp;
}
while (true) {
if (b === 0) return a;
a %= b;
if (a === 0) return b;
b %= a;
}
};
doAfterLoad(() => {
$("#nav").appendChild(nav("/Tools/Simplify Fractions/"));
$("#header").appendChild(header({
title: "Simplify Fractions",
description: "Simple web tool for simplifying fractions"
}));
$("#footer").appendChild(footer({
author: "Felix W. Dekker",
authorURL: "https://fwdekker.com/",
license: "MIT License",
licenseURL: "https://git.fwdekker.com/FWDekker/simplify-fractions/src/branch/master/LICENSE",
vcs: "git",
vcsURL: "https://git.fwdekker.com/FWDekker/simplify-fractions/",
version: "v%%VERSION_NUMBER%%"
}));
showPage();
});
doAfterLoad(() => {
const numeratorInput = $("#numerator");
const denominatorInput = $("#denominator");
const outputField = $("#out");
/**
* Returns `undefined` if the inputs are valid, or a tuple consisting of the invalid element and an explanation
* of its invalidity otherwise.
*
* @param numerator {string} the numerator value
* @param denominator {string} the denominator value
* @returns {(HTMLElement|string)[]|undefined} `undefined` if the inputs are valid, or a tuple consisting of the
* invalid element and an explanation of its invalidity otherwise
*/
const validateInputs = (numerator, denominator) => {
if (numerator === "")
return [numeratorInput, ""];
if (denominator === "")
return [denominatorInput, ""];
if (!isInt(numerator))
return [numeratorInput, "Numerator must be an integer."];
if (!isInt(denominator))
return [denominatorInput, "Denominator must be an integer."];
if (+denominator === 0)
return [denominatorInput, "Denominator must not be 0."];
return undefined;
};
/**
* Reads the inputs and tries to output the simplified fraction.
*/
const outputSimplifiedFraction = () => {
let numerator = numeratorInput.value;
let denominator = denominatorInput.value;
const validationInfo = validateInputs(numerator, denominator);
if (validationInfo !== undefined) {
outputField.innerText = validationInfo[1];
return;
}
const fraction = new Fraction(numeratorInput.value, denominatorInput.value);
outputField.innerHTML = katex.renderToString(
fraction.toString() + " = " + fraction.simplify().toReducedString(),
{
displayMode: true,
throwOnError: false
}
);
};
numeratorInput.addEventListener("input", () => outputSimplifiedFraction());
denominatorInput.addEventListener("input", () => outputSimplifiedFraction());
});