humanizer-lint 1.1.0 → 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.
- package/README.md +24 -5
- package/SKILL.md +7 -5
- package/package.json +3 -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,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 };
|