-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgenerator.js
More file actions
199 lines (163 loc) · 5.06 KB
/
Copy pathgenerator.js
File metadata and controls
199 lines (163 loc) · 5.06 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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
const DETS_SING = ["the", "a", "this", "that", "my", "your", "our", "some"];
const DETS_PLUR = ["the", "these", "those", "my", "your", "our", "some"];
const PREPS = [
"in",
"on",
"under",
"over",
"through",
"toward",
"behind",
"within",
"beyond",
];
const CONJS = ["and", "but", "so", "while"];
const TAIL_PREP = ["of", "with", "without"];
let WORDS = null;
// For checking
let ALL_WORDS = null;
async function loadJSON(path) {
const r = await fetch(path);
if (!r.ok) throw new Error(`Failed to load ${path}: ${r.status}`);
return r.json();
}
async function loadWords() {
if (WORDS) return WORDS;
// Prefer JSON arrays over txt in browsers.
const [nouns, verbs, adjs, advs] = await Promise.all([
loadJSON("./words/nouns.json"),
loadJSON("./words/verbs.json"),
loadJSON("./words/adjectives.json"),
loadJSON("./words/adverbs.json"),
]);
for (const [name, arr] of Object.entries({ nouns, verbs, adjs, advs })) {
if (!Array.isArray(arr) || arr.length !== 256) {
throw new Error(`${name}.json must be an array of exactly 256 strings`);
}
}
WORDS = { nouns, verbs, adjs, advs };
return WORDS;
}
async function loadAllWords() {
if (!ALL_WORDS) {
const { nouns, verbs, adjs, advs } = await loadWords();
ALL_WORDS = new Set(
[
...nouns,
...verbs,
...adjs,
...advs,
...DETS_SING,
...DETS_PLUR,
...PREPS,
...CONJS,
...TAIL_PREP,
].map((w) => w.toLowerCase()),
);
}
return ALL_WORDS;
}
function tokenizeForWordlistScan(str) {
return str
.normalize("NFKC")
.toLowerCase()
.replace(/[^a-z\s]/g, " ") // remove punctuation, keep letters + spaces
.split(/\s+/)
.filter(Boolean);
}
// A very basic checking function to verify each word is in the list
export async function checkPoem(str) {
const allWords = await loadAllWords();
const tokens = tokenizeForWordlistScan(str);
for (const word of tokens) {
if (!allWords.has(word)) return false;
}
return true;
}
function aOrAn(word) {
return /^[aeiou]/i.test(word) ? "an" : "a";
}
function makeCursor(bytes) {
let i = 0;
return () => bytes[i++ % bytes.length];
}
async function sha512Bytes(str) {
const data = new TextEncoder().encode(str);
const hash = await crypto.subtle.digest("SHA-512", data);
return new Uint8Array(hash);
}
function cap(s) {
return s ? s[0].toUpperCase() + s.slice(1) : s;
}
export async function generatePoem(message) {
const { nouns, verbs, adjs, advs } = await loadWords();
const digest = await sha512Bytes(message);
const next = makeCursor(digest);
const twoSentences = next() % 100 < 35;
const makeSentence = () => {
const plural = (next() & 1) === 1;
const DETS = plural ? DETS_PLUR : DETS_SING;
let det1 = DETS[next() % DETS.length];
let det2 = DETS[next() % DETS.length];
const prep = PREPS[next() % PREPS.length];
const conj = CONJS[next() % CONJS.length];
const noun1i = next();
const noun2i = next();
const verb1i = next();
const verb2i = next();
const adv1i = next();
const adv2i = next();
const useAdj1 = next() % 100 < 70;
const useAdj2 = next() % 100 < 70;
let adj1 = useAdj1 ? adjs[next()] : "";
let adj2 = useAdj2 ? adjs[next()] : "";
let noun1 = nouns[noun1i];
let noun2 = nouns[noun2i];
let verb1 = verbs[verb1i];
let verb2 = verbs[verb2i];
let adv1 = advs[adv1i];
let adv2 = advs[adv2i];
if (noun2 === noun1) noun2 = nouns[next()];
if (adj2 && adj2 === adj1) adj2 = adjs[next()];
if (det1 === "a") det1 = aOrAn(adj1 || noun1);
if (det2 === "a") det2 = aOrAn(adj2 || noun2);
const subj = `${det1} ${adj1} ${noun1}`.replace(/\s+/g, " ").trim();
const obj = `${det2} ${adj2} ${noun2}`.replace(/\s+/g, " ").trim();
const advBefore = (next() & 1) === 1;
// Weighted template choice by repetition
const templates = [0, 0, 0, 1, 1, 2, 3];
const t = templates[next() % templates.length];
let s;
if (t === 0) {
s = advBefore
? `${subj} ${adv1} ${verb1} ${prep} ${obj}`
: `${subj} ${verb1} ${adv1} ${prep} ${obj}`;
} else if (t === 1) {
s = advBefore
? `${subj} ${verb1}, ${conj} ${obj} ${adv2} ${verb2}`
: `${subj} ${verb1}, ${conj} ${obj} ${verb2} ${adv2}`;
} else if (t === 2) {
s = advBefore
? `${prep} ${obj}, ${subj} ${adv1} ${verb1}`
: `${prep} ${obj}, ${subj} ${verb1} ${adv1}`;
} else {
s = `${subj} ${verb1} ${prep} ${obj}`;
}
// Optional tail phrase
if (next() % 100 < 35) {
const tp = TAIL_PREP[next() % TAIL_PREP.length];
const tadj = adjs[next()];
let tnoun = nouns[next()];
if (tnoun === noun1 || tnoun === noun2) tnoun = nouns[next()];
s = `${s} ${tp} ${tadj} ${tnoun}`;
}
// Punctuation
const pr = next() % 100;
const punct = pr < 85 ? "." : pr < 95 ? "!" : pr < 99 ? "…" : "?";
return cap(s.replace(/\s+/g, " ").trim()) + punct;
};
const s1 = makeSentence();
if (!twoSentences) return s1;
const s2 = makeSentence();
return `${s1} ${s2}`;
}