initial program commit
This commit is contained in:
6 files changed
+900
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
export const outputAlphabetASCII = "!#$&'()*+,-./0123456789:;=?~@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]_abcdefghijklmnopqrstuvwxyz".split("");
|
||||||
|
export const outputAlphabetQR = "$*+-./:0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||||
|
export const outputAlphabetEmoji = ["👨🏻❤️💋👨🏻","👨🏻❤️💋👨🏼","👨🏻❤️💋👨🏽","👨🏻❤️💋👨🏾","👨🏻❤️💋👨🏿","👨🏼❤️💋👨🏻","👨🏼❤️💋👨🏼","👨🏼❤️💋👨🏽","👨🏼❤️💋👨🏾","👨🏼❤️💋👨🏿","👨🏽❤️💋👨🏻","👨🏽❤️💋👨🏼","👨🏽❤️💋👨🏽","👨🏽❤️💋👨🏾","👨🏽❤️💋👨🏿","👨🏾❤️💋👨🏻","👨🏾❤️💋👨🏼","👨🏾❤️💋👨🏽","👨🏾❤️💋👨🏾","👨🏾❤️💋👨🏿","👨🏿❤️💋👨🏻","👨🏿❤️💋👨🏼","👨🏿❤️💋👨🏽","👨🏿❤️💋👨🏾","👨🏿❤️💋👨🏿","👩🏻❤️💋👨🏻","👩🏻❤️💋👨🏼","👩🏻❤️💋👨🏽","👩🏻❤️💋👨🏾","👩🏻❤️💋👨🏿","👩🏻❤️💋👩🏻","👩🏻❤️💋👩🏼","👩🏻❤️💋👩🏽","👩🏻❤️💋👩🏾","👩🏻❤️💋👩🏿","👩🏼❤️💋👨🏻","👩🏼❤️💋👨🏼","👩🏼❤️💋👨🏽","👩🏼❤️💋👨🏾","👩🏼❤️💋👨🏿","👩🏼❤️💋👩🏻","👩🏼❤️💋👩🏼","👩🏼❤️💋👩🏽","👩🏼❤️💋👩🏾","👩🏼❤️💋👩🏿","👩🏽❤️💋👨🏻","👩🏽❤️💋👨🏼","👩🏽❤️💋👨🏽","👩🏽❤️💋👨🏾","👩🏽❤️💋👨🏿","👩🏽❤️💋👩🏻","👩🏽❤️💋👩🏼","👩🏽❤️💋👩🏽","👩🏽❤️💋👩🏾","👩🏽❤️💋👩🏿","👩🏾❤️💋👨🏻","👩🏾❤️💋👨🏼","👩🏾❤️💋👨🏽","👩🏾❤️💋👨🏾","👩🏾❤️💋👨🏿","👩🏾❤️💋👩🏻","👩🏾❤️💋👩🏼","👩🏾❤️💋👩🏽","👩🏾❤️💋👩🏾","👩🏾❤️💋👩🏿","👩🏿❤️💋👨🏻","👩🏿❤️💋👨🏼","👩🏿❤️💋👨🏽","👩🏿❤️💋👨🏾","👩🏿❤️💋👨🏿","👩🏿❤️💋👩🏻","👩🏿❤️💋👩🏼","👩🏿❤️💋👩🏽","👩🏿❤️💋👩🏾","👩🏿❤️💋👩🏿","🧑🏻❤️💋🧑🏼","🧑🏻❤️💋🧑🏽","🧑🏻❤️💋🧑🏾","🧑🏻❤️💋🧑🏿","🧑🏼❤️💋🧑🏻","🧑🏼❤️💋🧑🏽","🧑🏼❤️💋🧑🏾","🧑🏼❤️💋🧑🏿","🧑🏽❤️💋🧑🏻","🧑🏽❤️💋🧑🏼","🧑🏽❤️💋🧑🏾","🧑🏽❤️💋🧑🏿","🧑🏾❤️💋🧑🏻","🧑🏾❤️💋🧑🏼","🧑🏾❤️💋🧑🏽","🧑🏾❤️💋🧑🏿","🧑🏿❤️💋🧑🏻","🧑🏿❤️💋🧑🏼","🧑🏿❤️💋🧑🏽","🧑🏿❤️💋🧑🏾","🏴","🏴","🏴","👨🏻❤️👨🏻","👨🏻❤️👨🏼","👨🏻❤️👨🏽","👨🏻❤️👨🏾","👨🏻❤️👨🏿","👨🏻🐰👨🏼","👨🏻🐰👨🏽","👨🏻🐰👨🏾","👨🏻🐰👨🏿","👨🏻🤝👨🏼","👨🏻🤝👨🏽","👨🏻🤝👨🏾","👨🏻🤝👨🏿","👨🏻👨🏼","👨🏻👨🏽","👨🏻👨🏾","👨🏻👨🏿","👨🏼❤️👨🏻","👨🏼❤️👨🏼","👨🏼❤️👨🏽","👨🏼❤️👨🏾","👨🏼❤️👨🏿","👨🏼🐰👨🏻","👨🏼🐰👨🏽","👨🏼🐰👨🏾","👨🏼🐰👨🏿","👨🏼🤝👨🏻","👨🏼🤝👨🏽","👨🏼🤝👨🏾","👨🏼🤝👨🏿","👨🏼👨🏻","👨🏼👨🏽","👨🏼👨🏾","👨🏼👨🏿","👨🏽❤️👨🏻","👨🏽❤️👨🏼","👨🏽❤️👨🏽","👨🏽❤️👨🏾","👨🏽❤️👨🏿","👨🏽🐰👨🏻","👨🏽🐰👨🏼","👨🏽🐰👨🏾","👨🏽Line truncated
|
||||||
+520
@@ -0,0 +1,520 @@
|
|||||||
|
/**
|
||||||
|
* @file Implements link compression/decompression.
|
||||||
|
*/
|
||||||
|
const VERSION = 0;
|
||||||
|
|
||||||
|
// Growing subcategories of the full URL alphabet
|
||||||
|
// Each also includes the hyphen and underscore as common separators
|
||||||
|
const subalphabets = [
|
||||||
|
// Numbers only
|
||||||
|
"0123456789-_",
|
||||||
|
// Uppercase only
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ-_",
|
||||||
|
// Lowercase only
|
||||||
|
"abcdefghijklmnopqrstuvwxyz-_",
|
||||||
|
// Uppercase and numbers
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_",
|
||||||
|
// Lowercase and numbers
|
||||||
|
"abcdefghijklmnopqrstuvwxyz0123456789-_",
|
||||||
|
// Uppercase and lowercase
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_",
|
||||||
|
// Upercase, lowercase and numbers (base64)
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
|
||||||
|
// Full alphabet without slash character
|
||||||
|
"!#$&'()*+,-.0123456789:;=?~@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]_abcdefghijklmnopqrstuvwxyz%"
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Huffman-coded dictionaries of URL characters/parts.
|
||||||
|
* Built using data from:
|
||||||
|
* - https://github.com/smythp/reddit_links_dataset
|
||||||
|
* - https://github.com/ada-url/url-dataset
|
||||||
|
*/
|
||||||
|
const tldEncode = {"us":"0000000","market":"0000001000","xyz":"0000001001","lt":"0000001010000","ae":"0000001010001","tt":"0000001010010","top":"0000001010011","hr":"000000101010","biz":"000000101011","es":"0000001011","wtf":"00000011","ceo":"000001000000000000","cbre":"000001000000000001","cfa":"000001000000000010","cern":"000001000000000011","catholic":"000001000000000100","catering":"000001000000000101","cbn":"000001000000000110","cba":"000001000000000111","chase":"000001000000001000","charity":"000001000000001001","chintai":"000001000000001010","cheap":"000001000000001011","cg":"000001000000001100","cfd":"000001000000001101","channel":"000001000000001110","chanel":"000001000000001111","canon":"000001000000010000","cam":"000001000000010001","capitalone":"000001000000010010","capetown":"000001000000010011","cal":"000001000000010100","cab":"000001000000010101","calvinklein":"000001000000010110","call":"000001000000010111","cars":"000001000000011000","careers":"000001000000011001","casino":"000001000000011010","case":"000001000000011011","caravan":"000001000000011100","car":"000001000000011101","career":"000001000000011110","care":"000001000000011111","comsec":"000001000000100000","compare":"000001000000100001","construction":"000001000000100010","condos":"000001000000100011","college":"000001000000100100","coffee":"000001000000100101","commbank":"000001000000100110","cologne":"000001000000100111","country":"000001000000101000","corsica":"000001000000101001","coupons":"000001000000101010","coupon":"000001000000101011","contact":"000001000000101100","consulting":"000001000000101101","cooking":"000001000000101110","contractors":"000001000000101111","citadel":"000001000000110000","cisco":"000001000000110001","citic":"000001000000110010","citi":"000001000000110011","chrome":"000001000000110100","christmas":"000001000000110101","circle":"000001000000110110","cipriani":"000001000000110111","clothing":"000001000000111000","clinique":"000001000000111001","coach":"000001000000111010","clubmed":"000001000000111011","claims":"000001000000111100","ck":"000001000000111101","clinic":"000001000000111110","cleaning":"000001000000111111","bcn":"000001000001000000","bcg":"000001000001000001","beauty":"000001000001000010","beats":"000001000001000011","bbc":"000001000001000100","bb":"000001000001000101","bbva":"000001000001000110","bbt":"000001000001000111","bible":"000001000001001000","bharti":"000001000001001001","bio":"000001000001001010","bing":"000001000001001011","bestbuy":"000001000001001100","best":"000001000001001101","bf":"000001000001001110","bet":"000001000001001111","azure":"000001000001010000","axa":"000001000001010001","banamex":"000001000001010010","baidu":"000001000001010011","aw":"000001000001010100","autos":"000001000001010101","ax":"000001000001010110","aws":"000001000001010111","baseball":"000001000001011000","bargains":"000001000001011001","bayern":"000001000001011010","bauhaus":"000001000001011011","barclaycard":"000001000001011100","barcelona":"000001000001011101","barefoot":"000001000001011110","barclays":"000001000001011111","box":"000001000001100000","boutique":"000001000001100001","bridgestone":"000001000001100010","bradesco":"000001000001100011","bosch":"000001000001100100","booking":"000001000001100101","boston":"000001000001100110","bostik":"000001000001100111","buy":"000001000001101000","bt":"000001000001101001","bzh":"000001000001101010","bv":"000001000001101011","broker":"000001000001101100","broadway":"000001000001101101","brussels":"000001000001101110","brother":"000001000001101111","bm":"000001000001110000","bloomberg":"000001000001110001","bmw":"000001000001110010","bms":"000001000001110011","black":"000001000001110100","bj":"000001000001110101","blockbuster":"000001000001110110","blackfriday":"000001000001110111","bond":"000001000001111000","bom":"000001000001111001","book":"000001000001111010","boo":"000001000001111011","boats":"000001000001111100","bnpparibas":"000001000001111101","bofa":"000001000001111110","boehringer":"000001000001111111","financial":"000001000010000000","final":"000001000010000001","firestone":"000001000010000010","fire":"000001000010000011","fidelity":"000001000010000100","ferrero":"000001000010000101","film":"000001000010000110","fido":"000001000010000111","flickr":"000001000010001000","fj":"000001000010001001","flir":"000001000010001010","flights":"000001000010001011","fish":"000001000010001100","firmdale":"000001000010001101","fit":"000001000010001110","fishing":"000001000010001111","fairwinds":"000001000010010000","fage":"000001000010010001","family":"000001000010010010","faith":"000001000010010011","eurovision":"000001000010010100","et":"000001000010010101","extraspace":"000001000010010110","express":"000001000010010111","fedex":"000001000010011000","fast":"000001000010011001","ferrari":"000001000010011010","feedback":"000001000010011011","fans":"000001000010011100","fan":"000001000010011101","fashion":"000001000010011110","farmers":"000001000010011111","gap"Line truncated
|
||||||
|
const sldEncode = {"cydia.saurik":"000000000000","images.wikia":"000000000001","juicedb":"000000000010","imgflip":"000000000011","globalresearch":"0000000001000","csmonitor":"00000000010010","bmw.com":"00000000010011","images.app.goo":"000000000101","docs.microsoft":"000000000110","500px":"000000000111","radd":"0000000010","boardgamegeek":"0000000011","autosedlo":"0000000100","redditinvestigator":"00000001010","sciencedirect":"00000001011","downtown":"00000001100","yugioh.wikia":"00000001101","express.co":"0000000111000","shop.nordstrom":"0000000111001","battlelog.battlefield":"0000000111010","asus":"0000000111011","foodnetwork":"0000000111100","sfgate":"0000000111101","developer.mozilla":"0000000111110","9gag":"0000000111111","weeklysift":"0000001000000","rbeertrade":"0000001000001","podcasts.apple":"000000100001","bitcoin":"0000001000100","247sports":"0000001000101","zkillboard":"0000001000110","i2.wp":"0000001000111","amazon.co":"0000001001","latimes":"00000010100","botrank.pastimes":"000000101010","a.video.nfl":"000000101011","craigslist":"000000101100","ontheissues":"0000001011010","fimfiction":"0000001011011","reddit.zendesk":"00000010111","edhrec":"00000011","slate":"00000100000","salon":"000001000010","seriouseats":"000001000011","goodreads":"0000010001","artstation":"0000010010","bbc.co":"0000010011","adl":"00000101000000","atf":"00000101000001","play.esea":"00000101000010","mugenmonkey":"00000101000011","w3schools":"00000101000100","radiolab":"00000101000101","harborfreight":"00000101000110","pitchfork":"00000101000111","media-cache-ak0.pinimg":"00000101001000","terraria.gamepedia":"00000101001001","ftw.usatoday":"00000101001010","forums.robertsspaceindustries":"00000101001011","statcan.gc":"00000101001100","wabbitwiki":"00000101001101","encrypted-tbn2.gstatic":"00000101001110","indianexpress":"00000101001111","examiner":"00000101010000","vatican":"00000101010001","techdirt":"00000101010010","wnd":"00000101010011","investigationbooks":"000001010101","marvel":"00000101011000","baltimoresun":"00000101011001","huffingtonpost.co":"00000101011010","torontosun":"00000101011011","ea":"00000101011100","theaustralian.com":"00000101011101","raspberrypi":"00000101011110","travel.state":"00000101011111","lcfgeorgia":"00000101100000","kauffman":"00000101100001","usff.navy":"00000101100010","newsroom.latam.paypal-corp":"00000101100011","yearup":"00000101100100","cvent":"00000101100101","accenture":"00000101100110","switchboardta":"00000101100111","doyok.kinja":"0000010110100","renews24":"0000010110101","yugipedia":"0000010110110","r.go1dfish":"0000010110111","americanexpress":"00000101110000","m.newegg":"00000101110001","feelthebern":"00000101110010","alg.cubing":"00000101110011","corsair":"00000101110100","content.time":"00000101110101","winlivestream":"00000101110110","m.fanfiction":"00000101110111","p2016":"00000101111000","masterypoints":"00000101111001","liquipedia":"00000101111010","help.steampowered":"00000101111011","m.spankbang":"00000101111100","p0ody-files":"00000101111101","pm1.narvii":"00000101111110","watchnfllivetv":"00000101111111","37.media.tumblr":"0000011000000","services.runescape":"0000011000001","voat":"0000011000010","shopify":"0000011000011","i.minus":"00000110001","mangaupdates":"0000011001","drive.google":"000001101","businessinsider":"0000011100","freebase":"000001110100","images-na.ssl-images-amazon":"000001110101","docs.python":"0000011101100","worldofwarcraft":"00000111011010","disposerx":"00000111011011","cdn.sesamestreetincommunities":"000001110111","edition.cnn":"000001111000","wiki.teamfortress":"000001111001","swappa":"000001111010","basketball-reference":"000001111011","dailymotion":"00000111110","researchgate":"000001111110","washingtontimes":"000001111111","twitter":"00001","reddit":"00010","doi":"0001100000","commons.wikimedia":"0001100001","sbnation":"0001100010000","nba":"0001100010001","ec.europa":"0001100010010","yourlogicalfallacyis":"0001100010011","explosm":"0001100010100","dailykos":"0001100010101","theoatmeal":"0001100010110","epa":"0001100010111","disasterphilanthropy":"0001100011000","deltasigmatheta":"0001100011001","goto.walmart":"000110001101","corgiorgy":"0001100011100","mythicspoiler":"0001100011101","connect.ala":"0001100011110","dynamics.microsoft":"0001100011111","docs.google":"00011001","pixiv":"00011010","subtletv":"0001101100","m.reddit":"000110110100","img.pandawhale":"000110110101","mozilla":"000110110110","pewforum":"0001101101110","quran":"0001101101111","combodeck":"000110111","un":"000111000","ca.pcpartpicker":"000111001","addtoany":"00011101","nerdwallet":"00011110000000","myfonts":"00011110000001","crutchfield":"00011110000010","cisco":"00011110000011","state":"00011110000100","windowsphone":"00011110000101","psnprofiles":"00011110000110","dwarffortresswiki":"00011110000111","femradebates":"00011110001000","shevibe":"00011110001001","foxsports":"00011110001010","hqhdtv":"00011110001011","talkingpointsmemo":"00011110001100","technologyreview":Line truncated
|
||||||
|
const domainEncode = {"0":"1100111000","1":"1100111100","2":"1100111101","3":"0010011100","4":"00100111101","5":"11001110010","6":"00100111011","7":"11001110011","8":"00100111010","9":"00100111100","l":"0000","c":"0001","v":"001000","-":"0010010","x":"00100110","q":"0010011111","g":"00101","n":"0011","p":"01000","y":"010010","w":"010011","i":"0101","t":"0110","r":"0111","s":"1000","d":"10010","m":"10011","END":"1010","o":"1011","u":"11000","f":"110010","k":"1100110","z":"110011101","j":"110011111","a":"1101","e":"1110",".":"11110","h":"111110","b":"111111"};
|
||||||
|
const pathEncode = {"0":"011011","1":"011001","2":"1111000","3":"1010011","4":"1010010","5":"1001011","6":"1001010","7":"1000101","8":"1000110","9":"0110001","o":"0000","/":"0001","s":"0010","&":"00110000","R":"00110001","?":"0011001","k":"001101","d":"00111","t":"0100","i":"0101",",":"01100000000","'":"0110000000100","]":"0110000000101","!":"011000000011",":":"0110000001","W":"011000001","q":"01100001","b":"011010","a":"0111","l":"10000","U":"100010000","L":"100010001","A":"10001001",".":"1000111","f":"100100","c":"10011","=":"1010000","G":"101000100","E":"101000101","I":"101000110","F":"101000111","m":"10101","-":"10110","w":"101110","X":"1011110000","+":"1011110001","D":"101111001","Z":"1011110100","*":"10111101010000","$":"1011110101000100",";":"1011110101000101","@":"101111010100011","[":"1011110101001","(":"101111010101",")":"10111101011","P":"101111011","v":"1011111","e":"1100","j":"11010000","O":"110100010","Q":"1101000110","K":"1101000111","y":"1101001","g":"110101","h":"110110","u":"110111","n":"11100","V":"1110100000","Y":"1110100001","B":"111010001","M":"111010010","C":"111010011","x":"11101010","J":"1110101100","#":"1110101101","S":"111010111","_":"111011","z":"111100100","%":"111100101","T":"111100110","H":"1111001110","N":"1111001111","p":"111101","r":"11111"};
|
||||||
|
|
||||||
|
// Create inverted copies of dictionaries for easier decoding
|
||||||
|
const tldDecode = {};
|
||||||
|
for (const key in tldEncode) {
|
||||||
|
tldDecode[tldEncode[key]] = key;
|
||||||
|
}
|
||||||
|
const sldDecode = {};
|
||||||
|
for (const key in sldEncode) {
|
||||||
|
sldDecode[sldEncode[key]] = key;
|
||||||
|
}
|
||||||
|
const sldList = Object.keys(sldEncode).sort((a, b) => b.length - a.length);
|
||||||
|
const domainDecode = {};
|
||||||
|
for (const key in domainEncode) {
|
||||||
|
domainDecode[domainEncode[key]] = key;
|
||||||
|
}
|
||||||
|
const pathDecode = {};
|
||||||
|
for (const key in pathEncode) {
|
||||||
|
pathDecode[pathEncode[key]] = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The following two functions convert between large numbers and strings.
|
||||||
|
* The idea is that, instead of working in a fixed base such as binary or
|
||||||
|
* whatever base the string is in, we can instead manipulate some number
|
||||||
|
* as an arbitrary-precision data stream.
|
||||||
|
*
|
||||||
|
* For example, a value with 3 possible options requires about 1.6 bits.
|
||||||
|
* In binary, that would round up to 2 bits, and in base-85 (for example),
|
||||||
|
* it would round up to a whole character. If we instead use one shared
|
||||||
|
* integer for the whole data stream, we can encode that value by:
|
||||||
|
* 1. multiplying the number by 3; (number *= 3n)
|
||||||
|
* 2. adding the encoded value (0, 1, or 2), (number += value)
|
||||||
|
* and decode it by:
|
||||||
|
* 1. reading the remainder when dividing by 3; (number % 3n)
|
||||||
|
* 2. dividing the number by 3. (number /= 3n)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a number into a string of the given alphabet.
|
||||||
|
* @param {BigInt} number Number to encode
|
||||||
|
* @param {string[]} alphabet Ordered list of possible character sequences in output
|
||||||
|
* @returns {string} String representing the input number
|
||||||
|
*/
|
||||||
|
function numberToString (number, alphabet) {
|
||||||
|
const alphabetSize = BigInt(alphabet.length);
|
||||||
|
let string = "";
|
||||||
|
|
||||||
|
while (number > 0) {
|
||||||
|
number --;
|
||||||
|
string += alphabet[number % alphabetSize];
|
||||||
|
number /= alphabetSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a string of the given alphabet into a number.
|
||||||
|
* @param {string} string Input string containing sequences from `alphabet`
|
||||||
|
* @param {string[]} alphabet Ordered list of possible character sequences in `string`
|
||||||
|
* @returns {BigInt} Decoded number
|
||||||
|
*/
|
||||||
|
function stringToNumber (string, alphabet) {
|
||||||
|
const alphabetSize = BigInt(alphabet.length);
|
||||||
|
let number = 0n;
|
||||||
|
|
||||||
|
// Not all alphabets are 1 byte per character. For example, the emoji
|
||||||
|
// alphabet includes some sequences that only make sense in specific
|
||||||
|
// combinations. To account for this, we assume the alphabets are
|
||||||
|
// ordered with the longest sequences first (they are), and find the
|
||||||
|
// first entry that matches the current position in the string.
|
||||||
|
while (string) {
|
||||||
|
const digit = BigInt(alphabet.findIndex(c => string.endsWith(c)));
|
||||||
|
if (digit < 0n) throw `Invalid character: "${string.at(-1)}"`;
|
||||||
|
number *= alphabetSize;
|
||||||
|
number += digit;
|
||||||
|
number ++;
|
||||||
|
const sequence = alphabet[digit];
|
||||||
|
string = string.slice(0, -sequence.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a binary sequence from a string into the number/data stream.
|
||||||
|
* @param {BigInt} number Data stream to encode to
|
||||||
|
* @param {string} sequence Binary sequence of "0"/"1" string characters
|
||||||
|
* @returns {BigInt} Modified input number with binary sequence encoded
|
||||||
|
*/
|
||||||
|
function huffmanEncode (number, sequence) {
|
||||||
|
for (let i = sequence.length - 1; i >= 0; i --) {
|
||||||
|
number <<= 1n;
|
||||||
|
if (sequence[i] === "1") number ++;
|
||||||
|
}
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a string from the data stream using the given dictionary.
|
||||||
|
* @param {BigInt} number Data stream to decode from
|
||||||
|
* @param {object} lookup Dictionary of binary sequence -> output pairs
|
||||||
|
* @returns {{newNumber: BigInt, digit: string}}
|
||||||
|
* Modified input number after decoding, output string
|
||||||
|
*/
|
||||||
|
function huffmanDecode (number, lookup) {
|
||||||
|
let sequence = "";
|
||||||
|
do {
|
||||||
|
sequence += number & 1n;
|
||||||
|
number >>= 1n;
|
||||||
|
if (sequence.length > 20) {
|
||||||
|
throw `Huffman sequence too long: "${sequence}".`;
|
||||||
|
}
|
||||||
|
} while (!(sequence in lookup));
|
||||||
|
return { newNumber: number, digit: lookup[sequence] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compresses the input link and encodes it to the given alphabet.
|
||||||
|
* @param {string} input Link to compress
|
||||||
|
* @param {string[]} alphabet Output alphabet as array of characters/strings
|
||||||
|
* @returns {string} Output payload (not a full link!)
|
||||||
|
*/
|
||||||
|
export function compress (input, alphabet) {
|
||||||
|
let number = 1n;
|
||||||
|
|
||||||
|
// Validate URL, add protocol if needed
|
||||||
|
let url;
|
||||||
|
if (URL.canParse(input)) {
|
||||||
|
url = new URL(input);
|
||||||
|
} else {
|
||||||
|
url = new URL("http://" + input);
|
||||||
|
}
|
||||||
|
|
||||||
|
let hostname = url.hostname.toLowerCase();
|
||||||
|
const port = BigInt(url.port);
|
||||||
|
const tld = hostname.includes(".") && hostname.split(".").at(-1).toLowerCase();
|
||||||
|
|
||||||
|
if (tld in tldEncode) {
|
||||||
|
hostname = hostname.split(".").slice(0, -1).join(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHTTPS = url.protocol === "https:";
|
||||||
|
const hasWWW = url.hostname.toLowerCase().startsWith("www.");
|
||||||
|
if (hasWWW) hostname = hostname.slice(4);
|
||||||
|
|
||||||
|
const knownSLD = sldList.find(c => hostname.endsWith(c)) || "";
|
||||||
|
const subdomain = hostname.slice(0, -knownSLD.length);
|
||||||
|
|
||||||
|
// Read URL path, split it into segments
|
||||||
|
let path = url.pathname;
|
||||||
|
|
||||||
|
// Remove "index" suffixes, encoded separately later
|
||||||
|
const hasIndexHTML = path.endsWith("/index.html");
|
||||||
|
const hasIndexPHP = path.endsWith("/index.php");
|
||||||
|
if (hasIndexHTML) path = path.slice(0, -11);
|
||||||
|
else if (hasIndexPHP) path = path.slice(0, -10);
|
||||||
|
|
||||||
|
// The seperable parts of a path/query are split into segments with
|
||||||
|
// their position/role in the link noted. This lets us pick optimal
|
||||||
|
// character sets for individual segments and enables us to encode
|
||||||
|
// only the transitions between segments. As-is, the system's a bit
|
||||||
|
// clumsy, but it works.
|
||||||
|
const pathSegments = path.split("/")
|
||||||
|
.filter(c => c.length)
|
||||||
|
.map(c => ({ type: "path", value: c }));
|
||||||
|
|
||||||
|
// Add search/query parameters to path segments
|
||||||
|
let queryParams = Array.from(url.searchParams)
|
||||||
|
.flat()
|
||||||
|
.map(c => ({ type: "query", value: c }));
|
||||||
|
pathSegments.push(...queryParams);
|
||||||
|
|
||||||
|
// Add hash value to path segments
|
||||||
|
if (url.hash && url.hash.length > 1) {
|
||||||
|
pathSegments.push({ type: "hash", value: url.hash.slice(1) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize path segment encoding
|
||||||
|
for (const segment of pathSegments) {
|
||||||
|
segment.value = encodeURI(decodeURI(segment.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode path following domain segment-by-segment, using best algorithm for each
|
||||||
|
let lastSegmentType = pathSegments.at(-1)?.type;
|
||||||
|
let queryParamIndex = 0;
|
||||||
|
for (let j = pathSegments.length - 1; j >= 0; j --) {
|
||||||
|
const segment = pathSegments[j];
|
||||||
|
const firstIteration = j === pathSegments.length - 1;
|
||||||
|
if (!firstIteration && queryParamIndex % 2 !== 1) {
|
||||||
|
// Indicate change of segment type (path -> param -> hash)
|
||||||
|
// First bit indicates that a change is happening,
|
||||||
|
// second bit indicates whether we're skipping straight to the hash.
|
||||||
|
number <<= 1n;
|
||||||
|
if (lastSegmentType === "hash" && segment.type === "query") {
|
||||||
|
number ++;
|
||||||
|
} else if (lastSegmentType === "hash" && segment.type === "path") {
|
||||||
|
number ++; // Second bit is 1
|
||||||
|
number <<= 1n;
|
||||||
|
number ++;
|
||||||
|
} else if (lastSegmentType !== segment.type) {
|
||||||
|
// Second bit is 0
|
||||||
|
number <<= 1n;
|
||||||
|
number ++;
|
||||||
|
}
|
||||||
|
lastSegmentType = segment.type;
|
||||||
|
}
|
||||||
|
if (segment.type === "query") {
|
||||||
|
queryParamIndex ++;
|
||||||
|
}
|
||||||
|
// Look for smallest subalphabet that fits this path segment
|
||||||
|
let subalphabetIndex = subalphabets.length - 1;
|
||||||
|
let subalphabet = subalphabets[subalphabetIndex];
|
||||||
|
for (let i = 0; i < subalphabets.length - 1; i ++) {
|
||||||
|
if (!Array.from(segment.value).some(c => !subalphabets[i].includes(c))) {
|
||||||
|
subalphabet = subalphabets[i];
|
||||||
|
subalphabetIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Compute number after Huffman coding
|
||||||
|
let huffmanNumber = firstIteration ? number : huffmanEncode(number, pathEncode["#"]);
|
||||||
|
for (let i = segment.value.length - 1; i >= 0; i --) {
|
||||||
|
if (segment.value[i - 2] === "%") {
|
||||||
|
const byte = parseInt(segment.value.slice(i - 1, i + 1), 16);
|
||||||
|
huffmanNumber *= 256n;
|
||||||
|
huffmanNumber += BigInt(byte);
|
||||||
|
huffmanNumber = huffmanEncode(huffmanNumber, pathEncode["%"]);
|
||||||
|
i -= 2;
|
||||||
|
} else {
|
||||||
|
huffmanNumber = huffmanEncode(huffmanNumber, pathEncode[segment.value[i]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Encode segment variant as 0
|
||||||
|
// (We're adding +1 here to introduce 0 as a special value indicating Huffman)
|
||||||
|
huffmanNumber *= BigInt(subalphabets.length + 1);
|
||||||
|
// Compute number after encoding with chosen subalphabet
|
||||||
|
const subalphabetLength = BigInt(subalphabet.length + 1);
|
||||||
|
let subalphabetNumber = firstIteration ? number : number * subalphabetLength;
|
||||||
|
for (let i = segment.value.length - 1; i >= 0; i--) {
|
||||||
|
subalphabetNumber *= subalphabetLength;
|
||||||
|
subalphabetNumber += BigInt(subalphabet.indexOf(segment.value[i]) + 1);
|
||||||
|
}
|
||||||
|
// Encode segment variant as subalphabet index + 1
|
||||||
|
subalphabetNumber *= BigInt(subalphabets.length + 1);
|
||||||
|
subalphabetNumber += BigInt(subalphabetIndex + 1);
|
||||||
|
// Compare candidate numbers, pick smallest one
|
||||||
|
if (huffmanNumber < subalphabetNumber) {
|
||||||
|
number = huffmanNumber;
|
||||||
|
} else {
|
||||||
|
number = subalphabetNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode type of first path segment
|
||||||
|
if (pathSegments.length > 0) {
|
||||||
|
number *= 3n;
|
||||||
|
if (pathSegments[0].type === "query") {
|
||||||
|
number += 1n;
|
||||||
|
} else if (pathSegments[0].type === "hash") {
|
||||||
|
number += 2n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode either SLD + subdomain or full hostname
|
||||||
|
if (!knownSLD) {
|
||||||
|
// Write stopping token only if path follows
|
||||||
|
if (path || search) number = huffmanEncode(number, domainEncode["END"]);
|
||||||
|
for (let i = hostname.length - 1; i >= 0; i --) {
|
||||||
|
number = huffmanEncode(number, domainEncode[hostname[i]]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Encode subdomain
|
||||||
|
if (subdomain) {
|
||||||
|
// Write stopping token only if path follows
|
||||||
|
if (path || search) number = huffmanEncode(number, domainEncode["END"]);
|
||||||
|
for (let i = subdomain.length - 1; i >= 0; i--) {
|
||||||
|
number = huffmanEncode(number, domainEncode[subdomain[i]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Encode Huffman code of known SLD
|
||||||
|
number = huffmanEncode(number, sldEncode[knownSLD]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indicate presence of known SLD and optional subdomain
|
||||||
|
if (knownSLD) {
|
||||||
|
number <<= 1n;
|
||||||
|
if (subdomain) number += 1n;
|
||||||
|
}
|
||||||
|
number <<= 1n;
|
||||||
|
if (knownSLD) number += 1n;
|
||||||
|
|
||||||
|
// Encode "index.html"/"index.php" suffix
|
||||||
|
number <<= 1n;
|
||||||
|
if (hasIndexPHP) number += 1n;
|
||||||
|
if (hasIndexHTML || hasIndexPHP) {
|
||||||
|
number <<= 1n;
|
||||||
|
number += 1n;
|
||||||
|
}
|
||||||
|
// Encode protocol
|
||||||
|
number <<= 1n;
|
||||||
|
if (isHTTPS) number += 1n;
|
||||||
|
// Encode "www." prefix
|
||||||
|
number <<= 1n;
|
||||||
|
if (hasWWW) number += 1n;
|
||||||
|
// Encode TLD
|
||||||
|
number = huffmanEncode(number, tldEncode[tld] || tldEncode[""]);
|
||||||
|
// Encode port number
|
||||||
|
if (port) {
|
||||||
|
number *= 65536n;
|
||||||
|
number += port;
|
||||||
|
}
|
||||||
|
number <<= 1n;
|
||||||
|
if (port) number += 1n;
|
||||||
|
|
||||||
|
// Encode version number
|
||||||
|
for (let i = 0; i < VERSION; i ++) {
|
||||||
|
number <<= 1n;
|
||||||
|
number += 1n;
|
||||||
|
}
|
||||||
|
number <<= 1n;
|
||||||
|
|
||||||
|
return numberToString(number, alphabet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes and decompresses the payload assuming the given alphabet and
|
||||||
|
* produces a full link.
|
||||||
|
* @param {string} input Compressed payload
|
||||||
|
* @param {string[]} alphabet Ordered alphabet used by payload
|
||||||
|
* @returns {string} Full link containing payload contents.
|
||||||
|
*/
|
||||||
|
export function decompress (input, alphabet) {
|
||||||
|
let number = stringToNumber(input, alphabet);
|
||||||
|
|
||||||
|
// Version number - currently unused
|
||||||
|
let version = 0;
|
||||||
|
while (number & 1n) {
|
||||||
|
version ++;
|
||||||
|
number >>= 1n;
|
||||||
|
}
|
||||||
|
number >>= 1n;
|
||||||
|
|
||||||
|
// Decode port number
|
||||||
|
const hasPort = number & 1n;
|
||||||
|
number >>= 1n;
|
||||||
|
let port;
|
||||||
|
if (hasPort) {
|
||||||
|
port = number % 65536n;
|
||||||
|
number /= 65536n;
|
||||||
|
}
|
||||||
|
// Decode TLD
|
||||||
|
const tldDecodeResult = huffmanDecode(number, tldDecode);
|
||||||
|
number = tldDecodeResult.newNumber;
|
||||||
|
const tld = tldDecodeResult.digit;
|
||||||
|
// Decode "www." prefix
|
||||||
|
const hasWWW = number & 1n;
|
||||||
|
number >>= 1n;
|
||||||
|
// Decode protocol
|
||||||
|
const isHTTPS = number & 1n;
|
||||||
|
number >>= 1n;
|
||||||
|
// Decode "index.html"/"index.php" suffix
|
||||||
|
let indexSuffix = "";
|
||||||
|
if (number & 1n) {
|
||||||
|
number >>= 1n;
|
||||||
|
if (number & 1n) {
|
||||||
|
indexSuffix = "/index.php";
|
||||||
|
} else {
|
||||||
|
indexSuffix = "/index.html";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
number >>= 1n;
|
||||||
|
// Determine domain format
|
||||||
|
const hasKnownSLD = number & 1n;
|
||||||
|
number >>= 1n;
|
||||||
|
let hasSubdomain = false;
|
||||||
|
if (hasKnownSLD) {
|
||||||
|
hasSubdomain = number & 1n;
|
||||||
|
number >>= 1n;
|
||||||
|
}
|
||||||
|
|
||||||
|
let domain = "";
|
||||||
|
let subdomain = "";
|
||||||
|
let path = "";
|
||||||
|
|
||||||
|
if (hasKnownSLD) {
|
||||||
|
const sldDecodeResult = huffmanDecode(number, sldDecode);
|
||||||
|
number = sldDecodeResult.newNumber;
|
||||||
|
domain = sldDecodeResult.digit;
|
||||||
|
if (hasSubdomain) {
|
||||||
|
while (number > 1n) {
|
||||||
|
const { newNumber, digit } = huffmanDecode(number, domainDecode);
|
||||||
|
number = newNumber;
|
||||||
|
if (digit === "END") break;
|
||||||
|
subdomain += digit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (number > 1n) {
|
||||||
|
const { newNumber, digit } = huffmanDecode(number, domainDecode);
|
||||||
|
number = newNumber;
|
||||||
|
if (digit === "END") break;
|
||||||
|
domain += digit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const segmentTypeIndex = number % 3n;
|
||||||
|
number /= 3n;
|
||||||
|
let currentSegmentType = ["path", "query", "hash"][segmentTypeIndex];
|
||||||
|
|
||||||
|
let queryParamIndex = 0;
|
||||||
|
|
||||||
|
while (number > 1n) {
|
||||||
|
if (currentSegmentType === "path") {
|
||||||
|
path += "/";
|
||||||
|
} else if (currentSegmentType === "hash") {
|
||||||
|
path += "#";
|
||||||
|
} else {
|
||||||
|
if (queryParamIndex % 2) {
|
||||||
|
path += "=";
|
||||||
|
} else if (queryParamIndex === 0) {
|
||||||
|
path += "?";
|
||||||
|
} else {
|
||||||
|
path += "&";
|
||||||
|
}
|
||||||
|
queryParamIndex ++;
|
||||||
|
}
|
||||||
|
// Get path segment variant
|
||||||
|
const variant = Number(number % BigInt(subalphabets.length + 1));
|
||||||
|
number /= BigInt(subalphabets.length + 1);
|
||||||
|
// Variant 0 is Huffman code, rest are subalphabets
|
||||||
|
if (variant === 0) {
|
||||||
|
while (number > 1n) {
|
||||||
|
const { newNumber, digit } = huffmanDecode(number, pathDecode);
|
||||||
|
number = newNumber;
|
||||||
|
if (digit === "#" && currentSegmentType !== "hash") break;
|
||||||
|
path += digit;
|
||||||
|
if (digit === "%") {
|
||||||
|
const byte = number % 256n;
|
||||||
|
path += byte.toString(16);
|
||||||
|
number /= 256n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const subalphabet = subalphabets[variant - 1];
|
||||||
|
const subalphabetLength = BigInt(subalphabet.length + 1);
|
||||||
|
while (number > 1n) {
|
||||||
|
const index = Number(number % subalphabetLength);
|
||||||
|
number /= subalphabetLength;
|
||||||
|
if (index === 0) break;
|
||||||
|
path += subalphabet[index - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle changing between path segment types, unless we're in the
|
||||||
|
// middle of decoding a query parameter key/value pair, in which
|
||||||
|
// case switching to the hash value doesn't make sense.
|
||||||
|
if (queryParamIndex % 2) continue;
|
||||||
|
if (number & 1n) { // Changing segment type?
|
||||||
|
if (currentSegmentType === "path") {
|
||||||
|
number >>= 1n;
|
||||||
|
if (number & 1n) { // Skipping to hash?
|
||||||
|
currentSegmentType = "hash";
|
||||||
|
} else {
|
||||||
|
currentSegmentType = "query";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentSegmentType = "hash";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
number >>= 1n;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = ""
|
||||||
|
+ (isHTTPS ? "https://" : "http://")
|
||||||
|
+ (hasWWW ? "www." : "")
|
||||||
|
+ subdomain
|
||||||
|
+ domain
|
||||||
|
+ (tld ? "." + tld : "")
|
||||||
|
+ (hasPort ? ":" + port : "")
|
||||||
|
+ path
|
||||||
|
+ indexSuffix;
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
+168
@@ -0,0 +1,168 @@
|
|||||||
|
<!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');
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 2.5em;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
font-size: 0.6em;
|
||||||
|
margin-top: -10px;
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
#input-link {
|
||||||
|
padding: 5px 8px;
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
border-bottom: 2px solid darkgray;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: rgb(230, 230, 230);
|
||||||
|
}
|
||||||
|
#output-link-container {
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#output-link {
|
||||||
|
color: rgb(50, 100, 255);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="/qrcode.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="loader"></div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import { compress, decompress } from "./compress.js";
|
||||||
|
import {
|
||||||
|
outputAlphabetASCII,
|
||||||
|
outputAlphabetQR,
|
||||||
|
outputAlphabetEmoji
|
||||||
|
} from "./alphabets.js";
|
||||||
|
|
||||||
|
var settings = {
|
||||||
|
emoji: false,
|
||||||
|
qr: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const settingsElements = {
|
||||||
|
emoji: "#settings-emoji",
|
||||||
|
qr: "#settings-qr"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const setting in settingsElements) {
|
||||||
|
const element = document.querySelector(settingsElements[setting]);
|
||||||
|
settings[setting] = element.checked;
|
||||||
|
element.addEventListener("change", (event) => {
|
||||||
|
settings[setting] = element.checked;
|
||||||
|
updateOutput();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function countSymbols (string, alphabet) {
|
||||||
|
let count = 0;
|
||||||
|
while (string) {
|
||||||
|
const symbol = alphabet.find(c => string.endsWith(c));
|
||||||
|
string = string.slice(0, symbol ? -symbol.length : -1);
|
||||||
|
count ++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputLinkElement = document.querySelector("#input-link");
|
||||||
|
const outputLinkElement = document.querySelector("#output-link");
|
||||||
|
const outputRatioElement = document.querySelector("#output-ratio");
|
||||||
|
const queryWarningElement = document.querySelector("#query-warning");
|
||||||
|
|
||||||
|
const qrCodeImage = document.querySelector("#qrcode");
|
||||||
|
const qrCodeCorrectionLevelContainer = document.querySelector("#qr-correct-level-container");
|
||||||
|
const qrCodeCorrectionLevelElement = document.querySelector("#qr-correct-level");
|
||||||
|
qrCodeCorrectionLevelElement.addEventListener("change", updateOutput);
|
||||||
|
|
||||||
|
function updateOutput () {
|
||||||
|
const input = inputLinkElement.value.trim();
|
||||||
|
try {
|
||||||
|
const alphabet = settings.emoji ? outputAlphabetEmoji : outputAlphabetASCII;
|
||||||
|
const output = compress(input, alphabet);
|
||||||
|
let inputNormalized = input;
|
||||||
|
if (input.startsWith("https://")) {
|
||||||
|
inputNormalized = input.slice(8);
|
||||||
|
} else if (input.startsWith("http://")) {
|
||||||
|
inputNormalized = input.slice(7);
|
||||||
|
}
|
||||||
|
let excessiveParams = false;
|
||||||
|
if (URL.canParse("http://" + inputNormalized)) {
|
||||||
|
const url = new URL("http://" + inputNormalized);
|
||||||
|
if (url.searchParams.size > 1) {
|
||||||
|
excessiveParams = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (excessiveParams) {
|
||||||
|
queryWarningElement.style.display = "inline";
|
||||||
|
} else {
|
||||||
|
queryWarningElement.style.display = "none";
|
||||||
|
}
|
||||||
|
const ratio = (1 - (countSymbols(output, alphabet) + 6) / inputNormalized.length) * 100;
|
||||||
|
if (ratio < -300) {
|
||||||
|
outputRatioElement.textContent = `Output is much larger than the input`;
|
||||||
|
outputRatioElement.style.color = "rgb(255, 50, 50)";
|
||||||
|
} else if (ratio < 0) {
|
||||||
|
outputRatioElement.textContent = `Output is ${Math.floor(-ratio)}% larger than the input`;
|
||||||
|
outputRatioElement.style.color = "rgb(255, 50, 50)";
|
||||||
|
} else if (ratio > 0) {
|
||||||
|
outputRatioElement.textContent = `Output is ${Math.ceil(ratio)}% smaller than the input`;
|
||||||
|
outputRatioElement.style.color = "rgb(15, 190, 15)";
|
||||||
|
} else {
|
||||||
|
outputRatioElement.textContent = "Output is the same length as the input";
|
||||||
|
outputRatioElement.style.color = "gray";
|
||||||
|
}
|
||||||
|
outputLinkElement.textContent = `http://ha.mr#${output}`;
|
||||||
|
outputLinkElement.href = `http://ha.mr#${output}`;
|
||||||
|
outputLinkElement.style.color = "";
|
||||||
|
if (settings.qr) {
|
||||||
|
const errorCorrection = ["L", "M", "Q", "H"][qrCodeCorrectionLevelElement.value];
|
||||||
|
qrCodeImage.style.display = "inline";
|
||||||
|
qrCodeCorrectionLevelContainer.style.display = "inline";
|
||||||
|
let qrCodeLink = `HTTP://HA.MR/${compress(input, outputAlphabetQR)}`;
|
||||||
|
QRCode.toDataURL(qrCodeLink, {
|
||||||
|
errorCorrectionLevel: errorCorrection,
|
||||||
|
scale: 8
|
||||||
|
}, (err, url) => {
|
||||||
|
if (err) {
|
||||||
|
qrCodeImage.style.display = "none";
|
||||||
|
qrCodeCorrectionLevelContainer.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qrCodeImage.src = url;
|
||||||
|
qrCodeImage.title = qrCodeLink;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
qrCodeImage.style.display = "none";
|
||||||
|
qrCodeCorrectionLevelContainer.style.display = "none";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!input.trim()) {
|
||||||
|
outputLinkElement.textContent = "Enter a link above to compress";
|
||||||
|
} else {
|
||||||
|
outputLinkElement.textContent = "Invalid link";
|
||||||
|
outputLinkElement.style.color = "rgb(255, 50, 50)";
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
qrCodeImage.style.display = "none";
|
||||||
|
qrCodeCorrectionLevelContainer.style.display = "none";
|
||||||
|
outputRatioElement.style.color = "rgba(255, 255, 255, 0)";
|
||||||
|
outputLinkElement.removeAttribute("href");
|
||||||
|
queryWarningElement.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inputLinkElement.addEventListener("input", updateOutput);
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
let payload = null;
|
||||||
|
let alphabet = outputAlphabetASCII;
|
||||||
|
|
||||||
|
// Get hash value of current address bar
|
||||||
|
if (window.location.hash) {
|
||||||
|
// Decode hash value in case it's non-ASCII
|
||||||
|
payload = decodeURIComponent(window.location.hash.slice(1));
|
||||||
|
// Remove all whitespace - we never use whitespace when encoding hash values
|
||||||
|
payload = payload.replaceAll(" ", "");
|
||||||
|
// Check if input is pure ASCII - potentially unreliable?
|
||||||
|
const useEmoji = Array.from(payload).some(c => !outputAlphabetASCII.includes(c));
|
||||||
|
alphabet = useEmoji ? outputAlphabetEmoji : outputAlphabetASCII;
|
||||||
|
} else {
|
||||||
|
// If no hash value, we're likely reading a QR code
|
||||||
|
// For that, use the path instead
|
||||||
|
payload = decodeURIComponent(window.location.pathname.slice(1));
|
||||||
|
alphabet = outputAlphabetQR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload && payload.trim()) {
|
||||||
|
try {
|
||||||
|
const target = decompress(payload, alphabet);
|
||||||
|
window.location.href = target;
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Redirect failed. Could not decode input.`);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOutput();
|
||||||
|
|
||||||
|
document.querySelector("#loader").style.opacity = 0;
|
||||||
|
document.querySelector("#content").style.opacity = 1;
|
||||||
|
document.querySelector("#content").style.pointerEvents = "auto";
|
||||||
|
|
||||||
|
})();
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
var QRCode=function(t){"use strict";var r,e=function(){return"function"==typeof Promise&&Promise.prototype&&Promise.prototype.then},n=[0,26,44,70,100,134,172,196,242,292,346,404,466,532,581,655,733,815,901,991,1085,1156,1258,1364,1474,1588,1706,1828,1921,2051,2185,2323,2465,2611,2761,2876,3034,3196,3362,3532,3706],o=function(t){if(!t)throw new Error('"version" cannot be null or undefined');if(t<1||t>40)throw new Error('"version" should be in range from 1 to 40');return 4*t+17},a=function(t){return n[t]},i=function(t){for(var r=0;0!==t;)r++,t>>>=1;return r},u=function(t){if("function"!=typeof t)throw new Error('"toSJISFunc" is not a valid function.');r=t},s=function(){return void 0!==r},f=function(t){return r(t)};function h(t,r){return t(r={exports:{}},r.exports),r.exports}var c=h((function(t,r){r.L={bit:1},r.M={bit:0},r.Q={bit:3},r.H={bit:2},r.isValid=function(t){return t&&void 0!==t.bit&&t.bit>=0&&t.bit<4},r.from=function(t,e){if(r.isValid(t))return t;try{return function(t){if("string"!=typeof t)throw new Error("Param is not a string");switch(t.toLowerCase()){case"l":case"low":return r.L;case"m":case"medium":return r.M;case"q":case"quartile":return r.Q;case"h":case"high":return r.H;default:throw new Error("Unknown EC Level: "+t)}}(t)}catch(t){return e}}}));function g(){this.buffer=[],this.length=0}c.L,c.M,c.Q,c.H,c.isValid,g.prototype={get:function(t){var r=Math.floor(t/8);return 1==(this.buffer[r]>>>7-t%8&1)},put:function(t,r){for(var e=0;e<r;e++)this.putBit(1==(t>>>r-e-1&1))},getLengthInBits:function(){return this.length},putBit:function(t){var r=Math.floor(this.length/8);this.buffer.length<=r&&this.buffer.push(0),t&&(this.buffer[r]|=128>>>this.length%8),this.length++}};var d=g;function l(t){if(!t||t<1)throw new Error("BitMatrix size must be defined and greater than 0");this.size=t,this.data=new Uint8Array(t*t),this.reservedBit=new Uint8Array(t*t)}l.prototype.set=function(t,r,e,n){var o=t*this.size+r;this.data[o]=e,n&&(this.reservedBit[o]=!0)},l.prototype.get=function(t,r){return this.data[t*this.size+r]},l.prototype.xor=function(t,r,e){this.data[t*this.size+r]^=e},l.prototype.isReserved=function(t,r){return this.reservedBit[t*this.size+r]};var v=l,p=h((function(t,r){var e=o;r.getRowColCoords=function(t){if(1===t)return[];for(var r=Math.floor(t/7)+2,n=e(t),o=145===n?26:2*Math.ceil((n-13)/(2*r-2)),a=[n-7],i=1;i<r-1;i++)a[i]=a[i-1]-o;return a.push(6),a.reverse()},r.getPositions=function(t){for(var e=[],n=r.getRowColCoords(t),o=n.length,a=0;a<o;a++)for(var i=0;i<o;i++)0===a&&0===i||0===a&&i===o-1||a===o-1&&0===i||e.push([n[a],n[i]]);return e}}));p.getRowColCoords,p.getPositions;var w=o,m=function(t){var r=w(t);return[[0,0],[r-7,0],[0,r-7]]},E=h((function(t,r){r.Patterns={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7};var e=3,n=3,o=40,a=10;function i(t,e,n){switch(t){case r.Patterns.PATTERN000:return(e+n)%2==0;case r.Patterns.PATTERN001:return e%2==0;case r.Patterns.PATTERN010:return n%3==0;case r.Patterns.PATTERN011:return(e+n)%3==0;case r.Patterns.PATTERN100:return(Math.floor(e/2)+Math.floor(n/3))%2==0;case r.Patterns.PATTERN101:return e*n%2+e*n%3==0;case r.Patterns.PATTERN110:return(e*n%2+e*n%3)%2==0;case r.Patterns.PATTERN111:return(e*n%3+(e+n)%2)%2==0;default:throw new Error("bad maskPattern:"+t)}}r.isValid=function(t){return null!=t&&""!==t&&!isNaN(t)&&t>=0&&t<=7},r.from=function(t){return r.isValid(t)?parseInt(t,10):void 0},r.getPenaltyN1=function(t){for(var r=t.size,n=0,o=0,a=0,i=null,u=null,s=0;s<r;s++){o=a=0,i=u=null;for(var f=0;f<r;f++){var h=t.get(s,f);h===i?o++:(o>=5&&(n+=e+(o-5)),i=h,o=1),(h=t.get(f,s))===u?a++:(a>=5&&(n+=e+(a-5)),u=h,a=1)}o>=5&&(n+=e+(o-5)),a>=5&&(n+=e+(a-5))}return n},r.getPenaltyN2=function(t){for(var r=t.size,e=0,o=0;o<r-1;o++)for(var a=0;a<r-1;a++){var i=t.get(o,a)+t.get(o,a+1)+t.get(o+1,a)+t.get(o+1,a+1);4!==i&&0!==i||e++}return e*n},r.getPenaltyN3=function(t){for(var r=t.size,e=0,n=0,a=0,i=0;i<r;i++){n=a=0;for(var u=0;u<r;u++)n=n<<1&2047|t.get(i,u),u>=10&&(1488===n||93===n)&&e++,a=a<<1&2047|t.get(u,i),u>=10&&(1488===a||93===a)&&e++}return e*o},r.getPenaltyN4=function(t){for(var r=0,e=t.data.length,n=0;n<e;n++)r+=t.data[n];return Math.abs(Math.ceil(100*r/e/5)-10)*a},r.applyMask=function(t,r){for(var e=r.size,n=0;n<e;n++)for(var o=0;o<e;o++)r.isReserved(o,n)||r.xor(o,n,i(t,o,n))},r.getBestMask=function(t,e){for(var n=Object.keys(r.Patterns).length,o=0,a=1/0,i=0;i<n;i++){e(i),r.applyMask(i,t);var u=r.getPenaltyN1(t)+r.getPenaltyN2(t)+r.getPenaltyN3(t)+r.getPenaltyN4(t);r.applyMask(i,t),u<a&&(a=u,o=i)}return o}}));E.Patterns,E.isValid,E.getPenaltyN1,E.getPenaltyN2,E.getPenaltyN3,E.getPenaltyN4,E.applyMask,E.getBestMask;var y=[1,1,1,1,1,1,1,1,1,1,2,2,1,2,2,4,1,2,4,4,2,4,4,4,2,4,6,5,2,4,6,6,2,5,8,8,4,5,8,8,4,5,8,11,4,8,10,11,4,9,12,16,4,9,16,16,6,10,12,18,6,10,17,16,6,11,16,19,6,13,18,21,7,14,21,25,8,16,20,25,8,17,23,25,9,17,23,34,9,18,25,30,10,20,27,32,12,21,29,35,12,23,34,37,12,25,34,40,13,26,35,42,14,28,38,45,15,29,Line truncated
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { compress, decompress } from "./compress.js";
|
||||||
|
import {
|
||||||
|
outputAlphabetASCII,
|
||||||
|
outputAlphabetQR,
|
||||||
|
outputAlphabetEmoji
|
||||||
|
} from "./alphabets.js";
|
||||||
|
|
||||||
|
const input = process.argv[2]?.trim();
|
||||||
|
const alphabetName = process.argv[3]?.trim() || "ascii";
|
||||||
|
if (!input) {
|
||||||
|
console.error(`Usage: hamr <link> [ascii|qr|emoji]`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload = "";
|
||||||
|
if (input.toLowerCase().startsWith("http://ha.mr")) {
|
||||||
|
payload = input.slice(12);
|
||||||
|
} else if (input.toLowerCase().startsWith("https://ha.mr")) {
|
||||||
|
payload = input.slice(13);
|
||||||
|
} else if (input.toLowerCase().startsWith("ha.mr")) {
|
||||||
|
payload = input.slice(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload) {
|
||||||
|
const isQRCode = input[0] === "/";
|
||||||
|
payload = payload.slice(1);
|
||||||
|
const useEmoji = Array.from(payload).some(c => !outputAlphabetASCII.includes(c));
|
||||||
|
if (isQRCode) console.log(decompress(payload, outputAlphabetQR));
|
||||||
|
else console.log(decompress(payload, useEmoji ? outputAlphabetEmoji : outputAlphabetASCII));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let alphabet = outputAlphabetASCII;
|
||||||
|
if (alphabetName === "qr") alphabet = outputAlphabetQR;
|
||||||
|
else if (alphabetName === "emoji") alphabet = outputAlphabetEmoji;
|
||||||
|
else if (alphabetName !== "ascii") {
|
||||||
|
console.error(`Unknown alphabet "${alphabetName}".`);
|
||||||
|
console.error("Select one of: ascii, qr, emoji");
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alphabetName === "qr") {
|
||||||
|
console.log("HTTP://HA.MR/" + compress(input, alphabet));
|
||||||
|
} else {
|
||||||
|
console.log("http://ha.mr#" + compress(input, alphabet));
|
||||||
|
}
|
||||||
Reference in new issue
Block a user