@twardoch/namzy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +62 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +48 -0
- package/dist/mangle.d.ts +10 -0
- package/dist/mangle.js +54 -0
- package/dist/web.d.ts +2 -0
- package/dist/web.js +3 -0
- package/dist/wordlist.d.ts +4 -0
- package/dist/wordlist.js +17 -0
- package/package.json +33 -0
- package/src/cli.ts +69 -0
- package/src/index.ts +58 -0
- package/src/mangle.ts +56 -0
- package/src/web.ts +5 -0
- package/src/wordlist.ts +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# namzy
|
|
2
|
+
|
|
3
|
+
Generates fun, human-friendly project names. Seeded by timestamp so you get
|
|
4
|
+
fresh names every run — but pass `--seed` for reproducibility.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install namzy
|
|
10
|
+
# or run without installing:
|
|
11
|
+
npx namzy
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## CLI
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
# One word (default)
|
|
18
|
+
namzy
|
|
19
|
+
|
|
20
|
+
# PascalCase two-word name
|
|
21
|
+
namzy --shape joined
|
|
22
|
+
|
|
23
|
+
# Space-separated two words
|
|
24
|
+
namzy --shape spaced
|
|
25
|
+
|
|
26
|
+
# Generate 5 names
|
|
27
|
+
namzy --count 5 --shape joined
|
|
28
|
+
|
|
29
|
+
# Fetch words from the web (falls back to offline on failure)
|
|
30
|
+
namzy --online --shape spaced
|
|
31
|
+
|
|
32
|
+
# Help
|
|
33
|
+
namzy --help
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Library
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { generate } from "namzy";
|
|
40
|
+
|
|
41
|
+
// Single word (default)
|
|
42
|
+
const name = await generate();
|
|
43
|
+
|
|
44
|
+
// Joined PascalCase
|
|
45
|
+
const joined = await generate({ shape: "joined" });
|
|
46
|
+
|
|
47
|
+
// Spaced, seeded for reproducibility
|
|
48
|
+
const seeded = await generate({ shape: "spaced", seed: 42 });
|
|
49
|
+
|
|
50
|
+
// Online mode (falls back to offline)
|
|
51
|
+
const online = await generate({ online: true, shape: "joined" });
|
|
52
|
+
|
|
53
|
+
console.log(name, joined, seeded, online);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Modes
|
|
57
|
+
|
|
58
|
+
| Mode | Description |
|
|
59
|
+
|------|-------------|
|
|
60
|
+
| `--offline` | Uses bundled wordlist (default, always works) |
|
|
61
|
+
| `--online` | Fetches from `random-word-api.herokuapp.com`, falls back offline |
|
|
62
|
+
|
|
63
|
+
## How it works
|
|
64
|
+
|
|
65
|
+
Picks words from geographic names (Tokyo, Oslo, Cairo…) and evocative English
|
|
66
|
+
words (ember, flint, willow…), then applies a small phonetic mangling pass
|
|
67
|
+
(e.g. `s→z`, `ph→f`, `oo→u`) for a playful feel. Results are deterministic
|
|
68
|
+
for a given seed.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// this_file: src/cli.ts
|
|
3
|
+
import { generate } from "./index.js";
|
|
4
|
+
const HELP = `
|
|
5
|
+
namzy — generate fun fused project names
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
namzy [options]
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--online Fetch words from a public API (falls back to offline)
|
|
12
|
+
--count <N> Number of names to generate (default: 1)
|
|
13
|
+
--help Show this help
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
namzy
|
|
17
|
+
namzy --count 5
|
|
18
|
+
namzy --online --count 3
|
|
19
|
+
`.trimStart();
|
|
20
|
+
function parseArgs(argv) {
|
|
21
|
+
const args = argv.slice(2);
|
|
22
|
+
const opts = {};
|
|
23
|
+
let count = 1;
|
|
24
|
+
for (let i = 0; i < args.length; i++) {
|
|
25
|
+
const arg = args[i];
|
|
26
|
+
switch (arg) {
|
|
27
|
+
case "--help":
|
|
28
|
+
case "-h":
|
|
29
|
+
process.stdout.write(HELP);
|
|
30
|
+
process.exit(0);
|
|
31
|
+
break;
|
|
32
|
+
case "--online":
|
|
33
|
+
opts.online = true;
|
|
34
|
+
break;
|
|
35
|
+
case "--count": {
|
|
36
|
+
const n = parseInt(args[++i], 10);
|
|
37
|
+
if (isNaN(n) || n < 1) {
|
|
38
|
+
process.stderr.write(`--count must be a positive integer\n`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
count = n;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
default:
|
|
45
|
+
process.stderr.write(`Unknown flag: ${arg}\nRun namzy --help for usage.\n`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { opts, count };
|
|
50
|
+
}
|
|
51
|
+
async function main() {
|
|
52
|
+
const { opts, count } = parseArgs(process.argv);
|
|
53
|
+
const base = Date.now();
|
|
54
|
+
for (let i = 0; i < count; i++) {
|
|
55
|
+
const name = await generate({ ...opts, seed: base + i * 1337 });
|
|
56
|
+
process.stdout.write(name + "\n");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
main().catch((err) => {
|
|
60
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { mangle, mulberry32, joinClean } from "./mangle.js";
|
|
2
|
+
export interface NamzyOptions {
|
|
3
|
+
online?: boolean;
|
|
4
|
+
seed?: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Generate a single fused, mangled namzy name.
|
|
8
|
+
* Two raw words → junction-cleaned fusion → consonant rotation → capitalize.
|
|
9
|
+
*/
|
|
10
|
+
export declare function generate(opts?: NamzyOptions): Promise<string>;
|
|
11
|
+
export { mangle, joinClean, mulberry32 };
|
|
12
|
+
export { GEO, COMMON } from "./wordlist.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// this_file: src/index.ts
|
|
2
|
+
import { GEO, COMMON } from "./wordlist.js";
|
|
3
|
+
import { mangle, mulberry32, joinClean } from "./mangle.js";
|
|
4
|
+
function capitalize(s) {
|
|
5
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
6
|
+
}
|
|
7
|
+
function pick(arr, rng) {
|
|
8
|
+
return arr[Math.floor(rng() * arr.length)];
|
|
9
|
+
}
|
|
10
|
+
async function fetchOnlineWords() {
|
|
11
|
+
const url = `https://random-word-api.herokuapp.com/word?number=2&length=6`;
|
|
12
|
+
const resp = await fetch(url);
|
|
13
|
+
if (!resp.ok)
|
|
14
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
15
|
+
const data = (await resp.json());
|
|
16
|
+
if (!Array.isArray(data) || data.length < 2)
|
|
17
|
+
throw new Error("Bad response");
|
|
18
|
+
return data.slice(0, 2).map((w) => w.toLowerCase().replace(/[^a-z]/g, ""));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Generate a single fused, mangled namzy name.
|
|
22
|
+
* Two raw words → junction-cleaned fusion → consonant rotation → capitalize.
|
|
23
|
+
*/
|
|
24
|
+
export async function generate(opts) {
|
|
25
|
+
const online = opts?.online ?? false;
|
|
26
|
+
const seed = opts?.seed ?? Date.now();
|
|
27
|
+
const rng = mulberry32(seed);
|
|
28
|
+
let w1;
|
|
29
|
+
let w2;
|
|
30
|
+
if (online) {
|
|
31
|
+
try {
|
|
32
|
+
const words = await fetchOnlineWords();
|
|
33
|
+
[w1, w2] = words;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
w1 = pick(GEO, rng).toLowerCase();
|
|
37
|
+
w2 = pick(COMMON, rng).toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
w1 = pick(GEO, rng).toLowerCase();
|
|
42
|
+
w2 = pick(COMMON, rng).toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
const fused = joinClean(w1, w2);
|
|
45
|
+
return capitalize(mangle(fused));
|
|
46
|
+
}
|
|
47
|
+
export { mangle, joinClean, mulberry32 };
|
|
48
|
+
export { GEO, COMMON } from "./wordlist.js";
|
package/dist/mangle.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Simple seeded mulberry32 RNG — returns a function that yields [0,1) floats */
|
|
2
|
+
export declare function mulberry32(seed: number): () => number;
|
|
3
|
+
/** Consonant rotation: c→q, f→v, k→c, q→k, s→z, z→s, v→f, w→u. Case-preserving. */
|
|
4
|
+
export declare function mangle(s: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Fuse two lowercase words at a clean junction.
|
|
7
|
+
* Drops the first char of `b` while it duplicates the tail of `a`,
|
|
8
|
+
* or while the seam is vowel-vowel. Max two trims.
|
|
9
|
+
*/
|
|
10
|
+
export declare function joinClean(a: string, b: string): string;
|
package/dist/mangle.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// this_file: src/mangle.ts
|
|
2
|
+
/** Simple seeded mulberry32 RNG — returns a function that yields [0,1) floats */
|
|
3
|
+
export function mulberry32(seed) {
|
|
4
|
+
let s = seed >>> 0;
|
|
5
|
+
return () => {
|
|
6
|
+
s += 0x6d2b79f5;
|
|
7
|
+
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
8
|
+
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
|
|
9
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
const ROTATION = {
|
|
13
|
+
c: "q", f: "v", k: "c", q: "k",
|
|
14
|
+
s: "z", z: "s", v: "f", w: "u",
|
|
15
|
+
};
|
|
16
|
+
/** Consonant rotation: c→q, f→v, k→c, q→k, s→z, z→s, v→f, w→u. Case-preserving. */
|
|
17
|
+
export function mangle(s) {
|
|
18
|
+
let out = "";
|
|
19
|
+
for (const ch of s) {
|
|
20
|
+
const lower = ch.toLowerCase();
|
|
21
|
+
const repl = ROTATION[lower];
|
|
22
|
+
if (repl === undefined) {
|
|
23
|
+
out += ch;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
out += ch === lower ? repl : repl.toUpperCase();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
const VOWELS = new Set(["a", "e", "i", "o", "u", "y"]);
|
|
32
|
+
/**
|
|
33
|
+
* Fuse two lowercase words at a clean junction.
|
|
34
|
+
* Drops the first char of `b` while it duplicates the tail of `a`,
|
|
35
|
+
* or while the seam is vowel-vowel. Max two trims.
|
|
36
|
+
*/
|
|
37
|
+
export function joinClean(a, b) {
|
|
38
|
+
let head = a;
|
|
39
|
+
let tail = b;
|
|
40
|
+
for (let i = 0; i < 2 && head.length > 0 && tail.length > 0; i++) {
|
|
41
|
+
const last = head[head.length - 1].toLowerCase();
|
|
42
|
+
const first = tail[0].toLowerCase();
|
|
43
|
+
if (last === first) {
|
|
44
|
+
tail = tail.slice(1);
|
|
45
|
+
}
|
|
46
|
+
else if (VOWELS.has(last) && VOWELS.has(first)) {
|
|
47
|
+
tail = tail.slice(1);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return head + tail;
|
|
54
|
+
}
|
package/dist/web.d.ts
ADDED
package/dist/web.js
ADDED
package/dist/wordlist.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// this_file: src/wordlist.ts
|
|
2
|
+
/** Geographic names (A-Za-z only, no diacritics) */
|
|
3
|
+
export const GEO = [
|
|
4
|
+
"tokyo", "paris", "oslo", "berlin", "lagos", "lima", "boston", "vienna",
|
|
5
|
+
"cairo", "kyoto", "dubai", "seoul", "milan", "tunis", "perth", "genoa",
|
|
6
|
+
"porto", "bruges", "ghent", "brest", "minsk", "sofia", "riga", "lyon",
|
|
7
|
+
"basel", "natal", "recife", "darwin", "hobart", "odessa", "varna", "Split",
|
|
8
|
+
"kotor", "tiran", "skopje", "bitola", "plovdiv", "gabrovo", "varna", "ruse",
|
|
9
|
+
];
|
|
10
|
+
/** Common evocative English words */
|
|
11
|
+
export const COMMON = [
|
|
12
|
+
"river", "stone", "ember", "frost", "harbor", "willow", "copper", "marble",
|
|
13
|
+
"anchor", "lantern", "cedar", "falcon", "coral", "dagger", "flint", "grove",
|
|
14
|
+
"haven", "iron", "jasper", "kelp", "larch", "mast", "nettle", "oak",
|
|
15
|
+
"pine", "quill", "reed", "sage", "thorn", "umber", "vale", "wren",
|
|
16
|
+
"yarrow", "zenith", "amber", "birch", "cliff", "drift", "forge", "glade",
|
|
17
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@twardoch/namzy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generates fun fused human-friendly project names",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"namzy": "./dist/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"src",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"build:web": "esbuild src/web.ts --bundle --format=iife --global-name=namzy --outfile=../docs/namzy.js --target=es2020",
|
|
21
|
+
"start": "node dist/cli.js",
|
|
22
|
+
"dev": "tsx src/cli.ts"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.0.0",
|
|
29
|
+
"esbuild": "^0.24.0",
|
|
30
|
+
"tsx": "^4.0.0",
|
|
31
|
+
"typescript": "^5.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// this_file: src/cli.ts
|
|
3
|
+
|
|
4
|
+
import { generate } from "./index.js";
|
|
5
|
+
import type { NamzyOptions } from "./index.js";
|
|
6
|
+
|
|
7
|
+
const HELP = `
|
|
8
|
+
namzy — generate fun fused project names
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
namzy [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--online Fetch words from a public API (falls back to offline)
|
|
15
|
+
--count <N> Number of names to generate (default: 1)
|
|
16
|
+
--help Show this help
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
namzy
|
|
20
|
+
namzy --count 5
|
|
21
|
+
namzy --online --count 3
|
|
22
|
+
`.trimStart();
|
|
23
|
+
|
|
24
|
+
function parseArgs(argv: string[]): { opts: NamzyOptions; count: number } {
|
|
25
|
+
const args = argv.slice(2);
|
|
26
|
+
const opts: NamzyOptions = {};
|
|
27
|
+
let count = 1;
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < args.length; i++) {
|
|
30
|
+
const arg = args[i];
|
|
31
|
+
switch (arg) {
|
|
32
|
+
case "--help":
|
|
33
|
+
case "-h":
|
|
34
|
+
process.stdout.write(HELP);
|
|
35
|
+
process.exit(0);
|
|
36
|
+
break;
|
|
37
|
+
case "--online":
|
|
38
|
+
opts.online = true;
|
|
39
|
+
break;
|
|
40
|
+
case "--count": {
|
|
41
|
+
const n = parseInt(args[++i], 10);
|
|
42
|
+
if (isNaN(n) || n < 1) {
|
|
43
|
+
process.stderr.write(`--count must be a positive integer\n`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
count = n;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
default:
|
|
50
|
+
process.stderr.write(`Unknown flag: ${arg}\nRun namzy --help for usage.\n`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { opts, count };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function main(): Promise<void> {
|
|
58
|
+
const { opts, count } = parseArgs(process.argv);
|
|
59
|
+
const base = Date.now();
|
|
60
|
+
for (let i = 0; i < count; i++) {
|
|
61
|
+
const name = await generate({ ...opts, seed: base + i * 1337 });
|
|
62
|
+
process.stdout.write(name + "\n");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
main().catch((err) => {
|
|
67
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// this_file: src/index.ts
|
|
2
|
+
|
|
3
|
+
import { GEO, COMMON } from "./wordlist.js";
|
|
4
|
+
import { mangle, mulberry32, joinClean } from "./mangle.js";
|
|
5
|
+
|
|
6
|
+
export interface NamzyOptions {
|
|
7
|
+
online?: boolean;
|
|
8
|
+
seed?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function capitalize(s: string): string {
|
|
12
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function pick<T>(arr: T[], rng: () => number): T {
|
|
16
|
+
return arr[Math.floor(rng() * arr.length)];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function fetchOnlineWords(): Promise<string[]> {
|
|
20
|
+
const url = `https://random-word-api.herokuapp.com/word?number=2&length=6`;
|
|
21
|
+
const resp = await fetch(url);
|
|
22
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
23
|
+
const data = (await resp.json()) as string[];
|
|
24
|
+
if (!Array.isArray(data) || data.length < 2) throw new Error("Bad response");
|
|
25
|
+
return data.slice(0, 2).map((w) => w.toLowerCase().replace(/[^a-z]/g, ""));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generate a single fused, mangled namzy name.
|
|
30
|
+
* Two raw words → junction-cleaned fusion → consonant rotation → capitalize.
|
|
31
|
+
*/
|
|
32
|
+
export async function generate(opts?: NamzyOptions): Promise<string> {
|
|
33
|
+
const online = opts?.online ?? false;
|
|
34
|
+
const seed = opts?.seed ?? Date.now();
|
|
35
|
+
const rng = mulberry32(seed);
|
|
36
|
+
|
|
37
|
+
let w1: string;
|
|
38
|
+
let w2: string;
|
|
39
|
+
|
|
40
|
+
if (online) {
|
|
41
|
+
try {
|
|
42
|
+
const words = await fetchOnlineWords();
|
|
43
|
+
[w1, w2] = words;
|
|
44
|
+
} catch {
|
|
45
|
+
w1 = pick(GEO, rng).toLowerCase();
|
|
46
|
+
w2 = pick(COMMON, rng).toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
w1 = pick(GEO, rng).toLowerCase();
|
|
50
|
+
w2 = pick(COMMON, rng).toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const fused = joinClean(w1, w2);
|
|
54
|
+
return capitalize(mangle(fused));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { mangle, joinClean, mulberry32 };
|
|
58
|
+
export { GEO, COMMON } from "./wordlist.js";
|
package/src/mangle.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// this_file: src/mangle.ts
|
|
2
|
+
|
|
3
|
+
/** Simple seeded mulberry32 RNG — returns a function that yields [0,1) floats */
|
|
4
|
+
export function mulberry32(seed: number): () => number {
|
|
5
|
+
let s = seed >>> 0;
|
|
6
|
+
return () => {
|
|
7
|
+
s += 0x6d2b79f5;
|
|
8
|
+
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
9
|
+
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
|
|
10
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ROTATION: Record<string, string> = {
|
|
15
|
+
c: "q", f: "v", k: "c", q: "k",
|
|
16
|
+
s: "z", z: "s", v: "f", w: "u",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Consonant rotation: c→q, f→v, k→c, q→k, s→z, z→s, v→f, w→u. Case-preserving. */
|
|
20
|
+
export function mangle(s: string): string {
|
|
21
|
+
let out = "";
|
|
22
|
+
for (const ch of s) {
|
|
23
|
+
const lower = ch.toLowerCase();
|
|
24
|
+
const repl = ROTATION[lower];
|
|
25
|
+
if (repl === undefined) {
|
|
26
|
+
out += ch;
|
|
27
|
+
} else {
|
|
28
|
+
out += ch === lower ? repl : repl.toUpperCase();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const VOWELS = new Set(["a", "e", "i", "o", "u", "y"]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fuse two lowercase words at a clean junction.
|
|
38
|
+
* Drops the first char of `b` while it duplicates the tail of `a`,
|
|
39
|
+
* or while the seam is vowel-vowel. Max two trims.
|
|
40
|
+
*/
|
|
41
|
+
export function joinClean(a: string, b: string): string {
|
|
42
|
+
let head = a;
|
|
43
|
+
let tail = b;
|
|
44
|
+
for (let i = 0; i < 2 && head.length > 0 && tail.length > 0; i++) {
|
|
45
|
+
const last = head[head.length - 1].toLowerCase();
|
|
46
|
+
const first = tail[0].toLowerCase();
|
|
47
|
+
if (last === first) {
|
|
48
|
+
tail = tail.slice(1);
|
|
49
|
+
} else if (VOWELS.has(last) && VOWELS.has(first)) {
|
|
50
|
+
tail = tail.slice(1);
|
|
51
|
+
} else {
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return head + tail;
|
|
56
|
+
}
|
package/src/web.ts
ADDED
package/src/wordlist.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// this_file: src/wordlist.ts
|
|
2
|
+
|
|
3
|
+
/** Geographic names (A-Za-z only, no diacritics) */
|
|
4
|
+
export const GEO: string[] = [
|
|
5
|
+
"tokyo", "paris", "oslo", "berlin", "lagos", "lima", "boston", "vienna",
|
|
6
|
+
"cairo", "kyoto", "dubai", "seoul", "milan", "tunis", "perth", "genoa",
|
|
7
|
+
"porto", "bruges", "ghent", "brest", "minsk", "sofia", "riga", "lyon",
|
|
8
|
+
"basel", "natal", "recife", "darwin", "hobart", "odessa", "varna", "Split",
|
|
9
|
+
"kotor", "tiran", "skopje", "bitola", "plovdiv", "gabrovo", "varna", "ruse",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
/** Common evocative English words */
|
|
13
|
+
export const COMMON: string[] = [
|
|
14
|
+
"river", "stone", "ember", "frost", "harbor", "willow", "copper", "marble",
|
|
15
|
+
"anchor", "lantern", "cedar", "falcon", "coral", "dagger", "flint", "grove",
|
|
16
|
+
"haven", "iron", "jasper", "kelp", "larch", "mast", "nettle", "oak",
|
|
17
|
+
"pine", "quill", "reed", "sage", "thorn", "umber", "vale", "wren",
|
|
18
|
+
"yarrow", "zenith", "amber", "birch", "cliff", "drift", "forge", "glade",
|
|
19
|
+
];
|