humanizer-lint 0.2.4 → 1.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.
@@ -1,82 +1,149 @@
1
- const ABBREVIATIONS = {
2
- id: ['dll.', 'dsb.', 'dst.', 'dkk.', 'hlm.', 'no.', 'dr.', 'prof.', 'ir.', 'hj.', 'pt.', 'cv.', 'tbk.', 'rp.', 'tgl.', 's.d.', 'a.n.', 'u.p.'],
3
- en: ['mr.', 'mrs.', 'ms.', 'dr.', 'prof.', 'sr.', 'jr.', 'vs.', 'etc.', 'e.g.', 'i.e.', 'fig.', 'inc.', 'ltd.', 'co.', 'st.'],
4
- };
5
-
6
- const DOT = '\uE000';
7
- const DECIMAL = '\uE001';
8
- const ELLIPSIS = '\uE002';
9
-
10
- function escapeRegExp(value) {
11
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
12
- }
13
-
14
- function protectAbbreviations(text, language) {
15
- const items = language === 'auto'
16
- ? [...new Set([...ABBREVIATIONS.id, ...ABBREVIATIONS.en])]
17
- : ABBREVIATIONS[language] ?? ABBREVIATIONS.en;
18
- let protectedText = text
19
- .replace(/\.{2,}/g, ELLIPSIS)
20
- .replace(/(\d)\.(\d)/g, `$1${DECIMAL}$2`);
21
- for (const abbreviation of items.sort((a, b) => b.length - a.length)) {
22
- protectedText = protectedText.replace(
23
- new RegExp(`\\b${escapeRegExp(abbreviation)}`, 'gi'),
24
- (match) => match.replaceAll('.', DOT),
25
- );
26
- }
27
- return protectedText;
28
- }
29
-
30
- export function tokenizeWords(text) {
31
- const normalized = String(text ?? '').replace(/—|--/g, ' ');
32
- return (normalized.toLowerCase().match(/token_entity|[\p{L}\p{N}]+(?:['’][\p{L}\p{N}]+)?(?:-[\p{L}\p{N}]+)*/gu) ?? []);
33
- }
34
-
35
- export function tokenizeText(input, language = 'auto') {
36
- const paragraphs = [];
37
- const sentences = [];
38
- const rawParagraphs = String(input ?? '').replace(/\r\n?/g, '\n').split(/\n\s*\n+/).map((value) => value.trim()).filter(Boolean);
39
-
40
- for (const [paragraphOffset, rawParagraph] of rawParagraphs.entries()) {
41
- const protectedText = protectAbbreviations(rawParagraph.replace(/\s+/g, ' '), language);
42
- const markedText = protectedText.replace(/([.!?](?:["')\]”’]+)?)\s+/gu, '$1\n');
43
- const parts = markedText.split('\n').map((value) => value
44
- .replaceAll(DOT, '.')
45
- .replaceAll(DECIMAL, '.')
46
- .replaceAll(ELLIPSIS, '...')
47
- .trim()).filter(Boolean);
48
- const paragraphSentences = [];
49
-
50
- for (const text of parts) {
51
- const words = tokenizeWords(text);
52
- if (!words.length) continue;
53
- const sentence = {
54
- text,
55
- words,
56
- wordCount: words.length,
57
- paragraphIndex: paragraphOffset + 1,
58
- sentenceIndex: sentences.length + 1,
59
- sentenceInParagraph: paragraphSentences.length + 1,
60
- };
61
- paragraphSentences.push(sentence);
62
- sentences.push(sentence);
63
- }
64
-
65
- if (paragraphSentences.length) {
66
- paragraphs.push({
67
- text: rawParagraph,
68
- paragraphIndex: paragraphOffset + 1,
69
- sentences: paragraphSentences,
70
- words: paragraphSentences.flatMap((sentence) => sentence.words),
71
- });
72
- }
73
- }
74
-
75
- return {
76
- paragraphs,
77
- sentences,
78
- words: sentences.flatMap((sentence) => sentence.words),
79
- };
80
- }
81
-
82
- export { ABBREVIATIONS };
1
+ const ABBREVIATIONS = {
2
+ id: ['dll.', 'dsb.', 'dst.', 'dkk.', 'hlm.', 'no.', 'dr.', 'prof.', 'ir.', 'hj.', 'pt.', 'cv.', 'tbk.', 'rp.', 'tgl.', 's.d.', 'a.n.', 'u.p.'],
3
+ en: ['mr.', 'mrs.', 'ms.', 'dr.', 'prof.', 'sr.', 'jr.', 'vs.', 'etc.', 'e.g.', 'i.e.', 'fig.', 'inc.', 'ltd.', 'co.', 'st.'],
4
+ eu: ['p.ej.', 'sr.', 'sra.', 'z.b.', 'bzw.', 'm.a.w.'],
5
+ };
6
+
7
+ const DOT = '\uE000';
8
+ const DECIMAL = '\uE001';
9
+ const ELLIPSIS = '\uE002';
10
+ const CJK_PATTERN = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f\uac00-\ud7af]/u;
11
+ const SPACED_TERMINAL_PUNCTUATION = /([.!?\u061f]+(?:["')\]”’]+)?)(?=\s|$)/gu;
12
+ const CJK_TERMINAL_PUNCTUATION = /([\u3002\uff01\uff1f]+(?:["')\]”’]+)?)(?=\s|$|[\p{L}\p{N}])/gu;
13
+ const WORD_CONNECTOR = /^(?:[-'’])$/u;
14
+
15
+ function escapeRegExp(value) {
16
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
17
+ }
18
+
19
+ export function normalizeLocale(language) {
20
+ const value = String(language ?? '').trim().toLowerCase();
21
+ if (!value || value === 'auto' || value === 'universal') return 'und';
22
+ if (!/^[a-z]{2}$/u.test(value)) return 'und';
23
+ try {
24
+ return Intl.getCanonicalLocales(value)[0] ?? 'und';
25
+ } catch {
26
+ return 'und';
27
+ }
28
+ }
29
+
30
+ export function isCJKText(text) {
31
+ return CJK_PATTERN.test(String(text ?? ''));
32
+ }
33
+
34
+ function segmenter(locale, granularity) {
35
+ if (typeof Intl?.Segmenter !== 'function') return null;
36
+ try {
37
+ return new Intl.Segmenter(normalizeLocale(locale), { granularity });
38
+ } catch {
39
+ return new Intl.Segmenter('und', { granularity });
40
+ }
41
+ }
42
+
43
+ function abbreviationsFor(language) {
44
+ if (language === 'id') return [...ABBREVIATIONS.id, ...ABBREVIATIONS.en, ...ABBREVIATIONS.eu];
45
+ if (language === 'en') return [...ABBREVIATIONS.en, ...ABBREVIATIONS.id, ...ABBREVIATIONS.eu];
46
+ return Object.values(ABBREVIATIONS).flat();
47
+ }
48
+
49
+ function protectAbbreviations(text, language) {
50
+ let protectedText = text
51
+ .replace(/\.{2,}/g, ELLIPSIS)
52
+ .replace(/(\d)\.(\d)/g, `$1${DECIMAL}$2`);
53
+ for (const abbreviation of [...new Set(abbreviationsFor(language))].sort((a, b) => b.length - a.length)) {
54
+ protectedText = protectedText.replace(
55
+ new RegExp(`\\b${escapeRegExp(abbreviation)}`, 'giu'),
56
+ (match) => match.replaceAll('.', DOT),
57
+ );
58
+ }
59
+ return protectedText;
60
+ }
61
+
62
+ function restoreProtectedText(text) {
63
+ return text
64
+ .replaceAll(DOT, '.')
65
+ .replaceAll(DECIMAL, '.')
66
+ .replaceAll(ELLIPSIS, '...');
67
+ }
68
+
69
+ function splitTerminalPunctuation(text) {
70
+ return text
71
+ .replace(SPACED_TERMINAL_PUNCTUATION, '$1\n')
72
+ .replace(CJK_TERMINAL_PUNCTUATION, '$1\n');
73
+ }
74
+
75
+ export function splitSentences(text, language = 'auto') {
76
+ const protectedText = protectAbbreviations(String(text ?? '').replace(/\s+/g, ' ').trim(), language);
77
+ if (!protectedText) return [];
78
+ const sentenceSegmenter = segmenter(language, 'sentence');
79
+ const segments = sentenceSegmenter
80
+ ? [...sentenceSegmenter.segment(protectedText)].map((entry) => entry.segment)
81
+ : [protectedText];
82
+ return segments
83
+ .flatMap((segment) => splitTerminalPunctuation(segment).split('\n'))
84
+ .map((segment) => restoreProtectedText(segment).trim())
85
+ .filter(Boolean);
86
+ }
87
+
88
+ function fallbackWords(text) {
89
+ return String(text ?? '').toLowerCase().match(/token_entity|[\p{L}\p{N}]+(?:['’][\p{L}\p{N}]+)?(?:-[\p{L}\p{N}]+)*/gu) ?? [];
90
+ }
91
+
92
+ export function tokenizeWords(text, language = 'auto') {
93
+ const normalized = String(text ?? '').replace(/—|--/g, ' ');
94
+ const wordSegmenter = segmenter(language, 'word');
95
+ if (!wordSegmenter) return fallbackWords(normalized);
96
+
97
+ const words = [];
98
+ let previousEnd = -1;
99
+ for (const entry of wordSegmenter.segment(normalized)) {
100
+ if (!entry.isWordLike) continue;
101
+ const word = entry.segment.toLowerCase();
102
+ const between = previousEnd < 0 ? '' : normalized.slice(previousEnd, entry.index);
103
+ if (words.length && WORD_CONNECTOR.test(between)) words[words.length - 1] += `${between}${word}`;
104
+ else words.push(word);
105
+ previousEnd = entry.index + entry.segment.length;
106
+ }
107
+ return words;
108
+ }
109
+
110
+ export function tokenizeText(input, language = 'auto') {
111
+ const paragraphs = [];
112
+ const sentences = [];
113
+ const rawParagraphs = String(input ?? '').replace(/\r\n?/g, '\n').split(/\n\s*\n+/).map((value) => value.trim()).filter(Boolean);
114
+
115
+ for (const [paragraphOffset, rawParagraph] of rawParagraphs.entries()) {
116
+ const paragraphSentences = [];
117
+ for (const text of splitSentences(rawParagraph, language)) {
118
+ const words = tokenizeWords(text, language);
119
+ if (!words.length) continue;
120
+ const sentence = {
121
+ text,
122
+ words,
123
+ wordCount: words.length,
124
+ paragraphIndex: paragraphOffset + 1,
125
+ sentenceIndex: sentences.length + 1,
126
+ sentenceInParagraph: paragraphSentences.length + 1,
127
+ };
128
+ paragraphSentences.push(sentence);
129
+ sentences.push(sentence);
130
+ }
131
+
132
+ if (paragraphSentences.length) {
133
+ paragraphs.push({
134
+ text: rawParagraph,
135
+ paragraphIndex: paragraphOffset + 1,
136
+ sentences: paragraphSentences,
137
+ words: paragraphSentences.flatMap((sentence) => sentence.words),
138
+ });
139
+ }
140
+ }
141
+
142
+ return {
143
+ paragraphs,
144
+ sentences,
145
+ words: sentences.flatMap((sentence) => sentence.words),
146
+ };
147
+ }
148
+
149
+ export { ABBREVIATIONS };