humanizer-lint 1.1.0 → 1.2.1
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/README.md +40 -9
- package/SKILL.md +19 -21
- package/package.json +6 -2
- package/rules/de-slop.json +190 -0
- package/rules/en-slop.json +50 -49
- package/rules/es-slop.json +190 -0
- package/rules/fr-slop.json +190 -0
- package/rules/id-slop.json +58 -57
- package/rules/ms-slop.json +190 -0
- package/rules/pt-slop.json +190 -0
- package/rules/zh-slop.json +190 -0
- package/scripts/analyze.mjs +9 -3
- package/scripts/core/analyzer-core.mjs +51 -24
- package/scripts/utils/tokenizer.mjs +149 -82
|
@@ -1,23 +1,45 @@
|
|
|
1
1
|
import { sanitizeMarkdown } from '../utils/sanitizer.mjs';
|
|
2
2
|
import { mean, punctuationRates } from '../utils/metrics-core.mjs';
|
|
3
|
-
import { tokenizeText } from '../utils/tokenizer.mjs';
|
|
3
|
+
import { isCJKText, tokenizeText } from '../utils/tokenizer.mjs';
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
5
|
+
const HAN = /[\u4e00-\u9fff]/gu;
|
|
6
|
+
const KANA = /[\u3040-\u30ff]/u;
|
|
7
|
+
const ARABIC = /[\u0600-\u06ff]/u;
|
|
8
|
+
const ID_EXCLUSIVE_WORDS = new Set(['tidak', 'karena', 'bisa', 'seperti', 'dari', 'bahwa']);
|
|
9
|
+
const MS_EXCLUSIVE_WORDS = new Set(['tak', 'kerana', 'boleh', 'bagi', 'daripada', 'bahawa', 'beliau']);
|
|
7
10
|
|
|
8
11
|
function round(value, places = 4) {
|
|
9
12
|
const factor = 10 ** places;
|
|
10
13
|
return Math.round(value * factor) / factor;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
function detectLanguage(words) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
function detectLanguage(text, words, rules) {
|
|
17
|
+
if (KANA.test(text)) return 'ja';
|
|
18
|
+
if (ARABIC.test(text)) return 'ar';
|
|
19
|
+
const hanCount = (text.match(HAN) ?? []).length;
|
|
20
|
+
if (hanCount > 0 && isCJKText(text)) return 'zh';
|
|
21
|
+
|
|
22
|
+
const scores = new Map();
|
|
23
|
+
for (const [language, rule] of Object.entries(rules)) {
|
|
24
|
+
let score = 0;
|
|
25
|
+
const stopwords = new Set(rule.stopwords ?? []);
|
|
26
|
+
for (const word of words) if (stopwords.has(word)) score += 1;
|
|
27
|
+
scores.set(language, score);
|
|
28
|
+
}
|
|
29
|
+
if (scores.has('id') && scores.has('ms')) {
|
|
30
|
+
const idExclusive = words.filter((word) => ID_EXCLUSIVE_WORDS.has(word)).length;
|
|
31
|
+
const msExclusive = words.filter((word) => MS_EXCLUSIVE_WORDS.has(word)).length;
|
|
32
|
+
scores.set('id', scores.get('id') + idExclusive * 2);
|
|
33
|
+
scores.set('ms', scores.get('ms') + msExclusive * 2);
|
|
19
34
|
}
|
|
20
|
-
|
|
35
|
+
|
|
36
|
+
const [language, score] = [...scores.entries()].sort((left, right) => right[1] - left[1])[0] ?? ['universal', 0];
|
|
37
|
+
return score >= Math.max(2, Math.ceil(words.length * 0.04)) ? language : 'universal';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function mattrThreshold(thresholds, language, tokenized) {
|
|
41
|
+
if (typeof thresholds.mattr?.[language] === 'number') return thresholds.mattr[language];
|
|
42
|
+
return isCJKText(tokenized.words.join('')) ? 0.68 : 0.72;
|
|
21
43
|
}
|
|
22
44
|
|
|
23
45
|
function locationFor(sentence) {
|
|
@@ -29,6 +51,7 @@ function regexFor(pattern) {
|
|
|
29
51
|
}
|
|
30
52
|
|
|
31
53
|
function patternViolations(tokenized, rules, wordCount) {
|
|
54
|
+
if (!rules?.patterns) return [];
|
|
32
55
|
const violations = [];
|
|
33
56
|
for (const rule of rules.patterns) {
|
|
34
57
|
const candidates = [];
|
|
@@ -122,7 +145,7 @@ function structuralViolations(tokenized, metrics) {
|
|
|
122
145
|
return violations;
|
|
123
146
|
}
|
|
124
147
|
|
|
125
|
-
function statisticalViolations(metrics, thresholds,
|
|
148
|
+
function statisticalViolations(metrics, thresholds, mattrTarget) {
|
|
126
149
|
const violations = [];
|
|
127
150
|
const cvTarget = metrics.sentenceCount >= 8 ? thresholds.burstinessCVLong : thresholds.burstinessCVShort;
|
|
128
151
|
if (metrics.burstinessCV < cvTarget) {
|
|
@@ -152,12 +175,12 @@ function statisticalViolations(metrics, thresholds, language) {
|
|
|
152
175
|
fixInstruction: 'Change neighboring sentence lengths by splitting or merging only where the content allows it.',
|
|
153
176
|
});
|
|
154
177
|
}
|
|
155
|
-
if (metrics.mattr <
|
|
178
|
+
if (metrics.mattr < mattrTarget) {
|
|
156
179
|
violations.push({
|
|
157
180
|
type: 'LOW_LEXICAL_DIVERSITY',
|
|
158
181
|
location: 'Whole text',
|
|
159
182
|
actual: round(metrics.mattr),
|
|
160
|
-
target:
|
|
183
|
+
target: mattrTarget,
|
|
161
184
|
fixInstruction: 'Replace repeated generic wording with precise terms already supported by the source.',
|
|
162
185
|
});
|
|
163
186
|
}
|
|
@@ -244,7 +267,7 @@ function voiceDeviation(metrics, tokenized, text, profile) {
|
|
|
244
267
|
return result < 0.0005 ? 0 : result;
|
|
245
268
|
}
|
|
246
269
|
|
|
247
|
-
function scoreResult(metrics, violations,
|
|
270
|
+
function scoreResult(metrics, violations, thresholds, mattrTarget) {
|
|
248
271
|
const cvTarget = metrics.sentenceCount >= 8 ? thresholds.burstinessCVLong : thresholds.burstinessCVShort;
|
|
249
272
|
let burstinessPenalty = metrics.burstinessCV < cvTarget ? Math.min(25, Math.round((cvTarget - metrics.burstinessCV) * 100)) : 0;
|
|
250
273
|
if (metrics.sentenceCount >= 8 && (metrics.shortSentenceRatio < thresholds.shortSentenceRatio || metrics.longSentenceRatio < thresholds.longSentenceRatio)) {
|
|
@@ -259,7 +282,7 @@ function scoreResult(metrics, violations, language, thresholds) {
|
|
|
259
282
|
const morphologyPenalty = Math.min(15,
|
|
260
283
|
(violations.some((violation) => violation.type === 'EXCESSIVE_NOMINALIZATION') ? 7.5 : 0)
|
|
261
284
|
+ Math.min(15, violations.filter((violation) => violation.type === 'PASSIVE_BLOATING').length * 7.5));
|
|
262
|
-
const vocabularyPenalty = metrics.mattr <
|
|
285
|
+
const vocabularyPenalty = metrics.mattr < mattrTarget ? 5 : 0;
|
|
263
286
|
const entropyPenalty = violations.some((violation) => ['LOW_ENTROPY_REPETITION', 'SYMMETRIC_PARAGRAPHS', 'REPETITIVE_STARTERS'].includes(violation.type)) ? 5 : 0;
|
|
264
287
|
const profilePenalty = violations.some((violation) => violation.type === 'VOICE_PROFILE_DEVIATION') ? 5 : 0;
|
|
265
288
|
const vocabularyAndProfilePenalty = Math.min(10, vocabularyPenalty + entropyPenalty + profilePenalty);
|
|
@@ -267,25 +290,27 @@ function scoreResult(metrics, violations, language, thresholds) {
|
|
|
267
290
|
}
|
|
268
291
|
|
|
269
292
|
export function createAnalyzer({ rules, defaultProfile, calculateMetrics, calculateMetricsAsync }) {
|
|
270
|
-
if (!rules
|
|
293
|
+
if (!rules || !defaultProfile || typeof calculateMetrics !== 'function') {
|
|
271
294
|
throw new TypeError('createAnalyzer requires rules, defaultProfile, and calculateMetrics');
|
|
272
295
|
}
|
|
273
296
|
|
|
274
297
|
const prepare = (input, options) => {
|
|
275
298
|
const sanitized = sanitizeMarkdown(input);
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
|
|
299
|
+
const requestedLanguage = String(options.language ?? 'auto').toLowerCase();
|
|
300
|
+
const initial = tokenizeText(sanitized, requestedLanguage);
|
|
301
|
+
const language = requestedLanguage === 'auto' ? detectLanguage(sanitized, initial.words, rules) : requestedLanguage;
|
|
279
302
|
return {
|
|
280
303
|
sanitized,
|
|
281
304
|
language,
|
|
282
|
-
|
|
305
|
+
rulePack: rules[language] ? language : 'universal',
|
|
306
|
+
tokenized: language === requestedLanguage ? initial : tokenizeText(sanitized, language),
|
|
283
307
|
};
|
|
284
308
|
};
|
|
285
309
|
|
|
286
|
-
const analyzePrepared = ({ sanitized, language, tokenized }, options, metrics) => {
|
|
310
|
+
const analyzePrepared = ({ sanitized, language, rulePack, tokenized }, options, metrics) => {
|
|
287
311
|
const profile = options.profile ?? defaultProfile;
|
|
288
312
|
const thresholds = { ...defaultProfile.thresholds, ...(profile.thresholds ?? {}) };
|
|
313
|
+
const mattrTarget = mattrThreshold(thresholds, language, tokenized);
|
|
289
314
|
const patternMatches = patternViolations(tokenized, rules[language], metrics.wordCount);
|
|
290
315
|
|
|
291
316
|
if (metrics.wordCount < thresholds.minimumWords || metrics.sentenceCount < thresholds.minimumSentences) {
|
|
@@ -294,13 +319,14 @@ export function createAnalyzer({ rules, defaultProfile, calculateMetrics, calcul
|
|
|
294
319
|
status: 'SKIP',
|
|
295
320
|
score: 100,
|
|
296
321
|
language,
|
|
322
|
+
rulePack,
|
|
297
323
|
warning: 'Text too short for statistical stylometry (< 3 sentences)',
|
|
298
324
|
metrics: Object.fromEntries(Object.entries({ ...metrics, nominalizationRatio: 0, passiveRatio: 0, voiceProfileDeviation: 0 }).map(([key, value]) => [key, typeof value === 'number' ? round(value) : value])),
|
|
299
325
|
violations,
|
|
300
326
|
};
|
|
301
327
|
}
|
|
302
328
|
|
|
303
|
-
const morphology = morphologyViolations(tokenized, rules[language]
|
|
329
|
+
const morphology = morphologyViolations(tokenized, rules[language]?.morphology);
|
|
304
330
|
metrics.nominalizationRatio = morphology.nominalizationRatio;
|
|
305
331
|
metrics.passiveRatio = morphology.passiveRatio;
|
|
306
332
|
metrics.voiceProfileDeviation = voiceDeviation(metrics, tokenized, sanitized, profile);
|
|
@@ -319,9 +345,9 @@ export function createAnalyzer({ rules, defaultProfile, calculateMetrics, calcul
|
|
|
319
345
|
...structuralViolations(tokenized, metrics),
|
|
320
346
|
...morphology.violations,
|
|
321
347
|
...profileViolations,
|
|
322
|
-
...statisticalViolations(metrics, thresholds,
|
|
348
|
+
...statisticalViolations(metrics, thresholds, mattrTarget),
|
|
323
349
|
];
|
|
324
|
-
const score = scoreResult(metrics, violations,
|
|
350
|
+
const score = scoreResult(metrics, violations, thresholds, mattrTarget);
|
|
325
351
|
const cvTarget = metrics.sentenceCount >= 8 ? thresholds.burstinessCVLong : thresholds.burstinessCVShort;
|
|
326
352
|
const forbidden = violations.some((violation) => violation.type === 'SLOP_PHRASE' || violation.type === 'DEFINITIONAL_OPENING');
|
|
327
353
|
const rhythmPass = metrics.adjacentDelta >= thresholds.adjacentDelta;
|
|
@@ -332,6 +358,7 @@ export function createAnalyzer({ rules, defaultProfile, calculateMetrics, calcul
|
|
|
332
358
|
status,
|
|
333
359
|
score,
|
|
334
360
|
language,
|
|
361
|
+
rulePack,
|
|
335
362
|
metrics: Object.fromEntries(Object.entries(metrics).map(([key, value]) => [key, typeof value === 'number' ? round(value) : value])),
|
|
336
363
|
violations,
|
|
337
364
|
};
|
|
@@ -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
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 };
|