@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.
- package/LICENSE.md +7 -0
- package/README.md +178 -0
- package/dist/EastAsianCharacterPatterns.d.ts +7 -0
- package/dist/EastAsianCharacterPatterns.js +24 -0
- package/dist/EastAsianCharacterPatterns.js.map +1 -0
- package/dist/Patterns.d.ts +34 -0
- package/dist/Patterns.js +126 -0
- package/dist/Patterns.js.map +1 -0
- package/dist/Suppressions.d.ts +4 -0
- package/dist/Suppressions.js +31 -0
- package/dist/Suppressions.js.map +1 -0
- package/dist/Test.d.ts +1 -0
- package/dist/Test.js +39 -0
- package/dist/Test.js.map +1 -0
- package/dist/TextSegmentation.d.ts +31 -0
- package/dist/TextSegmentation.js +208 -0
- package/dist/TextSegmentation.js.map +1 -0
- package/dist/WordSequence.d.ts +26 -0
- package/dist/WordSequence.js +67 -0
- package/dist/WordSequence.js.map +1 -0
- package/dist/utilities/Timer.d.ts +13 -0
- package/dist/utilities/Timer.js +70 -0
- package/dist/utilities/Timer.js.map +1 -0
- package/dist/utilities/Utilities.d.ts +6 -0
- package/dist/utilities/Utilities.js +24 -0
- package/dist/utilities/Utilities.js.map +1 -0
- package/package.json +55 -0
- package/src/EastAsianCharacterPatterns.ts +45 -0
- package/src/Patterns.ts +225 -0
- package/src/Suppressions.ts +35 -0
- package/src/Test.ts +55 -0
- package/src/TextSegmentation.ts +303 -0
- package/src/WordSequence.ts +94 -0
- package/src/utilities/Timer.ts +95 -0
- package/src/utilities/Utilities.ts +33 -0
- package/tsconfig.json +105 -0
package/src/Patterns.ts
ADDED
|
@@ -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
|
+
}
|