Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f4188cfaa7 | |||
| 2f7e26b02b | |||
| b609b3acf7 | |||
| 943a34935f | |||
| 0e147413eb | |||
| 611389d112 | |||
| 79258d4611 | |||
| 15684f9cb9 | |||
| 831a00b703 | |||
| d47b958fc3 | |||
| 7a5f333f48 | |||
| 3236e6e53d | |||
| 0b3aa5633f | |||
| a79bd4a862 | |||
| 084b5c665c | |||
| bdc3c33026 | |||
| b12948bbef | |||
| 2a99632a3b | |||
| 96dc069a34 | |||
| 9a8b6337a2 | |||
| c48d02d03b | |||
| 1ddf7cc2a7 |
@@ -0,0 +1,193 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ha.mr - link compressor</title>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Hammersmith+One&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 120%;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
font-family: "Hammersmith One", sans-serif;
|
||||||
|
}
|
||||||
|
#loader {
|
||||||
|
position: absolute;
|
||||||
|
border: 0.5em solid lightgray;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 0.5em solid gray;
|
||||||
|
width: 3em;
|
||||||
|
height: 3em;
|
||||||
|
-webkit-animation: spin 1.5s linear infinite;
|
||||||
|
animation: spin 1.5s linear infinite;
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 1s;
|
||||||
|
}
|
||||||
|
@-webkit-keyframes spin {
|
||||||
|
0% { -webkit-transform: rotate(0deg); }
|
||||||
|
100% { -webkit-transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
#content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 1s;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: rgb(50, 100, 255);
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 2.5em;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
font-size: 0.6em;
|
||||||
|
margin-top: -10px;
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.source-link {
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
|
#input-link {
|
||||||
|
padding: 5px 8px;
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
border-bottom: 2px solid light-dark(
|
||||||
|
rgba(0, 0, 0, 0.2),
|
||||||
|
rgba(255, 255, 255, 0.2)
|
||||||
|
);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: light-dark(
|
||||||
|
rgba(0, 0, 0, 0.1),
|
||||||
|
rgba(255, 255, 255, 0.1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#output-link-container {
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#output-link {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
#output-ratio {
|
||||||
|
transition: color 0.5s;
|
||||||
|
color: gray;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
#qrcode {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#qr-correct-level-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#query-warning {
|
||||||
|
display: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: justify;
|
||||||
|
text-align-last: center;
|
||||||
|
max-width: 80vmin;
|
||||||
|
background-color: gold;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 5px 0 15px 0;
|
||||||
|
}
|
||||||
|
#query-warning summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="/qrcode.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="loader"></div>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<a class="source-link" href="https://github.com/p2r3/ha.mr" target="_blank">Source code on GitHub</a>
|
||||||
|
|
|
||||||
|
<a class="source-link" href="https://youtu.be/TOr1Vvji6jA" target="_blank">Explanation video</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
|
||||||
|
<h1 class="title">ha.mr</h1>
|
||||||
|
<p class="subtitle">(read: "hammer")</p>
|
||||||
|
|
||||||
|
<input type="text" id="input-link" placeholder="https://some-long.link/" autofocus>
|
||||||
|
<details id="query-warning">
|
||||||
|
<summary>Consider removing parameters...</summary>
|
||||||
|
<br>
|
||||||
|
Some links include data that isn't necessary for the link to function,
|
||||||
|
such as tracking information or page state. This data comes in the
|
||||||
|
form of <i>query parameters</i>, marked by "?" and "&" characters.
|
||||||
|
<br><br>
|
||||||
|
Removing these segments can sometimes significantly shorten the link
|
||||||
|
without breaking it. For example, an Amazon product link contains a lot
|
||||||
|
of tracking information. Everything past the "?" symbol can be safely
|
||||||
|
removed, making the link much shorter.
|
||||||
|
</details>
|
||||||
|
<p id="output-link-container">
|
||||||
|
<a id="output-link" target="_blank">Enter a link above to compress</a>
|
||||||
|
<br><span id="output-ratio"> </span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<image width="256" height="256" id="qrcode"></image>
|
||||||
|
|
||||||
|
<span id="qr-correct-level-container">
|
||||||
|
<label>QR code error correction threshold</label>
|
||||||
|
<br>
|
||||||
|
<input type="range" name="qr-correct-level" id="qr-correct-level" autocomplete="off" min="0" max="3" value="1">
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span style="margin-top:20px">
|
||||||
|
<input type="checkbox" id="settings-emoji" name="settings-emoji" autocomplete="off">
|
||||||
|
<label for="settings-emoji"> Use emoji in text output</label><br>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<input type="checkbox" id="settings-qr" name="settings-qr" autocomplete="off">
|
||||||
|
<label for="settings-qr"> Output QR code</label><br>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY index.html 404.html /usr/share/nginx/html/
|
||||||
|
COPY alphabets.js compress.js main.js qrcode.js standalone.js /usr/share/nginx/html/
|
||||||
@@ -10,3 +10,5 @@ Compresses links and optimizes QR codes entirely in the browser, without a back-
|
|||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
- https://www.npmjs.com/package/qrcode
|
- https://www.npmjs.com/package/qrcode
|
||||||
|
- https://github.com/smythp/reddit_links_dataset
|
||||||
|
- https://github.com/ada-url/url-dataset
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
+34
-9
@@ -245,9 +245,9 @@ export function compress (input, alphabet) {
|
|||||||
queryParamIndex ++;
|
queryParamIndex ++;
|
||||||
}
|
}
|
||||||
// Look for smallest subalphabet that fits this path segment
|
// Look for smallest subalphabet that fits this path segment
|
||||||
let subalphabetIndex = subalphabets.length - 1;
|
let subalphabetIndex = -1;
|
||||||
let subalphabet = subalphabets[subalphabetIndex];
|
let subalphabet = null;
|
||||||
for (let i = 0; i < subalphabets.length - 1; i ++) {
|
for (let i = 0; i < subalphabets.length; i ++) {
|
||||||
if (!Array.from(segment.value).some(c => !subalphabets[i].includes(c))) {
|
if (!Array.from(segment.value).some(c => !subalphabets[i].includes(c))) {
|
||||||
subalphabet = subalphabets[i];
|
subalphabet = subalphabets[i];
|
||||||
subalphabetIndex = i;
|
subalphabetIndex = i;
|
||||||
@@ -264,12 +264,32 @@ export function compress (input, alphabet) {
|
|||||||
huffmanNumber = huffmanEncode(huffmanNumber, pathEncode["%"]);
|
huffmanNumber = huffmanEncode(huffmanNumber, pathEncode["%"]);
|
||||||
i -= 2;
|
i -= 2;
|
||||||
} else {
|
} else {
|
||||||
huffmanNumber = huffmanEncode(huffmanNumber, pathEncode[segment.value[i]]);
|
if (segment.value[i] === "~") {
|
||||||
|
/**
|
||||||
|
* HACK HACK HACK!!!
|
||||||
|
* Our Huffman tree is missing the tilde character (whoops!)
|
||||||
|
* It's too late to change it now without bumping the version
|
||||||
|
* number, and that currently costs 1 bit. Tildes are so rare
|
||||||
|
* that it makes more sense to %-encode them instead.
|
||||||
|
*/
|
||||||
|
huffmanNumber *= 256n;
|
||||||
|
huffmanNumber += BigInt(126);
|
||||||
|
huffmanNumber = huffmanEncode(huffmanNumber, pathEncode["%"]);
|
||||||
|
} else {
|
||||||
|
huffmanNumber = huffmanEncode(huffmanNumber, pathEncode[segment.value[i]]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Encode segment variant as 0
|
// Encode segment variant as 0
|
||||||
// (We're adding +1 here to introduce 0 as a special value indicating Huffman)
|
// (We're adding +1 here to introduce 0 as a special value indicating Huffman)
|
||||||
huffmanNumber *= BigInt(subalphabets.length + 1);
|
huffmanNumber *= BigInt(subalphabets.length + 1);
|
||||||
|
// If no subalphabet fits this segment, Huffman is the only option.
|
||||||
|
// Encoding a character missing from the subalphabet would produce the
|
||||||
|
// value 0, which the decoder treats as the end of the segment.
|
||||||
|
if (!subalphabet) {
|
||||||
|
number = huffmanNumber;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// Compute number after encoding with chosen subalphabet
|
// Compute number after encoding with chosen subalphabet
|
||||||
const subalphabetLength = BigInt(subalphabet.length + 1);
|
const subalphabetLength = BigInt(subalphabet.length + 1);
|
||||||
let subalphabetNumber = firstIteration ? number : number * subalphabetLength;
|
let subalphabetNumber = firstIteration ? number : number * subalphabetLength;
|
||||||
@@ -301,7 +321,7 @@ export function compress (input, alphabet) {
|
|||||||
// Encode either SLD + subdomain or full hostname
|
// Encode either SLD + subdomain or full hostname
|
||||||
if (!knownSLD) {
|
if (!knownSLD) {
|
||||||
// Write stopping token only if path follows
|
// Write stopping token only if path follows
|
||||||
if (path || search) number = huffmanEncode(number, domainEncode["END"]);
|
if (pathSegments.length > 0) number = huffmanEncode(number, domainEncode["END"]);
|
||||||
for (let i = hostname.length - 1; i >= 0; i --) {
|
for (let i = hostname.length - 1; i >= 0; i --) {
|
||||||
number = huffmanEncode(number, domainEncode[hostname[i]]);
|
number = huffmanEncode(number, domainEncode[hostname[i]]);
|
||||||
}
|
}
|
||||||
@@ -309,7 +329,7 @@ export function compress (input, alphabet) {
|
|||||||
// Encode subdomain
|
// Encode subdomain
|
||||||
if (subdomain) {
|
if (subdomain) {
|
||||||
// Write stopping token only if path follows
|
// Write stopping token only if path follows
|
||||||
if (path || search) number = huffmanEncode(number, domainEncode["END"]);
|
if (pathSegments.length > 0) number = huffmanEncode(number, domainEncode["END"]);
|
||||||
for (let i = subdomain.length - 1; i >= 0; i--) {
|
for (let i = subdomain.length - 1; i >= 0; i--) {
|
||||||
number = huffmanEncode(number, domainEncode[subdomain[i]]);
|
number = huffmanEncode(number, domainEncode[subdomain[i]]);
|
||||||
}
|
}
|
||||||
@@ -473,7 +493,7 @@ export function decompress (input, alphabet) {
|
|||||||
path += digit;
|
path += digit;
|
||||||
if (digit === "%") {
|
if (digit === "%") {
|
||||||
const byte = number % 256n;
|
const byte = number % 256n;
|
||||||
path += byte.toString(16);
|
path += byte.toString(16).padStart(2, "0");
|
||||||
number /= 256n;
|
number /= 256n;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -506,6 +526,10 @@ export function decompress (input, alphabet) {
|
|||||||
number >>= 1n;
|
number >>= 1n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pathSplitIndex = path.search(/[?#]/);
|
||||||
|
const pathBeforeQuery = pathSplitIndex === -1 ? path : path.slice(0, pathSplitIndex);
|
||||||
|
const pathFromQuery = pathSplitIndex === -1 ? "" : path.slice(pathSplitIndex);
|
||||||
|
|
||||||
let output = ""
|
let output = ""
|
||||||
+ (isHTTPS ? "https://" : "http://")
|
+ (isHTTPS ? "https://" : "http://")
|
||||||
+ (hasWWW ? "www." : "")
|
+ (hasWWW ? "www." : "")
|
||||||
@@ -513,8 +537,9 @@ export function decompress (input, alphabet) {
|
|||||||
+ domain
|
+ domain
|
||||||
+ (tld ? "." + tld : "")
|
+ (tld ? "." + tld : "")
|
||||||
+ (hasPort ? ":" + port : "")
|
+ (hasPort ? ":" + port : "")
|
||||||
+ path
|
+ pathBeforeQuery
|
||||||
+ indexSuffix;
|
+ indexSuffix
|
||||||
|
+ pathFromQuery;
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-5
@@ -6,6 +6,11 @@
|
|||||||
<title>ha.mr - link compressor</title>
|
<title>ha.mr - link compressor</title>
|
||||||
<style>
|
<style>
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Hammersmith+One&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Hammersmith+One&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -61,6 +66,9 @@
|
|||||||
span {
|
span {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
a {
|
||||||
|
color: rgb(50, 100, 255);
|
||||||
|
}
|
||||||
.title {
|
.title {
|
||||||
font-size: 2.5em;
|
font-size: 2.5em;
|
||||||
}
|
}
|
||||||
@@ -70,22 +78,30 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
.source-link {
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
#input-link {
|
#input-link {
|
||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
border: 0;
|
border: 0;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
border-bottom: 2px solid darkgray;
|
border-bottom: 2px solid light-dark(
|
||||||
|
rgba(0, 0, 0, 0.2),
|
||||||
|
rgba(255, 255, 255, 0.2)
|
||||||
|
);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background-color: rgb(230, 230, 230);
|
background-color: light-dark(
|
||||||
|
rgba(0, 0, 0, 0.1),
|
||||||
|
rgba(255, 255, 255, 0.1)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
#output-link-container {
|
#output-link-container {
|
||||||
margin: 10px 0 0 0;
|
margin: 10px 0 0 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
#output-link {
|
#output-link {
|
||||||
color: rgb(50, 100, 255);
|
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
#output-ratio {
|
#output-ratio {
|
||||||
@@ -113,6 +129,10 @@
|
|||||||
#query-warning summary {
|
#query-warning summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
header {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="/qrcode.js"></script>
|
<script src="/qrcode.js"></script>
|
||||||
</head>
|
</head>
|
||||||
@@ -120,6 +140,12 @@
|
|||||||
|
|
||||||
<div id="loader"></div>
|
<div id="loader"></div>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<a class="source-link" href="https://github.com/p2r3/ha.mr" target="_blank">Source code on GitHub</a>
|
||||||
|
|
|
||||||
|
<a class="source-link" href="https://youtu.be/TOr1Vvji6jA" target="_blank">Explanation video</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div id="content">
|
<div id="content">
|
||||||
|
|
||||||
<h1 class="title">ha.mr</h1>
|
<h1 class="title">ha.mr</h1>
|
||||||
@@ -159,10 +185,9 @@
|
|||||||
<input type="checkbox" id="settings-qr" name="settings-qr" autocomplete="off">
|
<input type="checkbox" id="settings-qr" name="settings-qr" autocomplete="off">
|
||||||
<label for="settings-qr"> Output QR code</label><br>
|
<label for="settings-qr"> Output QR code</label><br>
|
||||||
</span>
|
</span>
|
||||||
<br><br>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/main.js"></script>
|
<script type="module" src="/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -5,6 +5,15 @@ import {
|
|||||||
outputAlphabetEmoji
|
outputAlphabetEmoji
|
||||||
} from "./alphabets.js";
|
} from "./alphabets.js";
|
||||||
|
|
||||||
|
let domain = window.location.hostname;
|
||||||
|
if (domain !== "ha.mr" && domain !== "www.ha.mr") {
|
||||||
|
console.log(`This page is intended to be used on the ha.mr domain. You are currently on ${domain}.`);
|
||||||
|
}
|
||||||
|
const webPort = window.location.port;
|
||||||
|
if (webPort && webPort !== "80" && webPort !== "443") {
|
||||||
|
domain += `:${webPort}`;
|
||||||
|
}
|
||||||
|
|
||||||
var settings = {
|
var settings = {
|
||||||
emoji: false,
|
emoji: false,
|
||||||
qr: false
|
qr: false
|
||||||
@@ -42,7 +51,41 @@ const queryWarningElement = document.querySelector("#query-warning");
|
|||||||
const qrCodeImage = document.querySelector("#qrcode");
|
const qrCodeImage = document.querySelector("#qrcode");
|
||||||
const qrCodeCorrectionLevelContainer = document.querySelector("#qr-correct-level-container");
|
const qrCodeCorrectionLevelContainer = document.querySelector("#qr-correct-level-container");
|
||||||
const qrCodeCorrectionLevelElement = document.querySelector("#qr-correct-level");
|
const qrCodeCorrectionLevelElement = document.querySelector("#qr-correct-level");
|
||||||
qrCodeCorrectionLevelElement.addEventListener("change", updateOutput);
|
|
||||||
|
let qrCorrectionManuallySet = false;
|
||||||
|
|
||||||
|
qrCodeCorrectionLevelElement.addEventListener("change", () => {
|
||||||
|
qrCorrectionManuallySet = true;
|
||||||
|
updateOutput();
|
||||||
|
});
|
||||||
|
|
||||||
|
function getOptimalErrorCorrectionLevel (text) {
|
||||||
|
const levels = ["M", "Q", "H"];
|
||||||
|
|
||||||
|
const baseVersion = QRCode.create(text, {
|
||||||
|
errorCorrectionLevel: levels[0]
|
||||||
|
}).version;
|
||||||
|
|
||||||
|
let optimalLevel = levels[0];
|
||||||
|
|
||||||
|
for (const level of levels.slice(1)) {
|
||||||
|
try {
|
||||||
|
const candidate = QRCode.create(text, {
|
||||||
|
errorCorrectionLevel: level
|
||||||
|
});
|
||||||
|
|
||||||
|
if (candidate.version > baseVersion) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
optimalLevel = level;
|
||||||
|
} catch {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return optimalLevel;
|
||||||
|
}
|
||||||
|
|
||||||
function updateOutput () {
|
function updateOutput () {
|
||||||
const input = inputLinkElement.value.trim();
|
const input = inputLinkElement.value.trim();
|
||||||
@@ -50,9 +93,10 @@ function updateOutput () {
|
|||||||
const alphabet = settings.emoji ? outputAlphabetEmoji : outputAlphabetASCII;
|
const alphabet = settings.emoji ? outputAlphabetEmoji : outputAlphabetASCII;
|
||||||
const output = compress(input, alphabet);
|
const output = compress(input, alphabet);
|
||||||
let inputNormalized = input;
|
let inputNormalized = input;
|
||||||
if (input.startsWith("https://")) {
|
const inputLower = input.toLowerCase();
|
||||||
|
if (inputLower.startsWith("https://")) {
|
||||||
inputNormalized = input.slice(8);
|
inputNormalized = input.slice(8);
|
||||||
} else if (input.startsWith("http://")) {
|
} else if (inputLower.startsWith("http://")) {
|
||||||
inputNormalized = input.slice(7);
|
inputNormalized = input.slice(7);
|
||||||
}
|
}
|
||||||
let excessiveParams = false;
|
let excessiveParams = false;
|
||||||
@@ -81,14 +125,26 @@ function updateOutput () {
|
|||||||
outputRatioElement.textContent = "Output is the same length as the input";
|
outputRatioElement.textContent = "Output is the same length as the input";
|
||||||
outputRatioElement.style.color = "gray";
|
outputRatioElement.style.color = "gray";
|
||||||
}
|
}
|
||||||
outputLinkElement.textContent = `http://ha.mr#${output}`;
|
outputLinkElement.textContent = `http://${domain}#${output}`;
|
||||||
outputLinkElement.href = `http://ha.mr#${output}`;
|
outputLinkElement.href = `http://${domain}#${output}`;
|
||||||
outputLinkElement.style.color = "";
|
outputLinkElement.style.color = "";
|
||||||
if (settings.qr) {
|
if (settings.qr) {
|
||||||
const errorCorrection = ["L", "M", "Q", "H"][qrCodeCorrectionLevelElement.value];
|
const correctionLevels = ["L", "M", "Q", "H"];
|
||||||
|
|
||||||
qrCodeImage.style.display = "inline";
|
qrCodeImage.style.display = "inline";
|
||||||
qrCodeCorrectionLevelContainer.style.display = "inline";
|
qrCodeCorrectionLevelContainer.style.display = "inline";
|
||||||
let qrCodeLink = `HTTP://HA.MR/${compress(input, outputAlphabetQR)}`;
|
|
||||||
|
const qrCodeDomain = domain.toUpperCase();
|
||||||
|
const qrCodeLink = `HTTP://${qrCodeDomain}/${compress(input, outputAlphabetQR)}`;
|
||||||
|
|
||||||
|
if (!qrCorrectionManuallySet) {
|
||||||
|
const optimalLevel = getOptimalErrorCorrectionLevel(qrCodeLink);
|
||||||
|
qrCodeCorrectionLevelElement.value = correctionLevels.indexOf(optimalLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorCorrection =
|
||||||
|
correctionLevels[qrCodeCorrectionLevelElement.value];
|
||||||
|
|
||||||
QRCode.toDataURL(qrCodeLink, {
|
QRCode.toDataURL(qrCodeLink, {
|
||||||
errorCorrectionLevel: errorCorrection,
|
errorCorrectionLevel: errorCorrection,
|
||||||
scale: 8
|
scale: 8
|
||||||
@@ -120,7 +176,11 @@ function updateOutput () {
|
|||||||
queryWarningElement.style.display = "none";
|
queryWarningElement.style.display = "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inputLinkElement.addEventListener("input", updateOutput);
|
|
||||||
|
inputLinkElement.addEventListener("input", () => {
|
||||||
|
qrCorrectionManuallySet = false;
|
||||||
|
updateOutput();
|
||||||
|
});
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
let payload = null;
|
let payload = null;
|
||||||
|
|||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /404.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-2
@@ -7,8 +7,10 @@ import {
|
|||||||
|
|
||||||
const input = process.argv[2]?.trim();
|
const input = process.argv[2]?.trim();
|
||||||
const alphabetName = process.argv[3]?.trim() || "ascii";
|
const alphabetName = process.argv[3]?.trim() || "ascii";
|
||||||
|
const command = process.argv[4]?.trim() || "encode";
|
||||||
if (!input) {
|
if (!input) {
|
||||||
console.error(`Usage: hamr <link> [ascii|qr|emoji]`);
|
console.error(`Usage: hamr <link> [ascii|qr|emoji] {decode|encode}`);
|
||||||
|
console.error(`The final argument is optional and defaults to "encode".`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,9 +21,13 @@ if (input.toLowerCase().startsWith("http://ha.mr")) {
|
|||||||
payload = input.slice(13);
|
payload = input.slice(13);
|
||||||
} else if (input.toLowerCase().startsWith("ha.mr")) {
|
} else if (input.toLowerCase().startsWith("ha.mr")) {
|
||||||
payload = input.slice(5);
|
payload = input.slice(5);
|
||||||
|
} else if (command === "decode") {
|
||||||
|
const pos = input.indexOf("#");
|
||||||
|
payload = input.slice(pos);
|
||||||
|
console.log(`Payload: ${payload}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload) {
|
if (payload || command === "decode") {
|
||||||
const isQRCode = input[0] === "/";
|
const isQRCode = input[0] === "/";
|
||||||
payload = payload.slice(1);
|
payload = payload.slice(1);
|
||||||
const useEmoji = Array.from(payload).some(c => !outputAlphabetASCII.includes(c));
|
const useEmoji = Array.from(payload).some(c => !outputAlphabetASCII.includes(c));
|
||||||
|
|||||||
Reference in New Issue
Block a user