@echogarden/text-segmentation 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.
@@ -0,0 +1,225 @@
1
+ import { anyOf, buildRegExp, charRange, inputEnd, inputStart, matches, oneOrMore, possibly, repeated, tab, unicodeProperty, whitespace } from 'regexp-composer'
2
+
3
+ export function buildWordOrNumberPattern(suppressions: string[]) {
4
+ return anyOf(
5
+ buildSuppressionPattern(suppressions),
6
+ wordSegmentPattern
7
+ )
8
+ }
9
+
10
+ export function buildSuppressionPattern(suppressions: string[]) {
11
+ const suppressionsPattern =
12
+ matches(
13
+ anyOf(...suppressions),
14
+ { ifNotFollowedBy: wordCharacterPattern }
15
+ )
16
+
17
+ return suppressionsPattern
18
+ }
19
+
20
+ ////////////////////////////////////////////////////////////////////////////////////////////////
21
+ // Numeric patterns
22
+ ////////////////////////////////////////////////////////////////////////////////////////////////
23
+ export const punctuationPattern = unicodeProperty('Punctuation')
24
+ export const digitPattern = unicodeProperty('Decimal_Number')
25
+ export const arabicNumeralPattern = charRange('0', '9')
26
+
27
+ export const numericSeparatorPattern =
28
+ matches(
29
+ anyOf('.', ',', '٬', '_'), {
30
+ ifPrecededBy: arabicNumeralPattern,
31
+ ifFollowedBy: arabicNumeralPattern
32
+ })
33
+
34
+ export const dateTimeSeparatorPattern = matches(
35
+ anyOf('/', '-', ':'), {
36
+ ifPrecededBy: arabicNumeralPattern,
37
+ ifFollowedBy: arabicNumeralPattern
38
+ })
39
+
40
+ export const dateTimePattern =
41
+ oneOrMore(anyOf(
42
+ arabicNumeralPattern,
43
+ dateTimeSeparatorPattern,
44
+ ))
45
+
46
+ export const spacedThousandsSeparatorPattern =
47
+ matches(
48
+ ' ', {
49
+ ifPrecededBy: arabicNumeralPattern,
50
+ ifFollowedBy: repeated(2, arabicNumeralPattern)
51
+ })
52
+
53
+ export const numericSignPattern =
54
+ matches(
55
+ anyOf('-', '+'), {
56
+ ifPrecededBy: anyOf(whitespace, punctuationPattern),
57
+ ifFollowedBy: arabicNumeralPattern,
58
+ })
59
+
60
+ export const numberPattern = [
61
+ oneOrMore(anyOf(
62
+ digitPattern,
63
+ numericSeparatorPattern,
64
+ spacedThousandsSeparatorPattern,
65
+ numericSignPattern,
66
+ )),
67
+ ]
68
+
69
+ const percentageChars = ['%']
70
+ const currencySpecialChars = ['$', '¥', '€', '£', '¥', '₩', '₭', '₽', '₫', '฿', '¢', '₮', '؋', '₦', '₱', '₴', '₪']
71
+
72
+ const percentageOrCurrencyPattern = anyOf(...percentageChars, ...currencySpecialChars)
73
+
74
+ export const prefixPercentageOrCurrencyPattern =
75
+ matches([
76
+ percentageOrCurrencyPattern,
77
+ numberPattern,
78
+ ], {
79
+ ifNotPrecededBy: digitPattern,
80
+ ifFollowedBy: anyOf(whitespace, punctuationPattern),
81
+ })
82
+
83
+ export const suffixPercentagePattern =
84
+ matches([
85
+ numberPattern,
86
+ percentageOrCurrencyPattern,
87
+ ], {
88
+ ifPrecededBy: anyOf(whitespace, punctuationPattern),
89
+ ifNotFollowedBy: digitPattern,
90
+ })
91
+
92
+ export const percentagePattern = anyOf(
93
+ prefixPercentageOrCurrencyPattern,
94
+ suffixPercentagePattern
95
+ )
96
+
97
+ ////////////////////////////////////////////////////////////////////////////////////////////////
98
+ // Letter patterns
99
+ ////////////////////////////////////////////////////////////////////////////////////////////////
100
+ export const letterPattern = unicodeProperty('Letter')
101
+ export const markPattern = unicodeProperty('Mark')
102
+ export const apostrophPattern = anyOf(`'`, `’`, `‘`)
103
+
104
+ export const letterOrMarkPattern = anyOf(
105
+ letterPattern,
106
+ markPattern,
107
+ )
108
+
109
+ export const dottedAbbreviationSequencePattern =
110
+ matches(
111
+ anyOf(
112
+ [
113
+ letterPattern,
114
+
115
+ oneOrMore([
116
+ '. ',
117
+ letterPattern,
118
+ ]),
119
+
120
+ possibly('.'),
121
+ ],
122
+ [
123
+ letterPattern,
124
+
125
+ oneOrMore([
126
+ '.',
127
+ possibly(' '),
128
+ letterPattern,
129
+ ]),
130
+
131
+ possibly('.'),
132
+ ]
133
+ ), {
134
+ ifNotPrecededBy: anyOf(letterOrMarkPattern, digitPattern),
135
+ ifNotFollowedBy: anyOf(letterOrMarkPattern, digitPattern)
136
+ })
137
+
138
+
139
+ export const wordCharacterPattern =
140
+ anyOf(
141
+ letterPattern,
142
+ markPattern,
143
+ digitPattern,
144
+ )
145
+
146
+ export const wordSeparatorPattern =
147
+ matches(
148
+ anyOf('-', '_', '·', '‧', '&'), {
149
+
150
+ ifPrecededBy: letterOrMarkPattern,
151
+ ifFollowedBy: letterOrMarkPattern
152
+ })
153
+
154
+ export const wordInnerApostrophPattern =
155
+ matches(
156
+ apostrophPattern, {
157
+
158
+ ifPrecededBy: letterOrMarkPattern,
159
+ ifFollowedBy: letterOrMarkPattern,
160
+ })
161
+
162
+ export const wordStartApostrophPattern =
163
+ matches(
164
+ apostrophPattern, {
165
+
166
+ ifPrecededBy: whitespace,
167
+ ifFollowedBy: [letterOrMarkPattern, letterOrMarkPattern, whitespace]
168
+ })
169
+
170
+ export const basicWordPattern =
171
+ oneOrMore(
172
+ anyOf(
173
+ wordCharacterPattern,
174
+ wordSeparatorPattern,
175
+ wordInnerApostrophPattern,
176
+ ),
177
+ )
178
+
179
+ export const wordSegmentPattern = anyOf(
180
+ dottedAbbreviationSequencePattern,
181
+ dateTimeSeparatorPattern,
182
+ percentagePattern,
183
+ numberPattern,
184
+ basicWordPattern,
185
+ )
186
+
187
+ ////////////////////////////////////////////////////////////////////////////////////////////////
188
+ // Prebuilt regular expressions
189
+ ////////////////////////////////////////////////////////////////////////////////////////////////
190
+ export const phraseSeparators = [',', '、', ',', '،', ';', ';', ':', ':', '—']
191
+
192
+ export const phraseSeparatorRegExp = buildRegExp([
193
+ inputStart,
194
+ anyOf(...phraseSeparators),
195
+ inputEnd
196
+ ])
197
+
198
+ export const sentenceSeparators = ['.', '。', '?', '?', '!', '!', '\n']
199
+
200
+ export const sentenceSeparatorRegExp = buildRegExp([
201
+ inputStart,
202
+ anyOf(...sentenceSeparators),
203
+ inputEnd
204
+ ])
205
+
206
+ export const sentenceSeparatorTrailingPunctuationRegExp = buildRegExp([
207
+ inputStart,
208
+ anyOf('"', '”', '’', ')', ']', '}', '»', ...sentenceSeparators, ...phraseSeparators, oneOrMore(whitespace)),
209
+ inputEnd
210
+ ])
211
+
212
+ export const phraseSeparatorTrailingPunctuationRegExp = buildRegExp([
213
+ inputStart,
214
+ anyOf(...phraseSeparators, ' ', tab),
215
+ inputEnd
216
+ ])
217
+
218
+ export const oneOrMoreSpacesRegExp = buildRegExp([
219
+ inputStart,
220
+ oneOrMore(' '),
221
+ inputEnd
222
+ ])
223
+
224
+ export const wordCharacterRegExp = buildRegExp(wordCharacterPattern)
225
+ export const whitespacePatternRegExp = buildRegExp(whitespace)
@@ -0,0 +1,35 @@
1
+ export const cldrSuppressions: Record<string, string[]> = {
2
+ 'en': ['L.P.', 'Alt.', 'Approx.', 'E.G.', 'O.', 'Maj.', 'Misc.', 'P.O.', 'J.D.', 'Jam.', 'Card.', 'Dec.', 'Sept.', 'MR.', 'Long.', 'Hat.', 'G.', 'Link.', 'DC.', 'D.C.', 'M.T.', 'Hz.', 'Mrs.', 'By.', 'Act.', 'Var.', 'N.V.', 'Aug.', 'B.', 'S.A.', 'Up.', 'Job.', 'Num.', 'M.I.T.', 'Ok.', 'Org.', 'Ex.', 'Cont.', 'U.', 'Mart.', 'Fn.', 'Abs.', 'Lt.', 'OK.', 'Z.', 'E.', 'Kb.', 'Est.', 'A.M.', 'L.A.', 'Prof.', 'U.S.', 'Nov.', 'Ph.D.', 'Mar.', 'I.T.', 'exec.', 'Jan.', 'N.Y.', 'X.', 'Md.', 'Op.', 'vs.', 'D.A.', 'A.D.', 'R.L.', 'P.M.', 'Or.', 'M.R.', 'Cap.', 'PC.', 'Feb.', 'Exec.', 'I.e.', 'Sep.', 'Gb.', 'K.', 'U.S.C.', 'Mt.', 'S.', 'A.S.', 'C.O.D.', 'Capt.', 'Col.', 'In.', 'C.F.', 'Adj.', 'AD.', 'I.D.', 'Mgr.', 'R.T.', 'B.V.', 'M.', 'Conn.', 'Yr.', 'Rev.', 'Phys.', 'pp.', 'Ms.', 'To.', 'Sgt.', 'J.K.', 'Nr.', 'Jun.', 'Fri.', 'S.A.R.', 'Lev.', 'Lt.Cdr.', 'Def.', 'F.', 'Do.', 'Joe.', 'Id.', 'Mr.', 'Dept.', 'Is.', 'Pvt.', 'Diff.', 'Hon.B.A.', 'Q.', 'Mb.', 'On.', 'Min.', 'J.B.', 'Ed.', 'AB.', 'A.', 'S.p.A.', 'I.', 'a.m.', 'Comm.', 'Go.', 'VS.', 'L.', 'All.', 'PP.', 'P.V.', 'T.', 'K.R.', 'Etc.', 'D.', 'Adv.', 'Lib.', 'E.g.', 'Pro.', 'U.S.A.', 'S.E.', 'AA.', 'Rep.', 'Sq.', 'As.'],
3
+ 'de': ['Port.', 'Alt.', 'Di.', 'Ges.', 'frz.', 'entspr.', 'Gebr.', 'erw.', 'Frl.', 'Inh.', 'k.u.k.', 'Ca.', 'J.D.', 'Ausg.', 'evtl.', 'So.', 'i.B.', 's.a.', 'kgl.', 'Sept.', 'o.B.', 'Sa.', 'ev.', 'Dez.', 'am.', 'i.R.', 'eigtl.', 'i.J.', 'u.U.', 'G.', 'z.Hd.', 'u.A.w.g.', 'Kl.', 'Spezif.', 'Obj.', 'Ing.', 'D. h.', 'Folg.', 'Akt.', 'i.A.', 'Msp.', 'U.U.', 'Chr.', 'R.', 'Einh.', 'schwäb.', 'Vgl.', 'Aug.', 'Dipl.-Ing.', 'W.', 'B.', 'U. U.', 'J.', 'Fa.', 'Mo.', 'n.u.Z.', 'Op.', 'Mrd.', 'e.h.', 'Hr.', 'Hrn.', 'Ztr.', 'k. u. k.', 'Bibl.', 'd.Ä.', 'b.', 'M.', 'i.H.', 'v.R.w.', 'o.A.', 'St.', 'Dr.', 'Fn.', 'Abs.', 'Rd.', 'Dtzd.', 'Jahrh.', 'Z.', 'Std.', 'n. Chr.', 'möbl.', 'tägl.', 'gest.', 'gesch.', 'z.B.', 'Hbf.', 'Abt.', 'A.M.', 'e.Wz.', 'v.T.', 'Nov.', 'z.', 'Prot.', 'U.S.', 'Wg.', 'u.v.a.', 'Adr.', 'App.', 'ggf.', 'ggfs.', 'Jan.', 'O.', 'Rel.', 'od.', 'Pfd.', 'a.a.O.', 'p.Adr.', 'P.', 'Gem.', 'v. Chr.', 'Art.', 'z.Z.', 'S.A.', 'i.V.', 'verh.', 'Ausschl.', 'm.W.', 'Dir.', 'Verf.', 'Sek.', 'r.', 'Chin.', 'Feb.', 'Int.', 'Sep.', 'Gesch.', 'schweiz.', 'Bed.', 'a.Rh.', 'jew.', 'vgl.', 'a.M.', 'Str.', 'exkl.', 'gek.', 'Erf.', 'u.Ä.', 'ehem.', 'näml.', 'u. Z.', 'v. u. Z.', 'sog.', 'C.', 'Dipl.-Kfm.', 'mtl.', 'Hrsg.', 'Qu.', 'röm.', 'u.', 'U.', 'Adj.', 'Kap.', 'hpts.', 'a.D.', 'gedr.', 'Best.', 'N.', 'v.u.Z.', 'Phys.', 'Fr.', 'd.J.', 'Reg.-Bez.', 'm.E.', 'schles.', 'Max.', 'Ltd.', 'südd.', 'inkl.', 'geb.', 'Ggf.', 'Inc.', 'kath.', 'kfm.', 'Nr.', 'Proz.', 'Dim.', 'verw.', 'Reg.', 'Dat.', 'Evtl.', 'led.', 'F.', 'Test.', 'Schr.', 'Do.', 'PIN.', 'Z. Zt.', 'v.Chr.', 'Tägl.', 's.', 'amtl.', 'Temp.', 'Mind.', 'e.V.', 'Abw.', 'P.M.', 'F.f.', 'a.a.S.', 'Mod.', 'Co.', 'Min.', 'Allg.', 'Geograph.', 'Jr.', 'Urspr.', 'Apr.', 'Z. B.', 'v.H.', 'A.', 'einschl.', 'Trans.', 'zzgl.', 'StR.', 'Fam.', 'I.', 'jhrl.', 'u.a.', 'Ben.', 'o.g.', 'Kfm.', 'Konv.', 'Mi.', 'L.', 'beil.', 'T.', 'Ursprüngl.', 'röm.-kath.', 'Okt.', 'u.ä.', 'Tel.', 'D.', 'Ber.', 'Kop.', 'Mio.', 'Y.', 'U.S.A.', 'v. H.', 'Forts. f.', 'Rep.', 'Hptst.', 'österr.'],
4
+ 'es': ['Rdos.', 'JJ.OO.', 'Sres.', 'fig.', 'may.', 'RR.HH.', 'oct.', 'cap.', 'mié.', 'doc.', 'Excmo.', 'Trab.', 'Excmos.', 'Kit.', 'Inc.', 'FF.CC.', 'DC.', 'ago.', 'trad.', 'SA.', 'Rvdos.', 'ed.', 'Exmo.', 'jul.', 'col.', 'RAM.', 'Srtas.', 'ene.', 'Rol.', 'Fabric.', 'Comm.', 'vid.', 'Da.', 'dic.', 'ss.', 'abr.', 'ntra.', 'Sra.', 'dtor.', 'cf.', 'dom.', 'prov.', 'Emm.', 'Sr.', 'licdo.', 'p.ej.', 'bol.', 'figs.', 'Vda.', 'Dr.', 'ntro.', 'Desv.', 'O.M.', 'Ldo.', 'Drs.', 'sáb.', 'feb.', 'Ltda.', 'Lcda.', 'Exma.', 'C.V.', 'SS.MM.', 'Lda.', 'U.S.', 'hnos.', 'R.D.', 'Korn.', 'v.gr.', 'vs.', 'Ilmas.', 'Rdo.', 'ej.', 'vie.', 'jue.', 'a. C.', 'Ilmos.', 'e. c.', 'Excma.', 'afma.', 'licda.', 'Em.', 'K.', 'sras.', 'MM.', 'fund.', 'Mons.', 'Lcdo.', 'afmo.', 'C.', 'A.C.', 'dptos.', 'Col.', 'Srta.', 'Av.', 'Ant.', 'depto.', 'Var.', 'H.P.', 'D.', 'M.', 'C.P.', 'Rev.', 'Rvdmos.', 'Fr.', 'Ilmo.', 'afmos.', 'Ltd.', 'afmas.', 'prof.', 'lun.', 'SS.AA.', 'Sol.', 'nov.', 'mss.', 'Dña.', 'Seg.', 'mar.', 'Rvdmo.', 'Reg.', 'ms.', 'Sras.', 'sres.', 'U.S.A.', 'Sta.', 'Sdad.', 'Dra.', 'srs.', 'R.U.', 'deptos.', 'dpto.', 'jun.', 'bco.', 'Cía.', 'Id.', 'Mr.', 'e.g.', 'C.S.', 'Excmas.', 'Dª.', 'Rvdo.', 'Lic.', 'cfr.', 'Corp.', 'Dto.', 'Ilma.', 'L.', 'All.', 'PP.', 'd. C.', 'Ltdo.', 'mtro.', 'Mrs.', 'Desc.', 'Avda.', 'Exmas.', 'a. e. c.', 'Bien.', 'Exmos.', 'AA.', 'Sto.', 'CA.', 'sept.', 'Exc.', 'c/c.'],
5
+ 'fr': ['aux.', 'config.', 'collab.', 'M.', 'dim.', 'imprim.', 'oct.', 'syst.', 'bull.', 'MM.', 'doc.', 'P.O.', 'hôp.', 'Mart.', 'juil.', 'broch.', 'adr.', 'symb.', 'C.', 'anc.', 'voit.', 'Jr.', 'graph.', 'dir.', 'éd.', 'fig.', 'édit.', 'niv.', 'quart.', 'cam.', 'éval.', 'anon.', 'réf.', 'Comm.', 'Prof.', 'févr.', 'indus.', 'DC.', 'équiv.', 'illustr.', 'acoust.', 'nov.', 'L.', 'All.', 'U.S.', 'S.M.A.R.T.', 'sept.', 'avr.', 'jeu.', 'dest.', 'P.-D. G.', 'ill.', 'coll.', 'encycl.', 'mer.', 'Desc.', 'ven.', 'P.', 'lun.', 'Inc.', 'sam.', 'D.', 'append.', 'Var.', 'categ.', 'janv.', 'S.A.', 'imm.', 'U.S.A.', 'mar.', 'exempl.', 'déc.', 'ann.', 'U.', 'synth.', 'dict.', 'av. J.-C.', 'W.', 'Op.', 'ap. J.-C.', 'gouv.', 'trav. publ.'],
6
+ 'it': ['N.B.', 'div.', 'a.C.', 'fig.', 'd.p.R.', 'c.c.p.', 'Cfr.', 'vol.', 'Geom.', 'O.d.G.', 'S.p.A.', 'ver.', 'N.d.A.', 'dott.', 'arch.', 'd.C.', 'N.d.T.', 'rag.', 'Sig.', 'Mod.', 'pag.', 'dr.', 'tav.', 'N.d.E.', 'DC.', 'mitt.', 'Ing.', 'int.', 'on.', 'C.P.', 'ag.', 'L.', 'U.S.', 'S.M.A.R.T.', 'p.i.', 'tab.', 'Ltd.', 'Liv.', 'D.', 'U.S.A.', 'sez.', 'avv.', 'S.A.R.', 'all.', 'p.'],
7
+ 'pt': ['psicol.', 'fig.', 'compl.', 'rep.', 'cap.', 'doc.', 'fisiol.', 'dipl.', 'astron.', 'port.', 'eletrôn.', 'geom.', 'mov.', 'ago.', 'trad.', 'arquit.', 'dez.', 'ed.', 'apt.', 'Exmo.', 'col.', 'ff.', 'univ.', 'res.', 'R.', 'transp.', 'D.C', 'l.', 'des.', 'fev.', 'abr.', 'liter.', 'lat.', 'Dir.', 'cf.', 'adm.', 'fot.', 'p.m.', 'P.M.', 'créd.', 'jur.', 'com.', 'anat.', 'dir.', 'end.', 'fís.', 'E.', 'Est.', 'cont.', 'matem.', 'Drs.', 'gên.', 'neol.', 'pág.', 'índ.', 'Ltda.', 'Exma.', 'esp.', 'ingl.', 'tecnol.', 'Mar.', 'símb.', 'Pe.', 'pal.', 'filos.', 'V.T.', 'fasc.', 'vs.', 'mai.', 'S.A.', 'profa.', 'N.Sra.', 'r.s.v.p.', 'cel.', 'mat.', 'abrev.', 'out.', 'long.', 'aux.', 'arit.', 'aer.', 'jul.', 'lin.', 'S.', 'méd.', 'odontol.', 'org.', 'A.C.', 'jun.', 'déb.', 'Av.', 'álg.', 'sup.', 'fl.', 'odont.', 'caps.', 'relat.', 'organiz.', 'hist.', 'Fr.', 'Ilmo.', 'fem.', 'ap.', 'Ltd.', 'pol.', 'séc.', 'prof.', 'cx.', 'nov.', 'quím.', 'mús.', 'agric.', 'mar.', 'W.C.', 'fr.', 'cat.', 'jan.', 'pron.', 'rel.', 'autom.', 'Sta.', 'Dra.', 'p.', 'tel.', 'div.', 'p. ex.', 'a.C.', 'bras.', 'Alm.', 'Dr.', 'comp.', 'pq.', 'arqueol.', 'náut.', 'biogr.', 'f.', 'círc.', 'fac.', 'd.C.', 'apart.', 'ex.', 'Jr.', 'set.', 'tec.', 'sociol.', 'gram.', 'ind.', 'Ilma.', 'vol.', 'eng.', 'rod.', 'Ph.D.', 'Dras.', 'pp.', 'elem.', 'máq.', 'cód.', 'eletr.', 'prod.', 'ref.', 'fil.', 'a.m.', 'A.M', 'obs.', 'N.T.', 'contab.', 'Sto.', 'lit.', 'educ.', 'rementente', 'desc.', 'próx.'],
8
+ 'ru': ['руб.', 'янв.', 'до н. э.', 'сент.', 'тел.', 'дек.', 'февр.', 'нояб.', 'апр.', 'н. э.', 'окт.', 'тыс.', 'авг.', 'проф.', 'н.э.', 'кв.', 'ул.', 'отд.'],
9
+ }
10
+
11
+ export const leadingApostropheContractionSuppressions: Record<string, string[]> = {
12
+ 'en': [`'cause`, `'til`, `'bout`, `'twas`, `'tis`],
13
+ 'af': [`'n`]
14
+ }
15
+
16
+ export const nounSuppressions = [
17
+ 'C#', 'F#', 'C++', 'Yahoo!', 'Toys"R"Us', `Dunkin'`, 'Ke$ha', 'I/O', 'Sky+', 'A/C', 'A/V'
18
+ ]
19
+
20
+ export const tldSuppressions = [
21
+ '.com', '.org', '.net', '.co', '.us'
22
+ ]
23
+
24
+ // What about plural possesive English nouns like:
25
+ // The brothers' friend.
26
+ // The professors' books.
27
+ // Trees' roots.
28
+ // Diplomats' contracts.
29
+ //
30
+ // Not easy to confidently identify these.
31
+ // The apostrophe can also represent an ending single quotation mark,
32
+ // or a misplaced opening single quotation mark.
33
+ // Maybe if a word ends with `s'` and there is no opening `'` seen anywhere near,
34
+ // but that is still not 100% certain. The opening `'` may have appeared long before that.
35
+
package/src/Test.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { segmentText, splitToWords } from "./TextSegmentation.js"
2
+ import { Timer } from "./utilities/Timer.js"
3
+
4
+ const log = console.log
5
+
6
+ async function test1() {
7
+ {
8
+ const wordSequence = await splitToWords('Hello world! Привет мир! 你好世界!')
9
+ console.log(JSON.stringify(wordSequence.words))
10
+ }
11
+
12
+ const { readFileSync, writeFileSync } = await import('fs')
13
+ const text = readFileSync('test-data/Test.txt', 'utf-8')
14
+
15
+ const timer = new Timer()
16
+
17
+ const result = await segmentText(text, {
18
+ language: 'en',
19
+ customSuppressions: [],
20
+ enableEastAsianPostprocessing: true
21
+ })
22
+
23
+ //const json = JSON.stringify(result.sentences)
24
+
25
+ timer.logAndRestart(`Total execution time`)
26
+
27
+ log('')
28
+
29
+ //
30
+
31
+ let segmentedText = ''
32
+
33
+ for (let sentenceIndex = 0; sentenceIndex < result.sentences.length; sentenceIndex++) {
34
+ const sentence = result.sentences[sentenceIndex]
35
+ const phrases = sentence.phrases
36
+
37
+ for (let phraseIndex = 0; phraseIndex < phrases.length; phraseIndex++) {
38
+ const phrase = phrases[phraseIndex]
39
+
40
+ segmentedText += phrase.wordSequence.words.join(' | ')
41
+
42
+ if (phraseIndex < phrases.length - 1) {
43
+ segmentedText += `\n${'-'.repeat(100)}\n`
44
+ }
45
+ }
46
+
47
+ if (sentenceIndex < result.sentences.length - 1) {
48
+ segmentedText += `\n${ '='.repeat(100) } \n`
49
+ }
50
+ }
51
+
52
+ writeFileSync('out/segmented.txt', segmentedText)
53
+ }
54
+
55
+ test1()
@@ -0,0 +1,303 @@
1
+ import { buildRegExp } from 'regexp-composer'
2
+ import { buildWordOrNumberPattern as buildWordSplitterPattern, phraseSeparatorRegExp, sentenceSeparatorTrailingPunctuationRegExp, sentenceSeparatorRegExp, whitespacePatternRegExp } from './Patterns.js'
3
+ import { cldrSuppressions, leadingApostropheContractionSuppressions, nounSuppressions, tldSuppressions } from './Suppressions.js'
4
+ import { eastAsianCharRangesRegExp } from './EastAsianCharacterPatterns.js'
5
+ import { WordSequence } from './WordSequence.js'
6
+
7
+ export { cldrSuppressions } from './Suppressions.js'
8
+
9
+ export async function segmentText(text: string, options?: SegmentationOptions) {
10
+ options = { ...defaultSegmentationOptions, ...(options ?? {}) }
11
+
12
+ const wordSequence = await splitToWords(text, options)
13
+
14
+ return segmentWordSequence(wordSequence)
15
+ }
16
+
17
+ export async function segmentWordSequence(wordSequence: WordSequence) {
18
+ const sentenceWordRanges: Range[] = []
19
+
20
+ let sentenceStartWordOffset = 0
21
+
22
+ for (let wordIndex = 0; wordIndex < wordSequence.length; wordIndex++) {
23
+ const word = wordSequence.getWordAt(wordIndex)
24
+
25
+ if (sentenceSeparatorRegExp.test(word)) {
26
+ while (wordIndex < wordSequence.length - 1) {
27
+ const nextWord = wordSequence.getWordAt(wordIndex + 1)
28
+
29
+ if (!sentenceSeparatorTrailingPunctuationRegExp.test(nextWord)) {
30
+ break
31
+ }
32
+
33
+ wordIndex += 1
34
+ }
35
+
36
+ sentenceWordRanges.push({
37
+ start: sentenceStartWordOffset,
38
+ end: wordIndex + 1
39
+ })
40
+
41
+ sentenceStartWordOffset = wordIndex + 1
42
+ }
43
+ }
44
+
45
+ if (sentenceStartWordOffset < wordSequence.length) {
46
+ sentenceWordRanges.push({ start: sentenceStartWordOffset, end: wordSequence.length })
47
+ }
48
+
49
+ const sentences: Sentence[] = []
50
+
51
+ for (const wordRange of sentenceWordRanges) {
52
+ const sentenceWordSequence = wordSequence.slice(wordRange.start, wordRange.end)
53
+
54
+ sentences.push(new Sentence(wordRange, sentenceWordSequence))
55
+ }
56
+
57
+ for (const sentence of sentences) {
58
+ const phraseWordRanges: Range[] = []
59
+
60
+ let sentenceEndWordOffset = sentence.wordRange.end
61
+ let phraseStartWordOffset = sentence.wordRange.start
62
+
63
+ for (let wordIndex = phraseStartWordOffset; wordIndex < sentenceEndWordOffset; wordIndex++) {
64
+ const currentWord = wordSequence.getWordAt(wordIndex)
65
+
66
+ if (phraseSeparatorRegExp.test(currentWord)) {
67
+ let whitespaceSeenOnce = false
68
+
69
+ while (wordIndex < sentenceEndWordOffset - 1) {
70
+ const nextWord = wordSequence.getWordAt(wordIndex + 1)
71
+
72
+ if (!sentenceSeparatorTrailingPunctuationRegExp.test(nextWord)) {
73
+ break
74
+ }
75
+
76
+ if (whitespacePatternRegExp.test(nextWord)) {
77
+ whitespaceSeenOnce = true
78
+ }
79
+
80
+ if (nextWord === '"' && whitespaceSeenOnce) {
81
+ break
82
+ }
83
+
84
+ wordIndex += 1
85
+ }
86
+
87
+ phraseWordRanges.push({
88
+ start: phraseStartWordOffset,
89
+ end: wordIndex + 1
90
+ })
91
+
92
+ phraseStartWordOffset = wordIndex + 1
93
+ }
94
+ }
95
+
96
+ if (phraseStartWordOffset < sentenceEndWordOffset) {
97
+ phraseWordRanges.push({ start: phraseStartWordOffset, end: sentenceEndWordOffset })
98
+ }
99
+
100
+ for (const wordRange of phraseWordRanges) {
101
+ const phraseWordSequence = wordSequence.slice(wordRange.start, wordRange.end)
102
+
103
+ sentence.phrases.push(new Phrase(wordRange, phraseWordSequence))
104
+ }
105
+ }
106
+
107
+ const result: SegmentationResult = {
108
+ wordSequence,
109
+ sentences,
110
+ }
111
+
112
+ return result
113
+ }
114
+
115
+ export async function splitToWords(text: string, options?: SegmentationOptions) {
116
+ options = { ...defaultSegmentationOptions, ...(options ?? {}) }
117
+
118
+ const cldrSuppressionsForLang = cldrSuppressions[options.language ?? ''] ?? []
119
+ const contractionSuppressionsForLang = leadingApostropheContractionSuppressions[options.language ?? ''] ?? []
120
+ const contractionSuppressionsForLangWithSingleQuote = contractionSuppressionsForLang.map(str => str.replaceAll(`'`, `’`))
121
+ const customSuppressions = options.customSuppressions ?? []
122
+
123
+ let suppressions = [
124
+ ...customSuppressions,
125
+ ...cldrSuppressionsForLang,
126
+ ...contractionSuppressionsForLang,
127
+ ...contractionSuppressionsForLangWithSingleQuote,
128
+ ...nounSuppressions,
129
+ ...tldSuppressions,
130
+ ]
131
+
132
+ const wordPattern = buildWordSplitterPattern([
133
+ ...suppressions,
134
+ ...suppressions.map(word => word.toLocaleLowerCase()),
135
+ ...suppressions.map(word => word.toLocaleUpperCase()),
136
+ ])
137
+
138
+ const wordSplitterRegExp = buildRegExp(wordPattern, { global: true })
139
+
140
+ //console.log(`Encoded pattern: ${wordSplitterRegExp.source}\n`)
141
+
142
+ let wordSequence = new WordSequence()
143
+
144
+ function addNonWordsBetween(startOffset: number, endOffset: number) {
145
+ const nonWordSubstring = text.substring(startOffset, endOffset)
146
+
147
+ let charOffset = startOffset
148
+ let nonwordStartOffset = startOffset
149
+
150
+ function addNonWordIfNeeded() {
151
+ if (charOffset > nonwordStartOffset) {
152
+ wordSequence.addWordEntry(text, nonwordStartOffset, charOffset, true)
153
+ nonwordStartOffset = charOffset
154
+ }
155
+ }
156
+
157
+ for (const char of nonWordSubstring) {
158
+ if (char === ' ') {
159
+ charOffset += 1
160
+
161
+ continue
162
+ }
163
+
164
+ addNonWordIfNeeded()
165
+
166
+ charOffset += char.length
167
+
168
+ addNonWordIfNeeded()
169
+ }
170
+
171
+ addNonWordIfNeeded()
172
+ }
173
+
174
+ const wordMatches = text.matchAll(wordSplitterRegExp)
175
+
176
+ let lastMatchEndOffset = 0
177
+
178
+ if (wordMatches) {
179
+ for (const match of wordMatches) {
180
+ const offsets = match.indices![0]
181
+ const matchStartOffset = offsets[0]
182
+ const matchEndOffset = offsets[1]
183
+
184
+ if (matchStartOffset > lastMatchEndOffset) {
185
+ addNonWordsBetween(lastMatchEndOffset, matchStartOffset)
186
+ }
187
+
188
+ wordSequence.addWordEntry(text, matchStartOffset, matchEndOffset, false)
189
+
190
+ lastMatchEndOffset = matchEndOffset
191
+ }
192
+
193
+ addNonWordsBetween(lastMatchEndOffset, text.length)
194
+ }
195
+
196
+ if (options.enableEastAsianPostprocessing) {
197
+ wordSequence = await postprocessEastAsianWords(text, wordSequence)
198
+ }
199
+
200
+ return wordSequence
201
+ }
202
+
203
+ async function postprocessEastAsianWords(containingText: string, wordSequence: WordSequence) {
204
+ const icuSegmentation = await getIcuSegmentation()
205
+
206
+ if (icuSegmentation === undefined) {
207
+ return wordSequence
208
+ }
209
+
210
+ let icuInitialized = false
211
+
212
+ const newWordSequence = new WordSequence()
213
+
214
+ for (let wordIndex = 0; wordIndex < wordSequence.length; wordIndex++) {
215
+ const wordEntry = wordSequence.entries[wordIndex]
216
+ const wordStartOffset = wordEntry.startOffset
217
+
218
+ const word = wordSequence.getWordAt(wordIndex)
219
+
220
+ if (eastAsianCharRangesRegExp.test(word)) {
221
+ if (!icuInitialized) {
222
+ await icuSegmentation.initialize()
223
+
224
+ icuInitialized = true
225
+ }
226
+
227
+ const wordBreaks = [...icuSegmentation.createWordBreakIterator(word)]
228
+
229
+ for (let i = 0; i < wordBreaks.length - 1; i++) {
230
+ newWordSequence.addWordEntry(
231
+ containingText,
232
+ wordStartOffset + wordBreaks[i],
233
+ wordStartOffset + wordBreaks[i + 1],
234
+ false
235
+ )
236
+ }
237
+ } else {
238
+ newWordSequence.addWordEntry(containingText, wordEntry.startOffset, wordEntry.endOffset, wordEntry.isPunctuation)
239
+ }
240
+ }
241
+
242
+ return newWordSequence
243
+ }
244
+
245
+ async function getIcuSegmentation() {
246
+ try {
247
+ const icuSegmentation = await import('@echogarden/icu-segmentation-wasm')
248
+
249
+ return icuSegmentation
250
+ } catch {
251
+ return undefined
252
+ }
253
+ }
254
+
255
+ export interface SegmentationResult {
256
+ wordSequence: WordSequence
257
+ sentences: Sentence[]
258
+ }
259
+
260
+ export class TextFragment {
261
+ wordRange: Range
262
+ wordSequence: WordSequence
263
+
264
+ constructor(wordRange: Range, wordSequence: WordSequence) {
265
+ this.wordRange = wordRange
266
+ this.wordSequence = wordSequence
267
+ }
268
+
269
+ get text() {
270
+ return this.wordSequence.text
271
+ }
272
+
273
+ get charRange(): Range {
274
+ return {
275
+ start: this.wordSequence.firstEntry.startOffset,
276
+ end: this.wordSequence.lastEntry.endOffset
277
+ }
278
+ }
279
+ }
280
+
281
+ export class Sentence extends TextFragment {
282
+ phrases: Phrase[] = []
283
+ }
284
+
285
+ export class Phrase extends TextFragment {
286
+ }
287
+
288
+ export interface Range {
289
+ start: number
290
+ end: number
291
+ }
292
+
293
+ export interface SegmentationOptions {
294
+ language?: string
295
+ customSuppressions?: string[]
296
+ enableEastAsianPostprocessing?: boolean
297
+ }
298
+
299
+ export const defaultSegmentationOptions: SegmentationOptions = {
300
+ language: '',
301
+ customSuppressions: [],
302
+ enableEastAsianPostprocessing: true,
303
+ }