initial program commit

This commit is contained in:
p2r3 authored and GitHub committed 2026-08-09 13:17:47 +03:00
1 parent afb72c740f
commit a4ddfaeefa
6 files changed
+900

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
export const outputAlphabetASCII = "!#$&'()*+,-./0123456789:;=?~@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]_abcdefghijklmnopqrstuvwxyz".split("");
export const outputAlphabetQR = "$*+-./:0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
export const outputAlphabetEmoji = ["👨🏻‍❤️‍💋‍👨🏻","👨🏻‍❤️‍💋‍👨🏼","👨🏻‍❤️‍💋‍👨🏽","👨🏻‍❤️‍💋‍👨🏾","👨🏻‍❤️‍💋‍👨🏿","👨🏼‍❤️‍💋‍👨🏻","👨🏼‍❤️‍💋‍👨🏼","👨🏼‍❤️‍💋‍👨🏽","👨🏼‍❤️‍💋‍👨🏾","👨🏼‍❤️‍💋‍👨🏿","👨🏽‍❤️‍💋‍👨🏻","👨🏽‍❤️‍💋‍👨🏼","👨🏽‍❤️‍💋‍👨🏽","👨🏽‍❤️‍💋‍👨🏾","👨🏽‍❤️‍💋‍👨🏿","👨🏾‍❤️‍💋‍👨🏻","👨🏾‍❤️‍💋‍👨🏼","👨🏾‍❤️‍💋‍👨🏽","👨🏾‍❤️‍💋‍👨🏾","👨🏾‍❤️‍💋‍👨🏿","👨🏿‍❤️‍💋‍👨🏻","👨🏿‍❤️‍💋‍👨🏼","👨🏿‍❤️‍💋‍👨🏽","👨🏿‍❤️‍💋‍👨🏾","👨🏿‍❤️‍💋‍👨🏿","👩🏻‍❤️‍💋‍👨🏻","👩🏻‍❤️‍💋‍👨🏼","👩🏻‍❤️‍💋‍👨🏽","👩🏻‍❤️‍💋‍👨🏾","👩🏻‍❤️‍💋‍👨🏿","👩🏻‍❤️‍💋‍👩🏻","👩🏻‍❤️‍💋‍👩🏼","👩🏻‍❤️‍💋‍👩🏽","👩🏻‍❤️‍💋‍👩🏾","👩🏻‍❤️‍💋‍👩🏿","👩🏼‍❤️‍💋‍👨🏻","👩🏼‍❤️‍💋‍👨🏼","👩🏼‍❤️‍💋‍👨🏽","👩🏼‍❤️‍💋‍👨🏾","👩🏼‍❤️‍💋‍👨🏿","👩🏼‍❤️‍💋‍👩🏻","👩🏼‍❤️‍💋‍👩🏼","👩🏼‍❤️‍💋‍👩🏽","👩🏼‍❤️‍💋‍👩🏾","👩🏼‍❤️‍💋‍👩🏿","👩🏽‍❤️‍💋‍👨🏻","👩🏽‍❤️‍💋‍👨🏼","👩🏽‍❤️‍💋‍👨🏽","👩🏽‍❤️‍💋‍👨🏾","👩🏽‍❤️‍💋‍👨🏿","👩🏽‍❤️‍💋‍👩🏻","👩🏽‍❤️‍💋‍👩🏼","👩🏽‍❤️‍💋‍👩🏽","👩🏽‍❤️‍💋‍👩🏾","👩🏽‍❤️‍💋‍👩🏿","👩🏾‍❤️‍💋‍👨🏻","👩🏾‍❤️‍💋‍👨🏼","👩🏾‍❤️‍💋‍👨🏽","👩🏾‍❤️‍💋‍👨🏾","👩🏾‍❤️‍💋‍👨🏿","👩🏾‍❤️‍💋‍👩🏻","👩🏾‍❤️‍💋‍👩🏼","👩🏾‍❤️‍💋‍👩🏽","👩🏾‍❤️‍💋‍👩🏾","👩🏾‍❤️‍💋‍👩🏿","👩🏿‍❤️‍💋‍👨🏻","👩🏿‍❤️‍💋‍👨🏼","👩🏿‍❤️‍💋‍👨🏽","👩🏿‍❤️‍💋‍👨🏾","👩🏿‍❤️‍💋‍👨🏿","👩🏿‍❤️‍💋‍👩🏻","👩🏿‍❤️‍💋‍👩🏼","👩🏿‍❤️‍💋‍👩🏽","👩🏿‍❤️‍💋‍👩🏾","👩🏿‍❤️‍💋‍👩🏿","🧑🏻‍❤️‍💋‍🧑🏼","🧑🏻‍❤️‍💋‍🧑🏽","🧑🏻‍❤️‍💋‍🧑🏾","🧑🏻‍❤️‍💋‍🧑🏿","🧑🏼‍❤️‍💋‍🧑🏻","🧑🏼‍❤️‍💋‍🧑🏽","🧑🏼‍❤️‍💋‍🧑🏾","🧑🏼‍❤️‍💋‍🧑🏿","🧑🏽‍❤️‍💋‍🧑🏻","🧑🏽‍❤️‍💋‍🧑🏼","🧑🏽‍❤️‍💋‍🧑🏾","🧑🏽‍❤️‍💋‍🧑🏿","🧑🏾‍❤️‍💋‍🧑🏻","🧑🏾‍❤️‍💋‍🧑🏼","🧑🏾‍❤️‍💋‍🧑🏽","🧑🏾‍❤️‍💋‍🧑🏿","🧑🏿‍❤️‍💋‍🧑🏻","🧑🏿‍❤️‍💋‍🧑🏼","🧑🏿‍❤️‍💋‍🧑🏽","🧑🏿‍❤️‍💋‍🧑🏾","🏴󠁧󠁢󠁥󠁮󠁧󠁿","🏴󠁧󠁢󠁳󠁣󠁴󠁿","🏴󠁧󠁢󠁷󠁬󠁳󠁿","👨🏻‍❤️‍👨🏻","👨🏻‍❤️‍👨🏼","👨🏻‍❤️‍👨🏽","👨🏻‍❤️‍👨🏾","👨🏻‍❤️‍👨🏿","👨🏻‍🐰‍👨🏼","👨🏻‍🐰‍👨🏽","👨🏻‍🐰‍👨🏾","👨🏻‍🐰‍👨🏿","👨🏻‍🤝‍👨🏼","👨🏻‍🤝‍👨🏽","👨🏻‍🤝‍👨🏾","👨🏻‍🤝‍👨🏿","👨🏻‍🫯‍👨🏼","👨🏻‍🫯‍👨🏽","👨🏻‍🫯‍👨🏾","👨🏻‍🫯‍👨🏿","👨🏼‍❤️‍👨🏻","👨🏼‍❤️‍👨🏼","👨🏼‍❤️‍👨🏽","👨🏼‍❤️‍👨🏾","👨🏼‍❤️‍👨🏿","👨🏼‍🐰‍👨🏻","👨🏼‍🐰‍👨🏽","👨🏼‍🐰‍👨🏾","👨🏼‍🐰‍👨🏿","👨🏼‍🤝‍👨🏻","👨🏼‍🤝‍👨🏽","👨🏼‍🤝‍👨🏾","👨🏼‍🤝‍👨🏿","👨🏼‍🫯‍👨🏻","👨🏼‍🫯‍👨🏽","👨🏼‍🫯‍👨🏾","👨🏼‍🫯‍👨🏿","👨🏽‍❤️‍👨🏻","👨🏽‍❤️‍👨🏼","👨🏽‍❤️‍👨🏽","👨🏽‍❤️‍👨🏾","👨🏽‍❤️‍👨🏿","👨🏽‍🐰‍👨🏻","👨🏽‍🐰‍👨🏼","👨🏽‍🐰‍👨🏾","👨🏽‍Line truncated
+520
View File
@@ -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
View File
@@ -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 "&amp;" 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">&nbsp;</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>
+162
View File
@@ -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";
})();
+1
View File
@@ -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
+46
View File
@@ -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));
}