humanizer-lint 0.0.0-stage → 0.2.3
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 +21 -0
- package/README.md +469 -2
- package/SKILL.md +116 -0
- package/package.json +23 -4
- package/profiles/default-human.json +36 -0
- package/rules/en-slop.json +49 -0
- package/rules/id-slop.json +57 -0
- package/scripts/analyze.mjs +408 -0
- package/scripts/fingerprint.mjs +127 -0
- package/scripts/utils/metrics.mjs +73 -0
- package/scripts/utils/sanitizer.mjs +48 -0
- package/scripts/utils/tokenizer.mjs +82 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { deflateRawSync } from 'node:zlib';
|
|
2
|
+
|
|
3
|
+
export function mean(values) {
|
|
4
|
+
return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function standardDeviation(values) {
|
|
8
|
+
if (!values.length) return 0;
|
|
9
|
+
const average = mean(values);
|
|
10
|
+
return Math.sqrt(mean(values.map((value) => (value - average) ** 2)));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function coefficientOfVariation(values) {
|
|
14
|
+
const average = mean(values);
|
|
15
|
+
return average ? standardDeviation(values) / average : 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function adjacentDelta(values) {
|
|
19
|
+
if (values.length < 2) return 0;
|
|
20
|
+
let total = 0;
|
|
21
|
+
for (let index = 1; index < values.length; index += 1) total += Math.abs(values[index] - values[index - 1]);
|
|
22
|
+
return total / (values.length - 1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function mattr(words, requestedWindow) {
|
|
26
|
+
if (!words.length) return 0;
|
|
27
|
+
const window = Math.min(words.length, requestedWindow ?? Math.min(50, Math.max(10, Math.floor(words.length / 2))));
|
|
28
|
+
if (words.length <= window) return new Set(words).size / words.length;
|
|
29
|
+
let total = 0;
|
|
30
|
+
let windows = 0;
|
|
31
|
+
for (let index = 0; index <= words.length - window; index += 1) {
|
|
32
|
+
total += new Set(words.slice(index, index + window)).size / window;
|
|
33
|
+
windows += 1;
|
|
34
|
+
}
|
|
35
|
+
return total / windows;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function compressionRatio(text) {
|
|
39
|
+
const buffer = Buffer.from(String(text ?? '').trim(), 'utf8');
|
|
40
|
+
return buffer.length ? deflateRawSync(buffer).length / buffer.length : 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function punctuationRates(text, wordCount) {
|
|
44
|
+
const denominator = wordCount || 1;
|
|
45
|
+
const count = (pattern) => (String(text ?? '').match(pattern) ?? []).length / denominator;
|
|
46
|
+
return {
|
|
47
|
+
comma: count(/,/g),
|
|
48
|
+
semicolon: count(/;/g),
|
|
49
|
+
colon: count(/:/g),
|
|
50
|
+
question: count(/\?/g),
|
|
51
|
+
exclamation: count(/!/g),
|
|
52
|
+
parentheses: count(/[()]/g) / 2,
|
|
53
|
+
dash: count(/—|\s--\s/g),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function calculateMetrics(tokenized, normalizedText) {
|
|
58
|
+
const lengths = tokenized.sentences.map((sentence) => sentence.wordCount);
|
|
59
|
+
const wordCount = tokenized.words.length;
|
|
60
|
+
const sentenceCount = lengths.length;
|
|
61
|
+
return {
|
|
62
|
+
wordCount,
|
|
63
|
+
sentenceCount,
|
|
64
|
+
meanSentenceLength: mean(lengths),
|
|
65
|
+
stdDev: standardDeviation(lengths),
|
|
66
|
+
burstinessCV: coefficientOfVariation(lengths),
|
|
67
|
+
adjacentDelta: adjacentDelta(lengths),
|
|
68
|
+
shortSentenceRatio: sentenceCount ? lengths.filter((length) => length <= 6).length / sentenceCount : 0,
|
|
69
|
+
longSentenceRatio: sentenceCount ? lengths.filter((length) => length >= 26).length / sentenceCount : 0,
|
|
70
|
+
mattr: mattr(tokenized.words),
|
|
71
|
+
compressionRatio: compressionRatio(normalizedText),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const ENTITY = 'TOKEN_ENTITY';
|
|
2
|
+
|
|
3
|
+
function isCompleteListItem(text) {
|
|
4
|
+
const words = text.match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
5
|
+
return words.length >= 5 && /[.!?]["')\]]?$/.test(text.trim());
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function sanitizeMarkdown(input) {
|
|
9
|
+
let text = String(input ?? '').replace(/\r\n?/g, '\n');
|
|
10
|
+
text = text.replace(/^---\s*\n[\s\S]*?\n---\s*(?:\n|$)/, '');
|
|
11
|
+
text = text.replace(/^(?:```|~~~)[^\n]*\n[\s\S]*?^(?:```|~~~)\s*$/gm, '\n');
|
|
12
|
+
text = text.replace(/\[([^\]]+)\]\(https?:\/\/[^\s)]+\)/gi, '$1');
|
|
13
|
+
text = text.replace(/`[^`\n]+`/g, ENTITY);
|
|
14
|
+
text = text.replace(/https?:\/\/[^\s)]+/gi, (match) => {
|
|
15
|
+
const trailing = match.match(/[.!?,;:]+$/u)?.[0] ?? '';
|
|
16
|
+
return `${ENTITY}${trailing}`;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const sourceLines = text.split('\n');
|
|
20
|
+
const lines = [];
|
|
21
|
+
for (let index = 0; index < sourceLines.length; index += 1) {
|
|
22
|
+
const rawLine = sourceLines[index];
|
|
23
|
+
if (/^\s*#{1,6}\s+/.test(rawLine)) {
|
|
24
|
+
lines.push('');
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (/^\s*\|.*\|\s*$/.test(rawLine)) {
|
|
28
|
+
lines.push('');
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let line = rawLine.replace(/^\s*>\s?/, '');
|
|
33
|
+
const list = line.match(/^\s*(?:[-*+]|\d+\.)\s+(.*)$/);
|
|
34
|
+
if (list) {
|
|
35
|
+
let item = list[1].trim();
|
|
36
|
+
let cursor = index + 1;
|
|
37
|
+
while (cursor < sourceLines.length && /^\s{2,}\S/.test(sourceLines[cursor])) {
|
|
38
|
+
item += ` ${sourceLines[cursor].replace(/^\s*>\s?/, '').trim()}`;
|
|
39
|
+
cursor += 1;
|
|
40
|
+
}
|
|
41
|
+
index = cursor - 1;
|
|
42
|
+
line = isCompleteListItem(item) ? item : '';
|
|
43
|
+
}
|
|
44
|
+
lines.push(line.trimEnd());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
48
|
+
}
|
|
@@ -0,0 +1,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
|
+
};
|
|
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 };
|