compromise-speech 0.1.0 → 0.2.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.
@@ -4,13 +4,464 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.compromiseSpeech = factory());
5
5
  })(this, (function () { 'use strict';
6
6
 
7
+ var version = '0.2.0';
8
+
9
+ const api = function (View) {
10
+ /** */
11
+ View.prototype.syllables = function (options = {}) {
12
+ this.compute('syllables', options);
13
+ const all = [];
14
+ this.docs.forEach(terms => {
15
+ let some = [];
16
+ terms.forEach(term => {
17
+ some = some.concat(term.syllables);
18
+ });
19
+ if (some.length > 0) {
20
+ all.push(some);
21
+ }
22
+ });
23
+ return all
24
+ };
25
+ /** */
26
+ View.prototype.soundsLike = function () {
27
+ this.compute('soundsLike');
28
+ const all = [];
29
+ this.docs.forEach(terms => {
30
+ let some = [];
31
+ terms.forEach(term => {
32
+ some = some.concat(term.soundsLike);
33
+ });
34
+ if (some.length > 0) {
35
+ all.push(some);
36
+ }
37
+ });
38
+ return all
39
+ };
40
+ };
41
+
42
+ // Whole-word splits that override the vowel-span and boundary rules.
43
+ // Keys are lowercase; values are interior cut offsets, excluding the word's end.
44
+ const exceptions = {
45
+ acre: [1], // a-cre: the final re carries a syllable
46
+ // Borrowed words whose final e is pronounced.
47
+ cafe: [2], // ca-fe
48
+ abalone: [2, 3, 5], // ab-a-lo-ne
49
+ aborigine: [2, 3, 6, 7], // ab-o-rig-i-ne
50
+ comanche: [2, 5], // co-man-che
51
+ dolce: [3], // dol-ce
52
+ // Borrowed vowel combinations do not follow the usual English pair rules.
53
+ acacia: [1, 3], // a-ca-cia
54
+ fuchsia: [4], // fuch-sia
55
+ concierge: [3, 5], // con-ci-erge
56
+ concierges: [3, 5], // con-ci-erges
57
+ comedienne: [2, 4, 6], // co-me-di-enne
58
+ // The final re in massacre carries a syllable, including its inflected forms.
59
+ massacre: [3, 5], // mas-sa-cre
60
+ massacred: [3, 5], // mas-sa-cred
61
+ massacring: [3, 5], // mas-sa-cring
62
+ // Silent consonants make these look like syllabic -le endings.
63
+ aisle: [],
64
+ isle: [],
65
+ wednesday: [6], // wednes-day
66
+ // Keep the spoken vowel groups together or apart explicitly.
67
+ buoyant: [4], // buoy-ant
68
+ buoyancy: [4, 6], // buoy-an-cy
69
+ acquiesce: [2, 5], // ac-qui-esce
70
+ acquiesced: [2, 5], // ac-qui-esced
71
+ acquiescent: [2, 5, 7], // ac-qui-es-cent
72
+ acquiescence: [2, 5, 7], // ac-qui-es-cence
73
+ people: [3], //'peo ple',
74
+ queue: [], //'queue',
75
+ business: [4], //'busi ness',
76
+ quiet: [3], //'qui et',
77
+ science: [3], //'sci ence',
78
+ poet: [2], //'po et',
79
+ poetry: [2, 4], //'po et ry',
80
+ every: [3], //'eve ry',
81
+ everything: [3, 5], // 'eve ry thing',
82
+ everybody: [3, 5, 7], //'eve ry bo dy',
83
+ ilyich: [2], // 'il yich': consonantal y in this name
84
+ juan: [], // 'juan':
85
+ wyatt: [2], // 'wy att'
86
+ // Place names retain their recognizable compound boundaries.
87
+ batesville: [5], // bates-ville
88
+ belleville: [5], // belle-ville
89
+ bridgeport: [6], // bridge-port
90
+ cambridgeport: [3, 9], // cam-bridge-port
91
+ cambridgeside: [3, 9], // cam-bridge-side
92
+ janesville: [5], // janes-ville
93
+ roseberry: [4, 7], // rose-ber-ry
94
+ roseville: [4], // rose-ville
95
+ // These names retain vowels that the general spelling rules suppress or blend.
96
+ belge: [3], // bel-ge
97
+ canadienne: [2, 4, 6], // ca-na-di-enne
98
+ mildred: [3], // mil-dred
99
+ nietzsche: [5], // nietz-sche
100
+ odea: [1, 3], // o-de-a
101
+ // The two written o's belong to separate syllables in these words.
102
+ zoologist: [2, 4, 5], // zo-ol-o-gist
103
+ zoology: [2, 4, 5], // zo-ol-o-gy
104
+ };
105
+
106
+ const checkExceptions = str => {
107
+ if (Object.hasOwn(exceptions, str)) {
108
+ return exceptions[str]
109
+ }
110
+ // Reuse singular cuts for a plain -s ending; exact entries above take priority.
111
+ const singular = str.replace(/s$/, '');
112
+ if (Object.hasOwn(exceptions, singular)) {
113
+ // A sibilant before silent e adds a syllable: sci-ence becomes sci-en-ces.
114
+ if (/[csxz]e$/.test(singular)) {
115
+ return exceptions[singular].concat(singular.length - 2)
116
+ }
117
+ return exceptions[singular]
118
+ }
119
+ return null
120
+ };
121
+
122
+ // Match a compound's left component, including its silent e.
123
+ // Both vowel counting and boundary placement use these same spelling rules.
124
+ const compoundPrefixes = [
125
+ /^note(?=book)/, // note-book: silent e and a boundary after the complete base
126
+ /^side(?=kick|line|show|step|stream|track|walk|water|way|winder|wise)/,
127
+ /^some(?=bod|one|time|where)/,
128
+ /^none(?=such)/, // none-such retains the silent e of none
129
+ /^goose(?=berr)/, // goose-berry and goose-berries
130
+ /^above(?=board|ground)/,
131
+ /^a?fore(?=clos|taste|said|mentioned)/,
132
+ /^ware(?=house|housing)/,
133
+ /^space(?=port|ship)/,
134
+ /^race(?=horse)/,
135
+ /^lake(?=side)/,
136
+ /^white(?=side)/,
137
+ /^state(?=side)/,
138
+ /^juke(?=box)/,
139
+ /^police(?=woman|women|man|men)/,
140
+ /^(?:sale|spoke)s?(?=woman|women|man|men)/,
141
+ ];
142
+
143
+ // Roots ending in ed keep their vowel; these are not regular past-tense suffixes.
144
+ const voicedEd = /(?:[^aeiouyl]led|^(?:hundred|kindred|sacred|wicked|rugged|ragged|wretched|naked|crooked|(?:a|em|death|river|water|flower|hot|sick|sun|sea)bed|(?:in|inter|thorough|cross|pure)bred|infrared|newlywed|premed))(?:['’](?:s|d|ll|re|ve))?$/;
145
+
146
+ // Find vowel spans in lowercase spelling; return character offsets, not syllables.
147
+ // Candidate vowel letters; ignored rules handle consonantal uses of u and y.
148
+ const isVowel = /[aeiouy]/;
149
+
150
+ // Ignore the character at match.index + offset. Rules may veto a whole word.
151
+ // Global patterns visit every occurrence; except prevents that rule from applying.
152
+ const ignored = [
153
+ { match: /[^aeiou]e(?=['’](?:s|d|ll|re|ve)$|$)/g, offset: 1, except: /[^aeiouyl]le(?:['’](?:s|d|ll|re|ve))?$/ }, // silent e
154
+ { match: /[^td]ed(?=['’](?:s|d|ll|re|ve)$|$)/g, offset: 1, except: voicedEd }, // walked vs hundred/bubbled
155
+ { match: /es(?=['’](?:s|d|ll|re|ve)$|$)/g, offset: 0, except: /([sxzcg]|ch|sh|[^aeiouyl]l)es(?:['’](?:s|d|ll|re|ve))?$/ }, // cakes vs boxes/tables
156
+ {
157
+ match: /e(?=ments?$|thing$|s?man$)/g,
158
+ offset: 0,
159
+ except: /(?:[^aeiouyl]le(?:ments?|thing|s?man)|elements?)$/,
160
+ }, // movement(s), but settlement(s) and element(s)
161
+ // Silent e stays silent before consonant suffixes, but syllabic -le survives.
162
+ {
163
+ match: /[^aeiou]e(?=ness(?:es)?$|l(?:y|iness)$|ful(?:ly)?$|less$|ships?$|some$)/g,
164
+ offset: 1,
165
+ except: /[^aeiouyl]le(?:ness(?:es)?|l(?:y|iness)|ful(?:ly)?|less|ships?|some)$/,
166
+ },
167
+ { match: /['’][rv]e$/g, offset: 2 }, // we're, they've: suffix e adds no syllable
168
+ { match: /qu(?=[aeiouy])/g, offset: 1 }, // consonantal u
169
+ { match: /que(?=s?$|ly$|ness$)/g, offset: 2 }, // opaque, opaques, opaquely
170
+ { match: /^y(?=[aeiou])/g, offset: 0 }, // yard, yellow
171
+ { match: /[aeiouwy]y(?=[aeiou])/g, offset: 1 }, // beyond, lawyer, doubled y
172
+ { match: /[aeiou]ny(?=[aeiou])/g, offset: 2, except: /^any/ }, // canyon, but anyone
173
+ { match: /gu(?=a)/g, offset: 1 }, // guard, language
174
+ { match: /busi(?=ness)/g, offset: 3 }, // businesses, agribusiness: the i stays silent
175
+ { match: /ever(?=y(?:body|thing|where|one|day|time|$))/g, offset: 2 }, // everywhere, everyone
176
+ ];
177
+
178
+ // Mc contributes a spoken syllable despite having no written vowel.
179
+ const syllabicPrefix = /^mc(?=[a-z])/;
180
+
181
+ // These consonants carry a syllable despite having no written vowel.
182
+ // The captured consonant starts the span; surrounding letters provide context.
183
+ const syllabicConsonants = [
184
+ /(?:[sxz]e?|[cs]h|[cg]e)['’](s)$/, // horse's, church's
185
+ /[ai]s(m)(?:s|['’]s)?$/, // chasm, criticism, and plurals/possessives
186
+ /[iy]th(m)(?:s|['’]s)?$/, // algorithm, rhythms
187
+ /[^aeiouyn](n)['’]t$/, // couldn't, wasn't; not can't or hasn't
188
+ /^(?:could|should|would|might|must)['’](v)e$/, // modal + have: would-'ve, unlike we've
189
+ ];
190
+
191
+ // Keep a boundary after these matched prefixes, even inside a vowel group.
192
+ const separate = [
193
+ /^(?:re)?cre(?=at[eio])/, // create, recreate, recreation (but not creature)
194
+ /^re(?=act)/, // re-act and re-action
195
+ /^re(?=absorb|app|elect|emerg|enter|establish|evalu|impos|invent|issue|us[ei])/, // reuse, reenter; not reed
196
+ /^pre(?=adolesc|ambl|arrang|eminen|empt|exist|ignition|invas)/, // preempt, preexisting; not preen
197
+ /^(?:micro|macro)(?=[aeiou])/, // micro-organism, macro-economic
198
+ /^co(?=operat|ordinat|alesc|erc)/, // cooperate, coercion; not cooperage
199
+ /^pro(?=act)/, // proactive; not approach
200
+ /^flu(?=id)/, // flu-id, unlike fruit
201
+ /^ru(?=in)/, // ru-in and ru-ined
202
+ /di(?=et|en[ct])/, // diet, audience, obedient; not died or soldier
203
+ /^ide(?=a)/, // idea, ideal; not deal
204
+ /^sovi(?=et)/, // Soviet and its derivatives retain separate i and e sounds
205
+ /^soci(?=et)/, // society, societies, societal; not social
206
+ /i(?=ell[aoi])/, // Gabriella, Gabrielli, -iello names; not field
207
+ /i(?=et(?:te)?s?$)/, // Juliet, Henriette
208
+ /^qui(?=et)/, // quiet keeps its separate vowels in quietly and quieter
209
+ /^acre(?=age)/, // acreage retains the vowel of acre before -age
210
+ /^sci(?=on)/, // scion has two vowels, unlike the ending in vision
211
+ /^intra(?=[aeiou])/, // intrauterine, intraocular
212
+ /re(?=al(?!ly|m))/, // cereal, reality; not really or realm
213
+ /[^q]u(?=en[ctdz])/, // fluent, innuendo, influenza; not subsequent
214
+ /[^g]u(?=ing)/, // suing, continuing; not plaguing or intriguing
215
+ /argu(?=ing)/, // arguing retains its pronounced u
216
+ /^lou(?=is)/, // Louis, Louisville
217
+ /[eo]u(?=illes?$)/, // separate the vowel groups in -euille/-ouille
218
+ /ou(?=ett)/, // silhouette, pirouette: ou and e are separate vowels
219
+ /o(?=as?$)/, // boa, boas; not boat
220
+ ];
221
+
222
+ // Match a bounded window around the current vowel: [i - before, i + after).
223
+ const joined = [
224
+ // Common vowel pairs share a span unless a separate rule overrides them.
225
+ { before: 1, after: 1, match: /^(ai|au|ay|ea|ee|ei|eu|ey|ie|oa|oe|oi|oo|ou|oy|ue|ui)$/ },
226
+ { before: 3, after: 2, match: /^llion/ }, // million, unlike lion
227
+ { before: 2, after: 3, match: /^[cst]i(?:on|an|al|ous)/ }, // nation, special, musician
228
+ { before: 2, after: 4, match: /^cia(?:s|['’]s)?$/ }, // final -cia blends; association and glaciation keep separate vowels
229
+ { before: 2, after: 1, match: /^eau$/ }, // extend ea to eau in beautiful
230
+ { before: 1, after: 3, match: /^ae(?:r|st)/ }, // aerospace, aesthetic
231
+ { before: 2, after: 3, match: /^[cg]eous/ }, // gorgeous, herbaceous; not courteous
232
+ { before: 3, after: 3, match: /^hteous/ }, // righteous and its suffixes
233
+ ];
234
+
235
+ // Mark pronounced vowel groups before choosing any consonant boundaries.
236
+ const vowelSpan = word => {
237
+ // Record ignored positions without deleting letters or shifting offsets.
238
+ const silent = new Set();
239
+ // A silent e remains silent inside a recognized compound.
240
+ compoundPrefixes.forEach(pattern => {
241
+ const match = pattern.exec(word);
242
+ if (match) {
243
+ silent.add(match[0].lastIndexOf('e'));
244
+ }
245
+ });
246
+ ignored.forEach(rule => {
247
+ if (!rule.except || !rule.except.test(word)) {
248
+ for (const match of word.matchAll(rule.match)) {
249
+ silent.add(match.index + rule.offset);
250
+ }
251
+ }
252
+ });
253
+ // Forced separations take precedence over the joined-vowel rules below.
254
+ const breaks = new Set();
255
+ separate.forEach(pattern => {
256
+ const match = pattern.exec(word);
257
+ if (match) {
258
+ breaks.add(match.index + match[0].length);
259
+ }
260
+ });
261
+ // Spans use slice-compatible offsets: start is inclusive, end is exclusive.
262
+ const groups = [];
263
+ const prefix = syllabicPrefix.exec(word);
264
+ if (prefix) {
265
+ groups.push({ start: 0, end: prefix[0].length });
266
+ }
267
+ // Mark the captured consonants without inserting letters or shifting offsets.
268
+ const voiced = new Set();
269
+ syllabicConsonants.forEach(pattern => {
270
+ const match = pattern.exec(word);
271
+ if (match) {
272
+ voiced.add(match.index + match[0].lastIndexOf(match[1]));
273
+ }
274
+ });
275
+ for (let i = 0; i < word.length; i += 1) {
276
+ if ((!isVowel.test(word[i]) && !voiced.has(i)) || silent.has(i)) {
277
+ continue
278
+ }
279
+ const previous = groups[groups.length - 1];
280
+ // Only adjacent pronounced vowels may extend an existing span.
281
+ const blended =
282
+ previous &&
283
+ previous.end === i &&
284
+ !breaks.has(i) &&
285
+ joined.some(rule => rule.match.test(word.slice(Math.max(0, i - rule.before), i + rule.after)));
286
+ if (blended) {
287
+ previous.end = i + 1;
288
+ } else {
289
+ groups.push({ start: i, end: i + 1 });
290
+ }
291
+ }
292
+ return groups
293
+ };
294
+
295
+ // Turn vowel spans into cut positions by assigning the consonants between them.
296
+ // Consonant clusters allowed to start a chunk; the picker checks longest first.
297
+ const onset = new Set([
298
+ 'bl', 'br', 'ch', 'cl', 'cr', 'dr', 'fl', 'fr', 'gl', 'gr',
299
+ 'ph', 'pl', 'pr', 'qu', 'sh', 'sk', 'sl', 'sm', 'sn', 'sp',
300
+ 'st', 'sw', 'th', 'tr', 'tw', 'wh', 'scr', 'shr', 'spl', 'spr', 'str', 'thr',
301
+ ]);
302
+
303
+ // Applied in order after onset placement; later rules take precedence.
304
+ // left/right are offsets from the surrounding vowel groups' edges.
305
+ // minLeft only moves a boundary forward, preserving a longer consonant cluster.
306
+ // gap matches intervening letters; tail matches the complete remaining spelling.
307
+ const adjustments = [
308
+ { minGap: 1, maxGap: 1, gap: /^x$/, left: 1 }, // box-es
309
+ { minGap: 2, gap: /^(.)\1/, minLeft: 1 }, // rab-bit
310
+ { minGap: 2, tail: /^ed$/, right: -1 }, // res-ted
311
+ { minGap: 2, gap: /^sh$/, tail: /^(es|ing)$/, right: 0 }, // wish-ing
312
+ { minGap: 2, gap: /l$/, tail: /^e(?:[sd]|['’](?:s|d|ll|re|ve))?$/, right: -2 }, // bot-tle, ta-bles, bub-bled
313
+ { minGap: 2, gap: /l$/, tail: /^e(?:ments?|ships?|ness(?:es)?|less|ful(?:ly)?|ly)(?:['’]s)?$/, right: -2 }, // set-tle-ment, bat-tle-ships
314
+ { minGap: 1, gap: /l$/, tail: /^ess(?:ly|ness(?:es)?)?$/, right: -1 }, // help-less: keep the suffix intact
315
+ { minGap: 3, gap: /^ssl$/, tail: /^y$/, right: -1 }, // help-less-ly, not help-les-sly
316
+ { minGap: 1, gap: /^(?:s|th)$/, tail: /^m(?:s|['’]s)?$/, right: 0 }, // chas-m, rhyth-m
317
+ { minGap: 1, tail: /^n['’]t$/, right: 0 }, // could-n't: keep the base consonants together
318
+ { minGap: 1, gap: /^[-‐‑–—]+$/, right: 0 }, // re-enter: keep the hyphen on the left
319
+ ];
320
+
321
+ // Choose one cut between each pair of vowel spans, preserving their count.
322
+ const pickBoundaries = (word, vowelSpans) => {
323
+ const cuts = [];
324
+ // Keep recognized compound components intact, such as side-walk and some-one.
325
+ const compoundCuts = compoundPrefixes.flatMap(pattern => {
326
+ const match = pattern.exec(word);
327
+ return match ? [match[0].length] : []
328
+ });
329
+ for (let i = 1; i < vowelSpans.length; i += 1) {
330
+ // The gap contains the letters not assigned to either vowel span.
331
+ const left = vowelSpans[i - 1].end;
332
+ const right = vowelSpans[i].start;
333
+ const gap = word.slice(left, right);
334
+ // Match full suffixes so long endings are not mistaken for shorter ones.
335
+ const tail = word.slice(right);
336
+ // Adjacent vowels split directly; a single consonant starts the next chunk.
337
+ let cut = left;
338
+ if (gap.length > 1) {
339
+ cut = right - 1;
340
+ // Prefer the longest recognized consonant cluster at the next chunk's start.
341
+ for (let size = Math.min(3, gap.length); size > 1; size -= 1) {
342
+ if (onset.has(gap.slice(-size))) {
343
+ cut = right - size;
344
+ break
345
+ }
346
+ }
347
+ }
348
+ // Apply every matching adjustment in order, allowing specific rules to override.
349
+ adjustments.forEach(rule => {
350
+ if (
351
+ gap.length < rule.minGap ||
352
+ (rule.maxGap !== undefined && gap.length > rule.maxGap) ||
353
+ (rule.gap && !rule.gap.test(gap)) ||
354
+ (rule.tail && !rule.tail.test(tail))
355
+ ) {
356
+ return
357
+ }
358
+ // Exact offsets replace the cut; minLeft only advances an earlier cut.
359
+ if (rule.left !== undefined) {
360
+ cut = left + rule.left;
361
+ } else if (rule.right !== undefined) {
362
+ cut = right + rule.right;
363
+ } else {
364
+ cut = Math.max(cut, left + rule.minLeft);
365
+ }
366
+ });
367
+ // A compound boundary overrides ordinary consonant placement within this gap.
368
+ compoundCuts.forEach(boundary => {
369
+ if (boundary >= left && boundary <= right) {
370
+ cut = boundary;
371
+ }
372
+ });
373
+ cuts.push(cut);
374
+ }
375
+ return cuts
376
+ };
377
+
378
+ const findCuts = str => {
379
+ // Exact exceptions and their plural forms take priority over spelling rules.
380
+ const found = checkExceptions(str);
381
+ if (found !== null) {
382
+ return found
383
+ }
384
+ // Keep apostrophe suffixes intact so vowel rules can recognize an extra syllable.
385
+ const vowelSpans = vowelSpan(str);
386
+ const cuts = pickBoundaries(str, vowelSpans);
387
+ // An exception may supply base-word cuts without overriding a syllabic suffix.
388
+ const key = str.replace(/['’](?:s|d|ll|re|ve)$/, '');
389
+ // Without a suffix, the initial lookup already checked this word.
390
+ if (key === str) {
391
+ return cuts
392
+ }
393
+ const baseCuts = checkExceptions(key);
394
+ if (baseCuts !== null) {
395
+ return baseCuts.concat(cuts.filter(cut => cut >= key.length))
396
+ }
397
+ return cuts
398
+ };
399
+
400
+ const trailingPunct = /(?<!\p{Punctuation})\p{Punctuation}+$/u;
401
+ const leadingPunct = /^[\p{Punctuation}$]+/u;
402
+
403
+ // Prepare spelling for rule matching; cuts are still applied to the original text.
404
+ const normalize = input => {
405
+ let txt = input.toLowerCase();
406
+ // Ignore trailing punctuation runs for analysis; internal apostrophes stay intact.
407
+ txt = txt.replace(trailingPunct, '');
408
+ // Keep the prefix length so cuts still align with the original spelling.
409
+ const prefix = txt.match(leadingPunct)?.[0] || '';
410
+ const clean = txt.slice(prefix.length);
411
+ return { clean, prefix }
412
+ };
413
+
414
+ const pushCutsBack = (cuts, prefix = '') => {
415
+ return cuts.map(cut => cut + prefix.length)
416
+ };
417
+
418
+ // split a string into chunks based on the given cuts
419
+ const applyCuts = (text, cuts) => {
420
+ let start = 0;
421
+ const parts = cuts.map(end => {
422
+ const part = text.slice(start, end);
423
+ start = end;
424
+ return part
425
+ });
426
+ // Cuts are interior boundaries; the final chunk always runs to the text's end.
427
+ parts.push(text.slice(start));
428
+ return parts
429
+ };
430
+
431
+ // Pass the original term text through; the tokenizer owns pre/post punctuation.
432
+ const splitTerm = (term, options = {}) => {
433
+ if (!term.text) {
434
+ return []
435
+ }
436
+ let str = term.text;
437
+ if (options.normalize) {
438
+ str = term.normal || term.text;
439
+ }
440
+ // we can split on a normalized string
441
+ const { clean, prefix } = normalize(str);
442
+ let cuts = findCuts(clean);
443
+ if (prefix && prefix.length > 0) {
444
+ cuts = pushCutsBack(cuts, prefix);
445
+ }
446
+ return applyCuts(str, cuts)
447
+ };
448
+
449
+ // Store syllables on each term; the public API collects them by sentence.
450
+ const syllables = function (view, options = {}) {
451
+ view.docs.forEach(terms => {
452
+ terms.forEach(term => {
453
+ term.syllables = splitTerm(term, options);
454
+ });
455
+ });
456
+ };
457
+
7
458
  //individual manipulations of the text
8
459
  const transformations = {
9
460
  dedup: (s) => {
10
461
  return s.replace(/([^c])\1/g, '$1')
11
462
  },
12
463
  dropInitialLetters: (s) => {
13
- if (s.match(/^(kn|gn|pn|ae|wr)/)) {
464
+ if (/^(kn|gn|pn|ae|wr)/.test(s)) {
14
465
  return s.substring(1, s.length - 1)
15
466
  }
16
467
  return s
@@ -86,299 +537,52 @@
86
537
  return s //.charAt(0) + s.substr(1, s.length).replace(/[aeiou]/g, '');
87
538
  },
88
539
  };
89
- var m = transformations;
90
540
 
91
541
  //a js version of the metaphone (#1) algorithm
542
+ //adapted from the work of Chris Umbel
543
+ // https://github.com/NaturalNode/natural/blob/master/lib/natural/phonetics/metaphone.js
544
+
92
545
 
93
546
  const metaphone = function (s) {
94
- s = m.dedup(s);
95
- s = m.dropInitialLetters(s);
96
- s = m.dropBafterMAtEnd(s);
97
- s = m.changeCK(s);
98
- s = m.cchange(s);
99
- s = m.dchange(s);
100
- s = m.dropG(s);
101
- s = m.changeG(s);
102
- s = m.dropH(s);
103
- s = m.changePH(s);
104
- s = m.changeQ(s);
105
- s = m.changeS(s);
106
- s = m.changeX(s);
107
- s = m.changeT(s);
108
- s = m.dropT(s);
109
- s = m.changeV(s);
110
- s = m.changeWH(s);
111
- s = m.dropW(s);
112
- s = m.dropY(s);
113
- s = m.changeZ(s);
114
- s = m.dropVowels(s);
547
+ s = transformations.dedup(s);
548
+ s = transformations.dropInitialLetters(s);
549
+ s = transformations.dropBafterMAtEnd(s);
550
+ s = transformations.changeCK(s);
551
+ s = transformations.cchange(s);
552
+ s = transformations.dchange(s);
553
+ s = transformations.dropG(s);
554
+ s = transformations.changeG(s);
555
+ s = transformations.dropH(s);
556
+ s = transformations.changePH(s);
557
+ s = transformations.changeQ(s);
558
+ s = transformations.changeS(s);
559
+ s = transformations.changeX(s);
560
+ s = transformations.changeT(s);
561
+ s = transformations.dropT(s);
562
+ s = transformations.changeV(s);
563
+ s = transformations.changeWH(s);
564
+ s = transformations.dropW(s);
565
+ s = transformations.dropY(s);
566
+ s = transformations.changeZ(s);
567
+ s = transformations.dropVowels(s);
115
568
  return s.trim()
116
569
  };
117
570
 
118
- var metaphone$1 = metaphone;
119
-
120
571
  const soundsLike = function (view) {
121
572
  view.docs.forEach(terms => {
122
573
  terms.forEach(term => {
123
- term.soundsLike = metaphone$1(term.normal || term.text);
574
+ term.soundsLike = metaphone(term.normal || term.text);
124
575
  });
125
576
  });
126
577
  };
127
578
 
128
- var soundsLike$1 = soundsLike;
129
-
130
- const starts_with_single_vowel_combos = /^(eu)/i;
131
- const joining_consonant_vowel = /^[^aeiou]e([^d]|$)/;
132
- const cvcv_same_consonant = /^([^aeiouy])[aeiouy]\1[aeiouy]/;
133
- const cvcv_same_vowel = /^[^aeiouy]([aeiouy])[^aeiouy]\1/;
134
- const cvcv_known_consonants = /^([tg][aeiouy]){2}/;
135
- const only_one_or_more_c = /^[^aeiouy]+$/;
136
-
137
- const ends_with_vowel$1 = /[aeiouy]$/;
138
- const starts_with_consonant_vowel$1 = /^[^aeiouy]h?[aeiouy]/;
139
-
140
- const ones = [
141
- /^[^aeiou]?ion/,
142
- /^[^aeiou]?ised/,
143
- /^[^aeiou]?iled/,
144
-
145
- // -ing, -ent
146
- /[aeiou]n[gt]$/,
147
-
148
- // -ate, -age
149
- /\wa[gt]e$/,
150
- ];
151
-
152
- //suffix fixes
153
- const postprocess = function (arr) {
154
- //trim whitespace
155
- arr = arr.map(function (w) {
156
- return w.trim()
157
- });
158
- arr = arr.filter(function (w) {
159
- return w !== ''
160
- });
161
- // if (arr.length > 2) {
162
- // return arr;
163
- // }
164
- let l = arr.length;
165
- if (l > 1) {
166
- let suffix = arr[l - 2] + arr[l - 1];
167
- for (let i = 0; i < ones.length; i++) {
168
- if (suffix.match(ones[i])) {
169
- arr[l - 2] = arr[l - 2] + arr[l - 1];
170
- arr.pop();
171
- }
172
- }
173
- }
174
-
175
- // since the open syllable detection is overzealous,
176
- // sometimes need to rejoin incorrect splits
177
- if (arr.length > 1) {
178
- let first_is_open =
179
- (arr[0].length === 1 || arr[0].match(starts_with_consonant_vowel$1)) &&
180
- arr[0].match(ends_with_vowel$1);
181
- let second_is_joining = arr[1].match(joining_consonant_vowel);
182
-
183
- if (first_is_open && second_is_joining) {
184
- let possible_combination = arr[0] + arr[1];
185
- let probably_separate_syllables =
186
- possible_combination.match(cvcv_same_consonant) ||
187
- possible_combination.match(cvcv_same_vowel) ||
188
- possible_combination.match(cvcv_known_consonants);
189
-
190
- if (!probably_separate_syllables) {
191
- arr[0] = arr[0] + arr[1];
192
- arr.splice(1, 1);
193
- }
194
- }
195
- }
196
-
197
- if (arr.length > 1) {
198
- let second_to_last_is_open =
199
- arr[arr.length - 2].match(starts_with_consonant_vowel$1) &&
200
- arr[arr.length - 2].match(ends_with_vowel$1);
201
- let last_is_joining =
202
- arr[arr.length - 1].match(joining_consonant_vowel) &&
203
- ones.every(re => !arr[arr.length - 1].match(re));
204
-
205
- if (second_to_last_is_open && last_is_joining) {
206
- let possible_combination = arr[arr.length - 2] + arr[arr.length - 1];
207
- let probably_separate_syllables =
208
- possible_combination.match(cvcv_same_consonant) ||
209
- possible_combination.match(cvcv_same_vowel) ||
210
- possible_combination.match(cvcv_known_consonants);
211
-
212
- if (!probably_separate_syllables) {
213
- arr[arr.length - 2] = arr[arr.length - 2] + arr[arr.length - 1];
214
- arr.splice(arr.length - 1, 1);
215
- }
216
- }
217
- }
218
-
219
- if (arr.length > 1) {
220
- let single = arr[0] + arr[1];
221
- if (single.match(starts_with_single_vowel_combos)) {
222
- arr[0] = single;
223
- arr.splice(1, 1);
224
- }
225
- }
226
-
227
- if (arr.length > 1) {
228
- if (arr[arr.length - 1].match(only_one_or_more_c)) {
229
- arr[arr.length - 2] = arr[arr.length - 2] + arr[arr.length - 1];
230
- arr.splice(arr.length - 1, 1);
231
- }
232
- }
233
-
234
- return arr
235
- };
236
- var postProcess = postprocess;
237
-
238
- //chop a string into pronounced syllables
239
-
240
- const all_spaces = / +/g;
241
- const ends_with_vowel = /[aeiouy]$/;
242
- const starts_with_consonant_vowel = /^[^aeiouy]h?[aeiouy]/;
243
- const starts_with_e_then_specials = /^e[sm]/;
244
- const starts_with_e = /^e/;
245
- const ends_with_noisy_vowel_combos = /(eo|eu|ia|oa|ua|ui)$/i;
246
- const aiouy = /[aiouy]/;
247
- const ends_with_ee = /ee$/;
248
- // const whitespace_dash = /\s\-/
249
-
250
- //method is nested because it's called recursively
251
- const doWord = function (w) {
252
- let all = [];
253
- let chars = w.split('');
254
- let before = '';
255
- let after = '';
256
- let current = '';
257
- for (let i = 0; i < chars.length; i++) {
258
- before = chars.slice(0, i).join('');
259
- current = chars[i];
260
- after = chars.slice(i + 1, chars.length).join('');
261
- let candidate = before + chars[i];
262
-
263
- //it's a consonant that comes after a vowel
264
- if (before.match(ends_with_vowel) && !current.match(ends_with_vowel)) {
265
- if (after.match(starts_with_e_then_specials)) {
266
- candidate += 'e';
267
- after = after.replace(starts_with_e, '');
268
- }
269
- all.push(candidate);
270
- return all.concat(doWord(after))
271
- }
272
-
273
- //unblended vowels ('noisy' vowel combinations)
274
- if (candidate.match(ends_with_noisy_vowel_combos)) {
275
- //'io' is noisy, not in 'ion'
276
- all.push(before);
277
- all.push(current);
278
- return all.concat(doWord(after)) //recursion
279
- }
280
-
281
- // if candidate is followed by a CV, assume consecutive open syllables
282
- if (candidate.match(ends_with_vowel) && after.match(starts_with_consonant_vowel)) {
283
- all.push(candidate);
284
- return all.concat(doWord(after))
285
- }
286
- }
287
- //if still running, end last syllable
288
- if (w.match(aiouy) || w.match(ends_with_ee)) {
289
- //allow silent trailing e
290
- all.push(w);
291
- } else if (w) {
292
- let last = all.length - 1;
293
- if (last < 0) {
294
- last = 0;
295
- }
296
- all[last] = (all[last] || '') + w; //append it to the last one
297
- }
298
- return all
299
- };
300
-
301
- let syllables$2 = function (str) {
302
- let all = [];
303
- if (!str) {
304
- return all
305
- }
306
- str = str.replace(/[.,?]/g, '');
307
- str.split(all_spaces).map(s => {
308
- all = all.concat(doWord(s));
309
- });
310
-
311
- // str.split(whitespace_dash).forEach(doWord)
312
- all = postProcess(all);
313
-
314
- //for words like 'tree' and 'free'
315
- if (all.length === 0) {
316
- all = [str];
317
- }
318
- //filter blanks
319
- all = all.filter(s => s);
320
-
321
- return all
322
- };
323
-
324
- // console.log(syllables('civilised'))
325
-
326
- var getSyllables = syllables$2;
327
-
328
- // const defaultObj = { normal: true, text: true, terms: false }
329
-
330
- const syllables = function (view) {
331
- view.docs.forEach(terms => {
332
- terms.forEach(term => {
333
- term.syllables = getSyllables(term.normal || term.text);
334
- });
335
- });
336
- };
337
-
338
- var syllables$1 = syllables;
339
-
340
- var compute = {
341
- soundsLike: soundsLike$1,
342
- syllables: syllables$1
343
- };
344
-
345
- const api = function (View) {
346
- /** */
347
- View.prototype.syllables = function () {
348
- this.compute('syllables');
349
- let all = [];
350
- this.docs.forEach(terms => {
351
- let some = [];
352
- terms.forEach(term => {
353
- some = some.concat(term.syllables);
354
- });
355
- if (some.length > 0) {
356
- all.push(some);
357
- }
358
- });
359
- return all
360
- };
361
- /** */
362
- View.prototype.soundsLike = function () {
363
- this.compute('soundsLike');
364
- let all = [];
365
- this.docs.forEach(terms => {
366
- let some = [];
367
- terms.forEach(term => {
368
- some = some.concat(term.soundsLike);
369
- });
370
- if (some.length > 0) {
371
- all.push(some);
372
- }
373
- });
374
- return all
375
- };
376
- };
377
- var api$1 = api;
378
-
379
579
  var plugin = {
380
- api: api$1,
381
- compute
580
+ version,
581
+ api,
582
+ compute: {
583
+ syllables,
584
+ soundsLike,
585
+ },
382
586
  };
383
587
 
384
588
  return plugin;