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.
- package/LICENSE +21 -0
- package/README.md +89 -21
- package/builds/compromise-speech.cjs +483 -279
- package/builds/compromise-speech.min.js +1 -1
- package/builds/compromise-speech.mjs +1 -1
- package/package.json +47 -26
- package/src/_version.js +1 -0
- package/src/api.js +5 -5
- package/src/plugin.js +9 -3
- package/src/{compute/soundsLike → sounds-like}/transformations.js +1 -1
- package/src/syllables/index.js +12 -0
- package/src/syllables/split/_lib.js +32 -0
- package/src/syllables/split/find-cuts/00-exceptions.js +81 -0
- package/src/syllables/split/find-cuts/01-vowel-span.js +155 -0
- package/src/syllables/split/find-cuts/02-boundaries.js +86 -0
- package/src/syllables/split/find-cuts/_compound-prefixes.js +22 -0
- package/src/syllables/split/find-cuts/index.js +27 -0
- package/src/syllables/split/index.js +22 -0
- package/types/index.d.cts +9 -0
- package/{index.d.ts → types/index.d.ts} +1 -1
- package/demo/index.html +0 -28
- package/rollup.config.js +0 -23
- package/scratch.js +0 -17
- package/src/compute/index.js +0 -7
- package/src/compute/syllables/index.js +0 -13
- package/src/compute/syllables/postProcess.js +0 -107
- package/src/compute/syllables/syllables.js +0 -90
- package/tests/_lib.js +0 -15
- package/tests/soundsLike.test.js +0 -21
- package/tests/syllables.test.js +0 -73
- /package/src/{compute/soundsLike → sounds-like}/index.js +0 -0
- /package/src/{compute/soundsLike → sounds-like}/metaphone.js +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).compromiseSpeech=t()}(this,function(){"use strict";const e={acre:[1],cafe:[2],abalone:[2,3,5],aborigine:[2,3,6,7],comanche:[2,5],dolce:[3],acacia:[1,3],fuchsia:[4],concierge:[3,5],concierges:[3,5],comedienne:[2,4,6],massacre:[3,5],massacred:[3,5],massacring:[3,5],aisle:[],isle:[],wednesday:[6],buoyant:[4],buoyancy:[4,6],acquiesce:[2,5],acquiesced:[2,5],acquiescent:[2,5,7],acquiescence:[2,5,7],people:[3],queue:[],business:[4],quiet:[3],science:[3],poet:[2],poetry:[2,4],every:[3],everything:[3,5],everybody:[3,5,7],ilyich:[2],juan:[],wyatt:[2],batesville:[5],belleville:[5],bridgeport:[6],cambridgeport:[3,9],cambridgeside:[3,9],janesville:[5],roseberry:[4,7],roseville:[4],belge:[3],canadienne:[2,4,6],mildred:[3],nietzsche:[5],odea:[1,3],zoologist:[2,4,5],zoology:[2,4,5]},t=t=>{if(Object.hasOwn(e,t))return e[t];const a=t.replace(/s$/,"");return Object.hasOwn(e,a)?/[csxz]e$/.test(a)?e[a].concat(a.length-2):e[a]:null},a=[/^note(?=book)/,/^side(?=kick|line|show|step|stream|track|walk|water|way|winder|wise)/,/^some(?=bod|one|time|where)/,/^none(?=such)/,/^goose(?=berr)/,/^above(?=board|ground)/,/^a?fore(?=clos|taste|said|mentioned)/,/^ware(?=house|housing)/,/^space(?=port|ship)/,/^race(?=horse)/,/^lake(?=side)/,/^white(?=side)/,/^state(?=side)/,/^juke(?=box)/,/^police(?=woman|women|man|men)/,/^(?:sale|spoke)s?(?=woman|women|man|men)/],s=/[aeiouy]/,o=[{match:/[^aeiou]e(?=['’](?:s|d|ll|re|ve)$|$)/g,offset:1,except:/[^aeiouyl]le(?:['’](?:s|d|ll|re|ve))?$/},{match:/[^td]ed(?=['’](?:s|d|ll|re|ve)$|$)/g,offset:1,except:/(?:[^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))?$/},{match:/es(?=['’](?:s|d|ll|re|ve)$|$)/g,offset:0,except:/([sxzcg]|ch|sh|[^aeiouyl]l)es(?:['’](?:s|d|ll|re|ve))?$/},{match:/e(?=ments?$|thing$|s?man$)/g,offset:0,except:/(?:[^aeiouyl]le(?:ments?|thing|s?man)|elements?)$/},{match:/[^aeiou]e(?=ness(?:es)?$|l(?:y|iness)$|ful(?:ly)?$|less$|ships?$|some$)/g,offset:1,except:/[^aeiouyl]le(?:ness(?:es)?|l(?:y|iness)|ful(?:ly)?|less|ships?|some)$/},{match:/['’][rv]e$/g,offset:2},{match:/qu(?=[aeiouy])/g,offset:1},{match:/que(?=s?$|ly$|ness$)/g,offset:2},{match:/^y(?=[aeiou])/g,offset:0},{match:/[aeiouwy]y(?=[aeiou])/g,offset:1},{match:/[aeiou]ny(?=[aeiou])/g,offset:2,except:/^any/},{match:/gu(?=a)/g,offset:1},{match:/busi(?=ness)/g,offset:3},{match:/ever(?=y(?:body|thing|where|one|day|time|$))/g,offset:2}],n=/^mc(?=[a-z])/,i=[/(?:[sxz]e?|[cs]h|[cg]e)['’](s)$/,/[ai]s(m)(?:s|['’]s)?$/,/[iy]th(m)(?:s|['’]s)?$/,/[^aeiouyn](n)['’]t$/,/^(?:could|should|would|might|must)['’](v)e$/],c=[/^(?:re)?cre(?=at[eio])/,/^re(?=act)/,/^re(?=absorb|app|elect|emerg|enter|establish|evalu|impos|invent|issue|us[ei])/,/^pre(?=adolesc|ambl|arrang|eminen|empt|exist|ignition|invas)/,/^(?:micro|macro)(?=[aeiou])/,/^co(?=operat|ordinat|alesc|erc)/,/^pro(?=act)/,/^flu(?=id)/,/^ru(?=in)/,/di(?=et|en[ct])/,/^ide(?=a)/,/^sovi(?=et)/,/^soci(?=et)/,/i(?=ell[aoi])/,/i(?=et(?:te)?s?$)/,/^qui(?=et)/,/^acre(?=age)/,/^sci(?=on)/,/^intra(?=[aeiou])/,/re(?=al(?!ly|m))/,/[^q]u(?=en[ctdz])/,/[^g]u(?=ing)/,/argu(?=ing)/,/^lou(?=is)/,/[eo]u(?=illes?$)/,/ou(?=ett)/,/o(?=as?$)/],r=[{before:1,after:1,match:/^(ai|au|ay|ea|ee|ei|eu|ey|ie|oa|oe|oi|oo|ou|oy|ue|ui)$/},{before:3,after:2,match:/^llion/},{before:2,after:3,match:/^[cst]i(?:on|an|al|ous)/},{before:2,after:4,match:/^cia(?:s|['’]s)?$/},{before:2,after:1,match:/^eau$/},{before:1,after:3,match:/^ae(?:r|st)/},{before:2,after:3,match:/^[cg]eous/},{before:3,after:3,match:/^hteous/}],l=new Set(["bl","br","ch","cl","cr","dr","fl","fr","gl","gr","ph","pl","pr","qu","sh","sk","sl","sm","sn","sp","st","sw","th","tr","tw","wh","scr","shr","spl","spr","str","thr"]),h=[{minGap:1,maxGap:1,gap:/^x$/,left:1},{minGap:2,gap:/^(.)\1/,minLeft:1},{minGap:2,tail:/^ed$/,right:-1},{minGap:2,gap:/^sh$/,tail:/^(es|ing)$/,right:0},{minGap:2,gap:/l$/,tail:/^e(?:[sd]|['’](?:s|d|ll|re|ve))?$/,right:-2},{minGap:2,gap:/l$/,tail:/^e(?:ments?|ships?|ness(?:es)?|less|ful(?:ly)?|ly)(?:['’]s)?$/,right:-2},{minGap:1,gap:/l$/,tail:/^ess(?:ly|ness(?:es)?)?$/,right:-1},{minGap:3,gap:/^ssl$/,tail:/^y$/,right:-1},{minGap:1,gap:/^(?:s|th)$/,tail:/^m(?:s|['’]s)?$/,right:0},{minGap:1,tail:/^n['’]t$/,right:0},{minGap:1,gap:/^[-‐‑–—]+$/,right:0}],u=e=>{const u=t(e);if(null!==u)return u;const g=((e,t)=>{const s=[],o=a.flatMap(t=>{const a=t.exec(e);return a?[a[0].length]:[]});for(let a=1;a<t.length;a+=1){const n=t[a-1].end,i=t[a].start,c=e.slice(n,i),r=e.slice(i);let u=n;if(c.length>1){u=i-1;for(let e=Math.min(3,c.length);e>1;e-=1)if(l.has(c.slice(-e))){u=i-e;break}}h.forEach(e=>{c.length<e.minGap||void 0!==e.maxGap&&c.length>e.maxGap||e.gap&&!e.gap.test(c)||e.tail&&!e.tail.test(r)||(u=void 0!==e.left?n+e.left:void 0!==e.right?i+e.right:Math.max(u,n+e.minLeft))}),o.forEach(e=>{e>=n&&e<=i&&(u=e)}),s.push(u)}return s})(e,(e=>{const t=new Set;a.forEach(a=>{const s=a.exec(e);s&&t.add(s[0].lastIndexOf("e"))}),o.forEach(a=>{if(!a.except||!a.except.test(e))for(const s of e.matchAll(a.match))t.add(s.index+a.offset)});const l=new Set;c.forEach(t=>{const a=t.exec(e);a&&l.add(a.index+a[0].length)});const h=[],u=n.exec(e);u&&h.push({start:0,end:u[0].length});const g=new Set;i.forEach(t=>{const a=t.exec(e);a&&g.add(a.index+a[0].lastIndexOf(a[1]))});for(let a=0;a<e.length;a+=1){if(!s.test(e[a])&&!g.has(a)||t.has(a))continue;const o=h[h.length-1];o&&o.end===a&&!l.has(a)&&r.some(t=>t.match.test(e.slice(Math.max(0,a-t.before),a+t.after)))?o.end=a+1:h.push({start:a,end:a+1})}return h})(e)),p=e.replace(/['’](?:s|d|ll|re|ve)$/,"");if(p===e)return g;const f=t(p);return null!==f?f.concat(g.filter(e=>e>=p.length)):g},g=/(?<!\p{Punctuation})\p{Punctuation}+$/u,p=/^[\p{Punctuation}$]+/u,f=(e,t={})=>{if(!e.text)return[];let a=e.text;t.normalize&&(a=e.normal||e.text);const{clean:s,prefix:o}=(e=>{let t=e.toLowerCase();t=t.replace(g,"");const a=t.match(p)?.[0]||"";return{clean:t.slice(a.length),prefix:a}})(a);let n=u(s);return o&&o.length>0&&(n=((e,t="")=>e.map(e=>e+t.length))(n,o)),((e,t)=>{let a=0;const s=t.map(t=>{const s=e.slice(a,t);return a=t,s});return s.push(e.slice(a)),s})(a,n)},d=e=>e.replace(/([^c])\1/g,"$1"),m=e=>/^(kn|gn|pn|ae|wr)/.test(e)?e.substring(1,e.length-1):e,$=e=>e.replace(/mb$/,"m"),y=e=>(e=(e=(e=e.replace(/([^s]|^)(c)(h)/g,"$1x$3").trim()).replace(/cia/g,"xia")).replace(/c([iey])/g,"s$1")).replace(/c/g,"k"),b=e=>(e=e.replace(/d(ge|gy|gi)/g,"j$1")).replace(/d/g,"t"),x=e=>(e=e.replace(/gh(^$|[^aeiou])/g,"h$1")).replace(/g(n|ned)$/g,"$1"),w=e=>(e=(e=(e=e.replace(/gh/g,"f")).replace(/([^g]|^)(g)([iey])/g,"$1j$3")).replace(/gg/g,"g")).replace(/g/g,"k"),v=e=>e.replace(/([aeiou])h([^aeiou]|$)/g,"$1$2"),k=e=>e.replace(/ck/g,"k"),G=e=>e.replace(/ph/g,"f"),E=e=>e.replace(/q/g,"k"),q=e=>e.replace(/s(h|io|ia)/g,"x$1"),z=e=>e=e.replace(/t(ia[^n]|io)/g,"x$1"),j=e=>e.replace(/tch/g,"ch"),L=e=>e.replace(/v/g,"f"),O=e=>e.replace(/^wh/,"w"),S=e=>e.replace(/w([^aeiou]|$)/g,"$1"),M=e=>(e=e.replace(/^x/,"s")).replace(/x/g,"ks"),P=e=>e.replace(/y([^aeiou]|$)/g,"$1"),I=e=>e.replace(/z/,"s"),T=e=>e;return{version:"0.2.0",api:function(e){e.prototype.syllables=function(e={}){this.compute("syllables",e);const t=[];return this.docs.forEach(e=>{let a=[];e.forEach(e=>{a=a.concat(e.syllables)}),a.length>0&&t.push(a)}),t},e.prototype.soundsLike=function(){this.compute("soundsLike");const e=[];return this.docs.forEach(t=>{let a=[];t.forEach(e=>{a=a.concat(e.soundsLike)}),a.length>0&&e.push(a)}),e}},compute:{syllables:function(e,t={}){e.docs.forEach(e=>{e.forEach(e=>{e.syllables=f(e,t)})})},soundsLike:function(e){e.docs.forEach(e=>{e.forEach(e=>{var t;e.soundsLike=(t=e.normal||e.text,t=d(t),t=m(t),t=$(t),t=k(t),t=y(t),t=b(t),t=x(t),t=w(t),t=v(t),t=G(t),t=E(t),t=q(t),t=M(t),t=z(t),t=j(t),t=L(t),t=O(t),t=S(t),t=P(t),t=I(t),(t=T(t)).trim())})})}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
const e={acre:[1],cafe:[2],abalone:[2,3,5],aborigine:[2,3,6,7],comanche:[2,5],dolce:[3],acacia:[1,3],fuchsia:[4],concierge:[3,5],concierges:[3,5],comedienne:[2,4,6],massacre:[3,5],massacred:[3,5],massacring:[3,5],aisle:[],isle:[],wednesday:[6],buoyant:[4],buoyancy:[4,6],acquiesce:[2,5],acquiesced:[2,5],acquiescent:[2,5,7],acquiescence:[2,5,7],people:[3],queue:[],business:[4],quiet:[3],science:[3],poet:[2],poetry:[2,4],every:[3],everything:[3,5],everybody:[3,5,7],ilyich:[2],juan:[],wyatt:[2],batesville:[5],belleville:[5],bridgeport:[6],cambridgeport:[3,9],cambridgeside:[3,9],janesville:[5],roseberry:[4,7],roseville:[4],belge:[3],canadienne:[2,4,6],mildred:[3],nietzsche:[5],odea:[1,3],zoologist:[2,4,5],zoology:[2,4,5]},t=t=>{if(Object.hasOwn(e,t))return e[t];const a=t.replace(/s$/,"");return Object.hasOwn(e,a)?/[csxz]e$/.test(a)?e[a].concat(a.length-2):e[a]:null},a=[/^note(?=book)/,/^side(?=kick|line|show|step|stream|track|walk|water|way|winder|wise)/,/^some(?=bod|one|time|where)/,/^none(?=such)/,/^goose(?=berr)/,/^above(?=board|ground)/,/^a?fore(?=clos|taste|said|mentioned)/,/^ware(?=house|housing)/,/^space(?=port|ship)/,/^race(?=horse)/,/^lake(?=side)/,/^white(?=side)/,/^state(?=side)/,/^juke(?=box)/,/^police(?=woman|women|man|men)/,/^(?:sale|spoke)s?(?=woman|women|man|men)/],s=/[aeiouy]/,r=[{match:/[^aeiou]e(?=['’](?:s|d|ll|re|ve)$|$)/g,offset:1,except:/[^aeiouyl]le(?:['’](?:s|d|ll|re|ve))?$/},{match:/[^td]ed(?=['’](?:s|d|ll|re|ve)$|$)/g,offset:1,except:/(?:[^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))?$/},{match:/es(?=['’](?:s|d|ll|re|ve)$|$)/g,offset:0,except:/([sxzcg]|ch|sh|[^aeiouyl]l)es(?:['’](?:s|d|ll|re|ve))?$/},{match:/e(?=ments?$|thing$|s?man$)/g,offset:0,except:/(?:[^aeiouyl]le(?:ments?|thing|s?man)|elements?)$/},{match:/[^aeiou]e(?=ness(?:es)?$|l(?:y|iness)$|ful(?:ly)?$|less$|ships?$|some$)/g,offset:1,except:/[^aeiouyl]le(?:ness(?:es)?|l(?:y|iness)|ful(?:ly)?|less|ships?|some)$/},{match:/['’][rv]e$/g,offset:2},{match:/qu(?=[aeiouy])/g,offset:1},{match:/que(?=s?$|ly$|ness$)/g,offset:2},{match:/^y(?=[aeiou])/g,offset:0},{match:/[aeiouwy]y(?=[aeiou])/g,offset:1},{match:/[aeiou]ny(?=[aeiou])/g,offset:2,except:/^any/},{match:/gu(?=a)/g,offset:1},{match:/busi(?=ness)/g,offset:3},{match:/ever(?=y(?:body|thing|where|one|day|time|$))/g,offset:2}],o=/^mc(?=[a-z])/,c=[/(?:[sxz]e?|[cs]h|[cg]e)['’](s)$/,/[ai]s(m)(?:s|['’]s)?$/,/[iy]th(m)(?:s|['’]s)?$/,/[^aeiouyn](n)['’]t$/,/^(?:could|should|would|might|must)['’](v)e$/],n=[/^(?:re)?cre(?=at[eio])/,/^re(?=act)/,/^re(?=absorb|app|elect|emerg|enter|establish|evalu|impos|invent|issue|us[ei])/,/^pre(?=adolesc|ambl|arrang|eminen|empt|exist|ignition|invas)/,/^(?:micro|macro)(?=[aeiou])/,/^co(?=operat|ordinat|alesc|erc)/,/^pro(?=act)/,/^flu(?=id)/,/^ru(?=in)/,/di(?=et|en[ct])/,/^ide(?=a)/,/^sovi(?=et)/,/^soci(?=et)/,/i(?=ell[aoi])/,/i(?=et(?:te)?s?$)/,/^qui(?=et)/,/^acre(?=age)/,/^sci(?=on)/,/^intra(?=[aeiou])/,/re(?=al(?!ly|m))/,/[^q]u(?=en[ctdz])/,/[^g]u(?=ing)/,/argu(?=ing)/,/^lou(?=is)/,/[eo]u(?=illes?$)/,/ou(?=ett)/,/o(?=as?$)/],i=[{before:1,after:1,match:/^(ai|au|ay|ea|ee|ei|eu|ey|ie|oa|oe|oi|oo|ou|oy|ue|ui)$/},{before:3,after:2,match:/^llion/},{before:2,after:3,match:/^[cst]i(?:on|an|al|ous)/},{before:2,after:4,match:/^cia(?:s|['’]s)?$/},{before:2,after:1,match:/^eau$/},{before:1,after:3,match:/^ae(?:r|st)/},{before:2,after:3,match:/^[cg]eous/},{before:3,after:3,match:/^hteous/}],l=new Set(["bl","br","ch","cl","cr","dr","fl","fr","gl","gr","ph","pl","pr","qu","sh","sk","sl","sm","sn","sp","st","sw","th","tr","tw","wh","scr","shr","spl","spr","str","thr"]),h=[{minGap:1,maxGap:1,gap:/^x$/,left:1},{minGap:2,gap:/^(.)\1/,minLeft:1},{minGap:2,tail:/^ed$/,right:-1},{minGap:2,gap:/^sh$/,tail:/^(es|ing)$/,right:0},{minGap:2,gap:/l$/,tail:/^e(?:[sd]|['’](?:s|d|ll|re|ve))?$/,right:-2},{minGap:2,gap:/l$/,tail:/^e(?:ments?|ships?|ness(?:es)?|less|ful(?:ly)?|ly)(?:['’]s)?$/,right:-2},{minGap:1,gap:/l$/,tail:/^ess(?:ly|ness(?:es)?)?$/,right:-1},{minGap:3,gap:/^ssl$/,tail:/^y$/,right:-1},{minGap:1,gap:/^(?:s|th)$/,tail:/^m(?:s|['’]s)?$/,right:0},{minGap:1,tail:/^n['’]t$/,right:0},{minGap:1,gap:/^[-‐‑–—]+$/,right:0}],g=e=>{const g=t(e);if(null!==g)return g;const u=((e,t)=>{const s=[],r=a.flatMap(t=>{const a=t.exec(e);return a?[a[0].length]:[]});for(let a=1;a<t.length;a+=1){const o=t[a-1].end,c=t[a].start,n=e.slice(o,c),i=e.slice(c);let g=o;if(n.length>1){g=c-1;for(let e=Math.min(3,n.length);e>1;e-=1)if(l.has(n.slice(-e))){g=c-e;break}}h.forEach(e=>{n.length<e.minGap||void 0!==e.maxGap&&n.length>e.maxGap||e.gap&&!e.gap.test(n)||e.tail&&!e.tail.test(i)||(g=void 0!==e.left?o+e.left:void 0!==e.right?c+e.right:Math.max(g,o+e.minLeft))}),r.forEach(e=>{e>=o&&e<=c&&(g=e)}),s.push(g)}return s})(e,(e=>{const t=new Set;a.forEach(a=>{const s=a.exec(e);s&&t.add(s[0].lastIndexOf("e"))}),r.forEach(a=>{if(!a.except||!a.except.test(e))for(const s of e.matchAll(a.match))t.add(s.index+a.offset)});const l=new Set;n.forEach(t=>{const a=t.exec(e);a&&l.add(a.index+a[0].length)});const h=[],g=o.exec(e);g&&h.push({start:0,end:g[0].length});const u=new Set;c.forEach(t=>{const a=t.exec(e);a&&u.add(a.index+a[0].lastIndexOf(a[1]))});for(let a=0;a<e.length;a+=1){if(!s.test(e[a])&&!u.has(a)||t.has(a))continue;const r=h[h.length-1];r&&r.end===a&&!l.has(a)&&i.some(t=>t.match.test(e.slice(Math.max(0,a-t.before),a+t.after)))?r.end=a+1:h.push({start:a,end:a+1})}return h})(e)),p=e.replace(/['’](?:s|d|ll|re|ve)$/,"");if(p===e)return u;const f=t(p);return null!==f?f.concat(u.filter(e=>e>=p.length)):u},u=/(?<!\p{Punctuation})\p{Punctuation}+$/u,p=/^[\p{Punctuation}$]+/u,f=(e,t={})=>{if(!e.text)return[];let a=e.text;t.normalize&&(a=e.normal||e.text);const{clean:s,prefix:r}=(e=>{let t=e.toLowerCase();t=t.replace(u,"");const a=t.match(p)?.[0]||"";return{clean:t.slice(a.length),prefix:a}})(a);let o=g(s);return r&&r.length>0&&(o=((e,t="")=>e.map(e=>e+t.length))(o,r)),((e,t)=>{let a=0;const s=t.map(t=>{const s=e.slice(a,t);return a=t,s});return s.push(e.slice(a)),s})(a,o)},m=e=>e.replace(/([^c])\1/g,"$1"),d=e=>/^(kn|gn|pn|ae|wr)/.test(e)?e.substring(1,e.length-1):e,$=e=>e.replace(/mb$/,"m"),y=e=>(e=(e=(e=e.replace(/([^s]|^)(c)(h)/g,"$1x$3").trim()).replace(/cia/g,"xia")).replace(/c([iey])/g,"s$1")).replace(/c/g,"k"),b=e=>(e=e.replace(/d(ge|gy|gi)/g,"j$1")).replace(/d/g,"t"),x=e=>(e=e.replace(/gh(^$|[^aeiou])/g,"h$1")).replace(/g(n|ned)$/g,"$1"),w=e=>(e=(e=(e=e.replace(/gh/g,"f")).replace(/([^g]|^)(g)([iey])/g,"$1j$3")).replace(/gg/g,"g")).replace(/g/g,"k"),v=e=>e.replace(/([aeiou])h([^aeiou]|$)/g,"$1$2"),k=e=>e.replace(/ck/g,"k"),G=e=>e.replace(/ph/g,"f"),E=e=>e.replace(/q/g,"k"),q=e=>e.replace(/s(h|io|ia)/g,"x$1"),z=e=>e=e.replace(/t(ia[^n]|io)/g,"x$1"),L=e=>e.replace(/tch/g,"ch"),j=e=>e.replace(/v/g,"f"),O=e=>e.replace(/^wh/,"w"),M=e=>e.replace(/w([^aeiou]|$)/g,"$1"),S=e=>(e=e.replace(/^x/,"s")).replace(/x/g,"ks"),P=e=>e.replace(/y([^aeiou]|$)/g,"$1"),I=e=>e.replace(/z/,"s"),A=e=>e;var C={version:"0.2.0",api:function(e){e.prototype.syllables=function(e={}){this.compute("syllables",e);const t=[];return this.docs.forEach(e=>{let a=[];e.forEach(e=>{a=a.concat(e.syllables)}),a.length>0&&t.push(a)}),t},e.prototype.soundsLike=function(){this.compute("soundsLike");const e=[];return this.docs.forEach(t=>{let a=[];t.forEach(e=>{a=a.concat(e.soundsLike)}),a.length>0&&e.push(a)}),e}},compute:{syllables:function(e,t={}){e.docs.forEach(e=>{e.forEach(e=>{e.syllables=f(e,t)})})},soundsLike:function(e){e.docs.forEach(e=>{e.forEach(e=>{var t;e.soundsLike=(t=e.normal||e.text,t=m(t),t=d(t),t=$(t),t=k(t),t=y(t),t=b(t),t=x(t),t=w(t),t=v(t),t=G(t),t=E(t),t=q(t),t=S(t),t=z(t),t=L(t),t=j(t),t=O(t),t=M(t),t=P(t),t=I(t),(t=A(t)).trim())})})}}};export{C as default};
|
package/package.json
CHANGED
|
@@ -1,38 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "compromise-speech",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Syllable counting and pronunciation estimates for compromise",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/spencermountain/compromise.git",
|
|
9
|
+
"directory": "plugins/speech"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/spencermountain/compromise/tree/master/plugins/speech",
|
|
12
|
+
"author": "Spencer Kelly <spencermountain@gmail.com> (https://spencermountain.dev)",
|
|
9
13
|
"type": "module",
|
|
10
|
-
"types": "index.d.ts",
|
|
11
|
-
"sideEffects": false,
|
|
12
14
|
"exports": {
|
|
13
15
|
".": {
|
|
14
|
-
"import":
|
|
15
|
-
|
|
16
|
+
"import": {
|
|
17
|
+
"types": "./types/index.d.ts",
|
|
18
|
+
"default": "./src/plugin.js"
|
|
19
|
+
},
|
|
20
|
+
"require": {
|
|
21
|
+
"types": "./types/index.d.cts",
|
|
22
|
+
"default": "./builds/compromise-speech.cjs"
|
|
23
|
+
}
|
|
16
24
|
}
|
|
17
25
|
},
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
"test": "tape \"./tests/**/*.test.js\" | tap-dancer --color always",
|
|
25
|
-
"testb": "TESTENV=prod tape \"./tests/**/*.test.js\" | tap-dancer --color always",
|
|
26
|
-
"watch": "amble ./scratch.js",
|
|
27
|
-
"build": "rollup -c --silent"
|
|
26
|
+
"main": "./builds/compromise-speech.cjs",
|
|
27
|
+
"module": "./builds/compromise-speech.mjs",
|
|
28
|
+
"types": "./types/index.d.ts",
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
28
32
|
},
|
|
29
|
-
"
|
|
30
|
-
"
|
|
33
|
+
"files": [
|
|
34
|
+
"types/",
|
|
35
|
+
"builds/",
|
|
36
|
+
"src/"
|
|
31
37
|
],
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@rollup/plugin-node-resolve": "16.0.3",
|
|
40
|
+
"@rollup/plugin-terser": "1.0.0",
|
|
41
|
+
"rollup": "4.64.8",
|
|
42
|
+
"rollup-plugin-filesize-check": "2.0.1",
|
|
43
|
+
"tape": "5.10.3",
|
|
44
|
+
"cross-env": "10.1.0",
|
|
45
|
+
"tap-dancer": "0.4.4"
|
|
46
|
+
},
|
|
32
47
|
"peerDependencies": {
|
|
33
|
-
"compromise": "
|
|
48
|
+
"compromise": "^14.19.0"
|
|
34
49
|
},
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
50
|
+
"unpkg": "./builds/compromise-speech.min.js",
|
|
51
|
+
"scripts": {
|
|
52
|
+
"test": "tape \"./tests/**/*.test.js\" | tap-dancer --color always",
|
|
53
|
+
"testb": "cross-env TESTENV=prod tape \"./tests/**/*.test.js\" | tap-dancer --color always",
|
|
54
|
+
"test:hmm": "tape \"./tests/**/*.hmm.js\" | tap-dancer --color always",
|
|
55
|
+
"watch": "node --watch ./scratch.js",
|
|
56
|
+
"version": "node ./scripts/version.js",
|
|
57
|
+
"build": "node ./scripts/version.js && rollup -c --silent"
|
|
58
|
+
}
|
|
38
59
|
}
|
package/src/_version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default '0.2.0'
|
package/src/api.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const api = function (View) {
|
|
2
2
|
/** */
|
|
3
|
-
View.prototype.syllables = function () {
|
|
4
|
-
this.compute('syllables')
|
|
5
|
-
|
|
3
|
+
View.prototype.syllables = function (options = {}) {
|
|
4
|
+
this.compute('syllables', options)
|
|
5
|
+
const all = []
|
|
6
6
|
this.docs.forEach(terms => {
|
|
7
7
|
let some = []
|
|
8
8
|
terms.forEach(term => {
|
|
@@ -17,7 +17,7 @@ const api = function (View) {
|
|
|
17
17
|
/** */
|
|
18
18
|
View.prototype.soundsLike = function () {
|
|
19
19
|
this.compute('soundsLike')
|
|
20
|
-
|
|
20
|
+
const all = []
|
|
21
21
|
this.docs.forEach(terms => {
|
|
22
22
|
let some = []
|
|
23
23
|
terms.forEach(term => {
|
|
@@ -30,4 +30,4 @@ const api = function (View) {
|
|
|
30
30
|
return all
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
export default api
|
|
33
|
+
export default api
|
package/src/plugin.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import version from './_version.js'
|
|
2
2
|
import api from './api.js'
|
|
3
|
+
import syllables from './syllables/index.js'
|
|
4
|
+
import soundsLike from './sounds-like/index.js'
|
|
3
5
|
|
|
4
6
|
export default {
|
|
7
|
+
version,
|
|
5
8
|
api,
|
|
6
|
-
compute
|
|
7
|
-
|
|
9
|
+
compute: {
|
|
10
|
+
syllables,
|
|
11
|
+
soundsLike,
|
|
12
|
+
},
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import splitTerm from './split/index.js'
|
|
2
|
+
|
|
3
|
+
// Store syllables on each term; the public API collects them by sentence.
|
|
4
|
+
const syllables = function (view, options = {}) {
|
|
5
|
+
view.docs.forEach(terms => {
|
|
6
|
+
terms.forEach(term => {
|
|
7
|
+
term.syllables = splitTerm(term, options)
|
|
8
|
+
})
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default syllables
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const trailingPunct = /(?<!\p{Punctuation})\p{Punctuation}+$/u
|
|
2
|
+
const leadingPunct = /^[\p{Punctuation}$]+/u
|
|
3
|
+
|
|
4
|
+
// Prepare spelling for rule matching; cuts are still applied to the original text.
|
|
5
|
+
const normalize = input => {
|
|
6
|
+
let txt = input.toLowerCase()
|
|
7
|
+
// Ignore trailing punctuation runs for analysis; internal apostrophes stay intact.
|
|
8
|
+
txt = txt.replace(trailingPunct, '')
|
|
9
|
+
// Keep the prefix length so cuts still align with the original spelling.
|
|
10
|
+
const prefix = txt.match(leadingPunct)?.[0] || ''
|
|
11
|
+
const clean = txt.slice(prefix.length)
|
|
12
|
+
return { clean, prefix }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const pushCutsBack = (cuts, prefix = '') => {
|
|
16
|
+
return cuts.map(cut => cut + prefix.length)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// split a string into chunks based on the given cuts
|
|
20
|
+
const applyCuts = (text, cuts) => {
|
|
21
|
+
let start = 0
|
|
22
|
+
const parts = cuts.map(end => {
|
|
23
|
+
const part = text.slice(start, end)
|
|
24
|
+
start = end
|
|
25
|
+
return part
|
|
26
|
+
})
|
|
27
|
+
// Cuts are interior boundaries; the final chunk always runs to the text's end.
|
|
28
|
+
parts.push(text.slice(start))
|
|
29
|
+
return parts
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { applyCuts, normalize, pushCutsBack }
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Whole-word splits that override the vowel-span and boundary rules.
|
|
2
|
+
// Keys are lowercase; values are interior cut offsets, excluding the word's end.
|
|
3
|
+
const exceptions = {
|
|
4
|
+
acre: [1], // a-cre: the final re carries a syllable
|
|
5
|
+
// Borrowed words whose final e is pronounced.
|
|
6
|
+
cafe: [2], // ca-fe
|
|
7
|
+
abalone: [2, 3, 5], // ab-a-lo-ne
|
|
8
|
+
aborigine: [2, 3, 6, 7], // ab-o-rig-i-ne
|
|
9
|
+
comanche: [2, 5], // co-man-che
|
|
10
|
+
dolce: [3], // dol-ce
|
|
11
|
+
// Borrowed vowel combinations do not follow the usual English pair rules.
|
|
12
|
+
acacia: [1, 3], // a-ca-cia
|
|
13
|
+
fuchsia: [4], // fuch-sia
|
|
14
|
+
concierge: [3, 5], // con-ci-erge
|
|
15
|
+
concierges: [3, 5], // con-ci-erges
|
|
16
|
+
comedienne: [2, 4, 6], // co-me-di-enne
|
|
17
|
+
// The final re in massacre carries a syllable, including its inflected forms.
|
|
18
|
+
massacre: [3, 5], // mas-sa-cre
|
|
19
|
+
massacred: [3, 5], // mas-sa-cred
|
|
20
|
+
massacring: [3, 5], // mas-sa-cring
|
|
21
|
+
// Silent consonants make these look like syllabic -le endings.
|
|
22
|
+
aisle: [],
|
|
23
|
+
isle: [],
|
|
24
|
+
wednesday: [6], // wednes-day
|
|
25
|
+
// Keep the spoken vowel groups together or apart explicitly.
|
|
26
|
+
buoyant: [4], // buoy-ant
|
|
27
|
+
buoyancy: [4, 6], // buoy-an-cy
|
|
28
|
+
acquiesce: [2, 5], // ac-qui-esce
|
|
29
|
+
acquiesced: [2, 5], // ac-qui-esced
|
|
30
|
+
acquiescent: [2, 5, 7], // ac-qui-es-cent
|
|
31
|
+
acquiescence: [2, 5, 7], // ac-qui-es-cence
|
|
32
|
+
people: [3], //'peo ple',
|
|
33
|
+
queue: [], //'queue',
|
|
34
|
+
business: [4], //'busi ness',
|
|
35
|
+
quiet: [3], //'qui et',
|
|
36
|
+
science: [3], //'sci ence',
|
|
37
|
+
poet: [2], //'po et',
|
|
38
|
+
poetry: [2, 4], //'po et ry',
|
|
39
|
+
every: [3], //'eve ry',
|
|
40
|
+
everything: [3, 5], // 'eve ry thing',
|
|
41
|
+
everybody: [3, 5, 7], //'eve ry bo dy',
|
|
42
|
+
ilyich: [2], // 'il yich': consonantal y in this name
|
|
43
|
+
juan: [], // 'juan':
|
|
44
|
+
wyatt: [2], // 'wy att'
|
|
45
|
+
// Place names retain their recognizable compound boundaries.
|
|
46
|
+
batesville: [5], // bates-ville
|
|
47
|
+
belleville: [5], // belle-ville
|
|
48
|
+
bridgeport: [6], // bridge-port
|
|
49
|
+
cambridgeport: [3, 9], // cam-bridge-port
|
|
50
|
+
cambridgeside: [3, 9], // cam-bridge-side
|
|
51
|
+
janesville: [5], // janes-ville
|
|
52
|
+
roseberry: [4, 7], // rose-ber-ry
|
|
53
|
+
roseville: [4], // rose-ville
|
|
54
|
+
// These names retain vowels that the general spelling rules suppress or blend.
|
|
55
|
+
belge: [3], // bel-ge
|
|
56
|
+
canadienne: [2, 4, 6], // ca-na-di-enne
|
|
57
|
+
mildred: [3], // mil-dred
|
|
58
|
+
nietzsche: [5], // nietz-sche
|
|
59
|
+
odea: [1, 3], // o-de-a
|
|
60
|
+
// The two written o's belong to separate syllables in these words.
|
|
61
|
+
zoologist: [2, 4, 5], // zo-ol-o-gist
|
|
62
|
+
zoology: [2, 4, 5], // zo-ol-o-gy
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const checkExceptions = str => {
|
|
66
|
+
if (Object.hasOwn(exceptions, str)) {
|
|
67
|
+
return exceptions[str]
|
|
68
|
+
}
|
|
69
|
+
// Reuse singular cuts for a plain -s ending; exact entries above take priority.
|
|
70
|
+
const singular = str.replace(/s$/, '')
|
|
71
|
+
if (Object.hasOwn(exceptions, singular)) {
|
|
72
|
+
// A sibilant before silent e adds a syllable: sci-ence becomes sci-en-ces.
|
|
73
|
+
if (/[csxz]e$/.test(singular)) {
|
|
74
|
+
return exceptions[singular].concat(singular.length - 2)
|
|
75
|
+
}
|
|
76
|
+
return exceptions[singular]
|
|
77
|
+
}
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default checkExceptions
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import compoundPrefixes from './_compound-prefixes.js'
|
|
2
|
+
|
|
3
|
+
// Roots ending in ed keep their vowel; these are not regular past-tense suffixes.
|
|
4
|
+
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))?$/
|
|
5
|
+
|
|
6
|
+
// Find vowel spans in lowercase spelling; return character offsets, not syllables.
|
|
7
|
+
// Candidate vowel letters; ignored rules handle consonantal uses of u and y.
|
|
8
|
+
const isVowel = /[aeiouy]/
|
|
9
|
+
|
|
10
|
+
// Ignore the character at match.index + offset. Rules may veto a whole word.
|
|
11
|
+
// Global patterns visit every occurrence; except prevents that rule from applying.
|
|
12
|
+
const ignored = [
|
|
13
|
+
{ match: /[^aeiou]e(?=['’](?:s|d|ll|re|ve)$|$)/g, offset: 1, except: /[^aeiouyl]le(?:['’](?:s|d|ll|re|ve))?$/ }, // silent e
|
|
14
|
+
{ match: /[^td]ed(?=['’](?:s|d|ll|re|ve)$|$)/g, offset: 1, except: voicedEd }, // walked vs hundred/bubbled
|
|
15
|
+
{ 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
|
|
16
|
+
{
|
|
17
|
+
match: /e(?=ments?$|thing$|s?man$)/g,
|
|
18
|
+
offset: 0,
|
|
19
|
+
except: /(?:[^aeiouyl]le(?:ments?|thing|s?man)|elements?)$/,
|
|
20
|
+
}, // movement(s), but settlement(s) and element(s)
|
|
21
|
+
// Silent e stays silent before consonant suffixes, but syllabic -le survives.
|
|
22
|
+
{
|
|
23
|
+
match: /[^aeiou]e(?=ness(?:es)?$|l(?:y|iness)$|ful(?:ly)?$|less$|ships?$|some$)/g,
|
|
24
|
+
offset: 1,
|
|
25
|
+
except: /[^aeiouyl]le(?:ness(?:es)?|l(?:y|iness)|ful(?:ly)?|less|ships?|some)$/,
|
|
26
|
+
},
|
|
27
|
+
{ match: /['’][rv]e$/g, offset: 2 }, // we're, they've: suffix e adds no syllable
|
|
28
|
+
{ match: /qu(?=[aeiouy])/g, offset: 1 }, // consonantal u
|
|
29
|
+
{ match: /que(?=s?$|ly$|ness$)/g, offset: 2 }, // opaque, opaques, opaquely
|
|
30
|
+
{ match: /^y(?=[aeiou])/g, offset: 0 }, // yard, yellow
|
|
31
|
+
{ match: /[aeiouwy]y(?=[aeiou])/g, offset: 1 }, // beyond, lawyer, doubled y
|
|
32
|
+
{ match: /[aeiou]ny(?=[aeiou])/g, offset: 2, except: /^any/ }, // canyon, but anyone
|
|
33
|
+
{ match: /gu(?=a)/g, offset: 1 }, // guard, language
|
|
34
|
+
{ match: /busi(?=ness)/g, offset: 3 }, // businesses, agribusiness: the i stays silent
|
|
35
|
+
{ match: /ever(?=y(?:body|thing|where|one|day|time|$))/g, offset: 2 }, // everywhere, everyone
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
// Mc contributes a spoken syllable despite having no written vowel.
|
|
39
|
+
const syllabicPrefix = /^mc(?=[a-z])/
|
|
40
|
+
|
|
41
|
+
// These consonants carry a syllable despite having no written vowel.
|
|
42
|
+
// The captured consonant starts the span; surrounding letters provide context.
|
|
43
|
+
const syllabicConsonants = [
|
|
44
|
+
/(?:[sxz]e?|[cs]h|[cg]e)['’](s)$/, // horse's, church's
|
|
45
|
+
/[ai]s(m)(?:s|['’]s)?$/, // chasm, criticism, and plurals/possessives
|
|
46
|
+
/[iy]th(m)(?:s|['’]s)?$/, // algorithm, rhythms
|
|
47
|
+
/[^aeiouyn](n)['’]t$/, // couldn't, wasn't; not can't or hasn't
|
|
48
|
+
/^(?:could|should|would|might|must)['’](v)e$/, // modal + have: would-'ve, unlike we've
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
// Keep a boundary after these matched prefixes, even inside a vowel group.
|
|
52
|
+
const separate = [
|
|
53
|
+
/^(?:re)?cre(?=at[eio])/, // create, recreate, recreation (but not creature)
|
|
54
|
+
/^re(?=act)/, // re-act and re-action
|
|
55
|
+
/^re(?=absorb|app|elect|emerg|enter|establish|evalu|impos|invent|issue|us[ei])/, // reuse, reenter; not reed
|
|
56
|
+
/^pre(?=adolesc|ambl|arrang|eminen|empt|exist|ignition|invas)/, // preempt, preexisting; not preen
|
|
57
|
+
/^(?:micro|macro)(?=[aeiou])/, // micro-organism, macro-economic
|
|
58
|
+
/^co(?=operat|ordinat|alesc|erc)/, // cooperate, coercion; not cooperage
|
|
59
|
+
/^pro(?=act)/, // proactive; not approach
|
|
60
|
+
/^flu(?=id)/, // flu-id, unlike fruit
|
|
61
|
+
/^ru(?=in)/, // ru-in and ru-ined
|
|
62
|
+
/di(?=et|en[ct])/, // diet, audience, obedient; not died or soldier
|
|
63
|
+
/^ide(?=a)/, // idea, ideal; not deal
|
|
64
|
+
/^sovi(?=et)/, // Soviet and its derivatives retain separate i and e sounds
|
|
65
|
+
/^soci(?=et)/, // society, societies, societal; not social
|
|
66
|
+
/i(?=ell[aoi])/, // Gabriella, Gabrielli, -iello names; not field
|
|
67
|
+
/i(?=et(?:te)?s?$)/, // Juliet, Henriette
|
|
68
|
+
/^qui(?=et)/, // quiet keeps its separate vowels in quietly and quieter
|
|
69
|
+
/^acre(?=age)/, // acreage retains the vowel of acre before -age
|
|
70
|
+
/^sci(?=on)/, // scion has two vowels, unlike the ending in vision
|
|
71
|
+
/^intra(?=[aeiou])/, // intrauterine, intraocular
|
|
72
|
+
/re(?=al(?!ly|m))/, // cereal, reality; not really or realm
|
|
73
|
+
/[^q]u(?=en[ctdz])/, // fluent, innuendo, influenza; not subsequent
|
|
74
|
+
/[^g]u(?=ing)/, // suing, continuing; not plaguing or intriguing
|
|
75
|
+
/argu(?=ing)/, // arguing retains its pronounced u
|
|
76
|
+
/^lou(?=is)/, // Louis, Louisville
|
|
77
|
+
/[eo]u(?=illes?$)/, // separate the vowel groups in -euille/-ouille
|
|
78
|
+
/ou(?=ett)/, // silhouette, pirouette: ou and e are separate vowels
|
|
79
|
+
/o(?=as?$)/, // boa, boas; not boat
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
// Match a bounded window around the current vowel: [i - before, i + after).
|
|
83
|
+
const joined = [
|
|
84
|
+
// Common vowel pairs share a span unless a separate rule overrides them.
|
|
85
|
+
{ before: 1, after: 1, match: /^(ai|au|ay|ea|ee|ei|eu|ey|ie|oa|oe|oi|oo|ou|oy|ue|ui)$/ },
|
|
86
|
+
{ before: 3, after: 2, match: /^llion/ }, // million, unlike lion
|
|
87
|
+
{ before: 2, after: 3, match: /^[cst]i(?:on|an|al|ous)/ }, // nation, special, musician
|
|
88
|
+
{ before: 2, after: 4, match: /^cia(?:s|['’]s)?$/ }, // final -cia blends; association and glaciation keep separate vowels
|
|
89
|
+
{ before: 2, after: 1, match: /^eau$/ }, // extend ea to eau in beautiful
|
|
90
|
+
{ before: 1, after: 3, match: /^ae(?:r|st)/ }, // aerospace, aesthetic
|
|
91
|
+
{ before: 2, after: 3, match: /^[cg]eous/ }, // gorgeous, herbaceous; not courteous
|
|
92
|
+
{ before: 3, after: 3, match: /^hteous/ }, // righteous and its suffixes
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
// Mark pronounced vowel groups before choosing any consonant boundaries.
|
|
96
|
+
const vowelSpan = word => {
|
|
97
|
+
// Record ignored positions without deleting letters or shifting offsets.
|
|
98
|
+
const silent = new Set()
|
|
99
|
+
// A silent e remains silent inside a recognized compound.
|
|
100
|
+
compoundPrefixes.forEach(pattern => {
|
|
101
|
+
const match = pattern.exec(word)
|
|
102
|
+
if (match) {
|
|
103
|
+
silent.add(match[0].lastIndexOf('e'))
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
ignored.forEach(rule => {
|
|
107
|
+
if (!rule.except || !rule.except.test(word)) {
|
|
108
|
+
for (const match of word.matchAll(rule.match)) {
|
|
109
|
+
silent.add(match.index + rule.offset)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
// Forced separations take precedence over the joined-vowel rules below.
|
|
114
|
+
const breaks = new Set()
|
|
115
|
+
separate.forEach(pattern => {
|
|
116
|
+
const match = pattern.exec(word)
|
|
117
|
+
if (match) {
|
|
118
|
+
breaks.add(match.index + match[0].length)
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
// Spans use slice-compatible offsets: start is inclusive, end is exclusive.
|
|
122
|
+
const groups = []
|
|
123
|
+
const prefix = syllabicPrefix.exec(word)
|
|
124
|
+
if (prefix) {
|
|
125
|
+
groups.push({ start: 0, end: prefix[0].length })
|
|
126
|
+
}
|
|
127
|
+
// Mark the captured consonants without inserting letters or shifting offsets.
|
|
128
|
+
const voiced = new Set()
|
|
129
|
+
syllabicConsonants.forEach(pattern => {
|
|
130
|
+
const match = pattern.exec(word)
|
|
131
|
+
if (match) {
|
|
132
|
+
voiced.add(match.index + match[0].lastIndexOf(match[1]))
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
for (let i = 0; i < word.length; i += 1) {
|
|
136
|
+
if ((!isVowel.test(word[i]) && !voiced.has(i)) || silent.has(i)) {
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
const previous = groups[groups.length - 1]
|
|
140
|
+
// Only adjacent pronounced vowels may extend an existing span.
|
|
141
|
+
const blended =
|
|
142
|
+
previous &&
|
|
143
|
+
previous.end === i &&
|
|
144
|
+
!breaks.has(i) &&
|
|
145
|
+
joined.some(rule => rule.match.test(word.slice(Math.max(0, i - rule.before), i + rule.after)))
|
|
146
|
+
if (blended) {
|
|
147
|
+
previous.end = i + 1
|
|
148
|
+
} else {
|
|
149
|
+
groups.push({ start: i, end: i + 1 })
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return groups
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export default vowelSpan
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import compoundPrefixes from './_compound-prefixes.js'
|
|
2
|
+
|
|
3
|
+
// Turn vowel spans into cut positions by assigning the consonants between them.
|
|
4
|
+
// Consonant clusters allowed to start a chunk; the picker checks longest first.
|
|
5
|
+
const onset = new Set([
|
|
6
|
+
'bl', 'br', 'ch', 'cl', 'cr', 'dr', 'fl', 'fr', 'gl', 'gr',
|
|
7
|
+
'ph', 'pl', 'pr', 'qu', 'sh', 'sk', 'sl', 'sm', 'sn', 'sp',
|
|
8
|
+
'st', 'sw', 'th', 'tr', 'tw', 'wh', 'scr', 'shr', 'spl', 'spr', 'str', 'thr',
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
// Applied in order after onset placement; later rules take precedence.
|
|
12
|
+
// left/right are offsets from the surrounding vowel groups' edges.
|
|
13
|
+
// minLeft only moves a boundary forward, preserving a longer consonant cluster.
|
|
14
|
+
// gap matches intervening letters; tail matches the complete remaining spelling.
|
|
15
|
+
const adjustments = [
|
|
16
|
+
{ minGap: 1, maxGap: 1, gap: /^x$/, left: 1 }, // box-es
|
|
17
|
+
{ minGap: 2, gap: /^(.)\1/, minLeft: 1 }, // rab-bit
|
|
18
|
+
{ minGap: 2, tail: /^ed$/, right: -1 }, // res-ted
|
|
19
|
+
{ minGap: 2, gap: /^sh$/, tail: /^(es|ing)$/, right: 0 }, // wish-ing
|
|
20
|
+
{ minGap: 2, gap: /l$/, tail: /^e(?:[sd]|['’](?:s|d|ll|re|ve))?$/, right: -2 }, // bot-tle, ta-bles, bub-bled
|
|
21
|
+
{ minGap: 2, gap: /l$/, tail: /^e(?:ments?|ships?|ness(?:es)?|less|ful(?:ly)?|ly)(?:['’]s)?$/, right: -2 }, // set-tle-ment, bat-tle-ships
|
|
22
|
+
{ minGap: 1, gap: /l$/, tail: /^ess(?:ly|ness(?:es)?)?$/, right: -1 }, // help-less: keep the suffix intact
|
|
23
|
+
{ minGap: 3, gap: /^ssl$/, tail: /^y$/, right: -1 }, // help-less-ly, not help-les-sly
|
|
24
|
+
{ minGap: 1, gap: /^(?:s|th)$/, tail: /^m(?:s|['’]s)?$/, right: 0 }, // chas-m, rhyth-m
|
|
25
|
+
{ minGap: 1, tail: /^n['’]t$/, right: 0 }, // could-n't: keep the base consonants together
|
|
26
|
+
{ minGap: 1, gap: /^[-‐‑–—]+$/, right: 0 }, // re-enter: keep the hyphen on the left
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
// Choose one cut between each pair of vowel spans, preserving their count.
|
|
30
|
+
const pickBoundaries = (word, vowelSpans) => {
|
|
31
|
+
const cuts = []
|
|
32
|
+
// Keep recognized compound components intact, such as side-walk and some-one.
|
|
33
|
+
const compoundCuts = compoundPrefixes.flatMap(pattern => {
|
|
34
|
+
const match = pattern.exec(word)
|
|
35
|
+
return match ? [match[0].length] : []
|
|
36
|
+
})
|
|
37
|
+
for (let i = 1; i < vowelSpans.length; i += 1) {
|
|
38
|
+
// The gap contains the letters not assigned to either vowel span.
|
|
39
|
+
const left = vowelSpans[i - 1].end
|
|
40
|
+
const right = vowelSpans[i].start
|
|
41
|
+
const gap = word.slice(left, right)
|
|
42
|
+
// Match full suffixes so long endings are not mistaken for shorter ones.
|
|
43
|
+
const tail = word.slice(right)
|
|
44
|
+
// Adjacent vowels split directly; a single consonant starts the next chunk.
|
|
45
|
+
let cut = left
|
|
46
|
+
if (gap.length > 1) {
|
|
47
|
+
cut = right - 1
|
|
48
|
+
// Prefer the longest recognized consonant cluster at the next chunk's start.
|
|
49
|
+
for (let size = Math.min(3, gap.length); size > 1; size -= 1) {
|
|
50
|
+
if (onset.has(gap.slice(-size))) {
|
|
51
|
+
cut = right - size
|
|
52
|
+
break
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Apply every matching adjustment in order, allowing specific rules to override.
|
|
57
|
+
adjustments.forEach(rule => {
|
|
58
|
+
if (
|
|
59
|
+
gap.length < rule.minGap ||
|
|
60
|
+
(rule.maxGap !== undefined && gap.length > rule.maxGap) ||
|
|
61
|
+
(rule.gap && !rule.gap.test(gap)) ||
|
|
62
|
+
(rule.tail && !rule.tail.test(tail))
|
|
63
|
+
) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
// Exact offsets replace the cut; minLeft only advances an earlier cut.
|
|
67
|
+
if (rule.left !== undefined) {
|
|
68
|
+
cut = left + rule.left
|
|
69
|
+
} else if (rule.right !== undefined) {
|
|
70
|
+
cut = right + rule.right
|
|
71
|
+
} else {
|
|
72
|
+
cut = Math.max(cut, left + rule.minLeft)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
// A compound boundary overrides ordinary consonant placement within this gap.
|
|
76
|
+
compoundCuts.forEach(boundary => {
|
|
77
|
+
if (boundary >= left && boundary <= right) {
|
|
78
|
+
cut = boundary
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
cuts.push(cut)
|
|
82
|
+
}
|
|
83
|
+
return cuts
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default pickBoundaries
|