-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMooCodec.ts
More file actions
152 lines (138 loc) · 4.71 KB
/
Copy pathMooCodec.ts
File metadata and controls
152 lines (138 loc) · 4.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
export class MooCodec {
/**
* Letters and digits are packed into moos; everything else (spaces,
* punctuation, symbols) passes through literally. Pass-through chars are never
* letters, so they never collide with moo glyphs and stay unambiguous.
*/
private static isPassThrough(char: string): boolean {
return /[^\p{L}\p{N}]/u.test(char);
}
/**
* Head char that opens every word, so each word reads as a "Moo". Its case
* mirrors the word's first letter: "M" if capitalized, "m" otherwise.
*/
private static head(run: string): string {
return /\p{Lu}/u.test([...run][0]) ? "M" : "m";
}
/** Quote chars are rendered as superscript zeros instead of passing through. */
private static readonly QUOTES: Record<string, string> = {
"'": "⁰",
'"': "~~",
};
/**
* Accent-free O-lookalikes from four scripts. A byte's code is a run of LEAD
* glyphs ended by one STOP glyph, so codes are prefix-free and concatenate
* without separators. LEAD/STOP are disjoint, which makes parsing trivial:
* read glyphs until a STOP.
*/
private static readonly STOP = ["o", "о", "ο", "օ"]; // Latin, Cyrillic, Greek, Armenian small o
private static readonly LEAD = ["O", "О", "Ο", "Օ"]; // their capital forms
private static readonly GLYPHS = [...MooCodec.STOP, ...MooCodec.LEAD].join("");
private static readonly TABLES = MooCodec.buildTables();
/**
* Builds the byte↔code maps. Codes are generated shortest-first and handed
* out to byte values in English-frequency order, so common letters are short.
*/
private static buildTables(): {
byteToCode: string[];
codeToByte: Map<string, number>;
} {
const codes: string[] = [];
let prefixes = [""];
while (codes.length < 256) {
for (const prefix of prefixes) {
for (const stop of MooCodec.STOP) {
if (codes.length < 256) codes.push(prefix + stop);
}
}
const next: string[] = [];
for (const prefix of prefixes) {
for (const lead of MooCodec.LEAD) next.push(prefix + lead);
}
prefixes = next;
}
const priority =
"etaoinshrdlcumwfgypbvkjxqzETAOINSHRDLCUMWFGYPBVKJXQZ0123456789";
const seen = new Set<number>();
const byteByRank: number[] = [];
for (const char of priority) {
const byte = char.charCodeAt(0);
if (!seen.has(byte)) {
seen.add(byte);
byteByRank.push(byte);
}
}
for (let byte = 0; byte < 256; byte++) {
if (!seen.has(byte)) byteByRank.push(byte);
}
const byteToCode: string[] = new Array(256);
const codeToByte = new Map<string, number>();
byteByRank.forEach((byte, rank) => {
byteToCode[byte] = codes[rank];
codeToByte.set(codes[rank], byte);
});
return { byteToCode, codeToByte };
}
/**
* Encode plain text into moo language. Each run of letters/digits becomes one
* moo: a leading "M" followed by the frequency code of each UTF-8 byte. Other
* characters pass through literally.
*/
static encode(text: string): string {
const encoder = new TextEncoder();
const parts: string[] = [];
let run = "";
const flushRun = () => {
if (run.length === 0) return;
let moo = MooCodec.head(run);
for (const byte of encoder.encode(run)) {
moo += MooCodec.TABLES.byteToCode[byte];
}
parts.push(moo);
run = "";
};
for (const char of text) {
const quote = MooCodec.QUOTES[char];
if (quote !== undefined) {
flushRun();
parts.push(quote);
} else if (MooCodec.isPassThrough(char)) {
flushRun();
parts.push(char);
} else {
run += char;
}
}
flushRun();
return parts.join("");
}
/**
* Decode moo language back into plain text. Each moo's glyph body is parsed
* code-by-code (a run of leads ended by a stop) into bytes; literals are
* emitted as-is.
*/
static decode(mooText: string): string {
const stops = new Set(MooCodec.STOP);
const g = MooCodec.GLYPHS;
// Group 1: a moo (head M/m + glyph body). Group 2: literal pass-through.
const regex = new RegExp(`([Mm][${g}]+)|([^Mm${g}]+)`, "gu");
const result: string[] = [];
for (const match of mooText.matchAll(regex)) {
if (match[1] !== undefined) {
const bytes: number[] = [];
let code = "";
for (const glyph of match[1].slice(1)) {
code += glyph;
if (stops.has(glyph)) {
bytes.push(MooCodec.TABLES.codeToByte.get(code)!);
code = "";
}
}
result.push(new TextDecoder().decode(new Uint8Array(bytes)));
} else {
result.push(match[2]!.replace(/~~/g, '"').replace(/⁰/g, "'"));
}
}
return result.join("");
}
}