slopfix 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.fr.md +58 -0
- package/README.md +224 -2
- package/bin/slopfix.js +233 -0
- package/index.d.ts +95 -0
- package/package.json +56 -4
- package/src/format.js +110 -0
- package/src/index.js +182 -0
- package/src/mask.js +65 -0
- package/src/rules/common.js +86 -0
- package/src/rules/en.js +221 -0
- package/src/rules/fr.js +206 -0
- package/src/rules/helpers.js +94 -0
package/src/rules/en.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { phraseRule, regexRule } from './helpers.js';
|
|
2
|
+
|
|
3
|
+
// English rules. To add a phrase, add a line to the right list.
|
|
4
|
+
// "phrase" only reports it, ["phrase", "replacement"] also fixes it with --fix.
|
|
5
|
+
|
|
6
|
+
const lang = 'en';
|
|
7
|
+
|
|
8
|
+
export default [
|
|
9
|
+
phraseRule({
|
|
10
|
+
id: 'en/chatbot-artifact',
|
|
11
|
+
lang,
|
|
12
|
+
category: 'chatbot',
|
|
13
|
+
severity: 'error',
|
|
14
|
+
message: 'Chatbot leftover. This was clearly pasted from an assistant.',
|
|
15
|
+
phrases: [
|
|
16
|
+
'as an AI language model',
|
|
17
|
+
'as an AI model',
|
|
18
|
+
'as a large language model',
|
|
19
|
+
'my knowledge cutoff',
|
|
20
|
+
'as of my last knowledge update',
|
|
21
|
+
'as of my last update',
|
|
22
|
+
'I cannot browse the internet',
|
|
23
|
+
"I don't have access to real-time",
|
|
24
|
+
'I hope this email finds you well',
|
|
25
|
+
'I hope this message finds you well',
|
|
26
|
+
"I'd be happy to help",
|
|
27
|
+
'I would be happy to help',
|
|
28
|
+
'great question',
|
|
29
|
+
"here's a breakdown",
|
|
30
|
+
'here is a breakdown',
|
|
31
|
+
"here's a comprehensive",
|
|
32
|
+
'here is a comprehensive',
|
|
33
|
+
'regenerate response',
|
|
34
|
+
],
|
|
35
|
+
}),
|
|
36
|
+
regexRule({
|
|
37
|
+
id: 'en/sycophantic-opener',
|
|
38
|
+
lang,
|
|
39
|
+
category: 'chatbot',
|
|
40
|
+
severity: 'error',
|
|
41
|
+
message: 'Sycophantic chatbot opener. Removed with --fix.',
|
|
42
|
+
patterns: [/^[ \t]*(?:certainly|absolutely|of course|sure thing)![ \t]*/gimu],
|
|
43
|
+
fix: '',
|
|
44
|
+
}),
|
|
45
|
+
regexRule({
|
|
46
|
+
id: 'en/chatbot-sign-off',
|
|
47
|
+
lang,
|
|
48
|
+
category: 'chatbot',
|
|
49
|
+
severity: 'error',
|
|
50
|
+
message: 'Chatbot sign-off. The whole sentence is removed with --fix.',
|
|
51
|
+
patterns: [
|
|
52
|
+
/(?:(?<=[.!?:])[ \t]+|^[ \t]*)(?:I hope (?:this|that) helps|let me know if you (?:have|need) (?:any|anything)|feel free to (?:reach out|ask|let me know)|don['’]t hesitate to (?:reach out|ask)|happy (?:coding|writing)!)[^.!?\n]*[.!?]*/gimu,
|
|
53
|
+
],
|
|
54
|
+
fix: '',
|
|
55
|
+
}),
|
|
56
|
+
phraseRule({
|
|
57
|
+
id: 'en/filler-opener',
|
|
58
|
+
lang,
|
|
59
|
+
category: 'filler',
|
|
60
|
+
severity: 'warning',
|
|
61
|
+
message: 'Filler phrase. Delete it and say the thing.',
|
|
62
|
+
drop: true,
|
|
63
|
+
phrases: [
|
|
64
|
+
"it's important to note that",
|
|
65
|
+
'it is important to note that',
|
|
66
|
+
"it's worth noting that",
|
|
67
|
+
'it is worth noting that',
|
|
68
|
+
"it's worth mentioning that",
|
|
69
|
+
'it is worth mentioning that',
|
|
70
|
+
"it's important to remember that",
|
|
71
|
+
'it is important to remember that',
|
|
72
|
+
"it's crucial to understand that",
|
|
73
|
+
'it is crucial to understand that',
|
|
74
|
+
'it should be noted that',
|
|
75
|
+
'needless to say,',
|
|
76
|
+
'at the end of the day,',
|
|
77
|
+
"in today's fast-paced world,",
|
|
78
|
+
"in today's digital age,",
|
|
79
|
+
"in today's digital landscape,",
|
|
80
|
+
"in today's ever-evolving landscape,",
|
|
81
|
+
'in an ever-evolving world,',
|
|
82
|
+
'without further ado,',
|
|
83
|
+
'in conclusion,',
|
|
84
|
+
'in summary,',
|
|
85
|
+
'to sum up,',
|
|
86
|
+
'all in all,',
|
|
87
|
+
],
|
|
88
|
+
}),
|
|
89
|
+
phraseRule({
|
|
90
|
+
id: 'en/hype-intro',
|
|
91
|
+
lang,
|
|
92
|
+
category: 'filler',
|
|
93
|
+
severity: 'warning',
|
|
94
|
+
message: 'Hype intro or outro typical of generated text.',
|
|
95
|
+
phrases: [
|
|
96
|
+
"let's dive in",
|
|
97
|
+
"let's dive into",
|
|
98
|
+
"let's dive deeper",
|
|
99
|
+
'buckle up',
|
|
100
|
+
'in the ever-evolving world of',
|
|
101
|
+
'in the ever-evolving landscape of',
|
|
102
|
+
'in the realm of',
|
|
103
|
+
'when it comes to',
|
|
104
|
+
'navigate the complexities',
|
|
105
|
+
'navigating the complexities',
|
|
106
|
+
'stands as a testament',
|
|
107
|
+
'serves as a testament',
|
|
108
|
+
'a testament to',
|
|
109
|
+
'plays a crucial role',
|
|
110
|
+
'plays a pivotal role',
|
|
111
|
+
'plays a vital role',
|
|
112
|
+
'unlock the full potential',
|
|
113
|
+
'unlock the power of',
|
|
114
|
+
'harness the power of',
|
|
115
|
+
'a game-changer',
|
|
116
|
+
'a game changer',
|
|
117
|
+
'take it to the next level',
|
|
118
|
+
],
|
|
119
|
+
}),
|
|
120
|
+
phraseRule({
|
|
121
|
+
id: 'en/ai-vocabulary',
|
|
122
|
+
lang,
|
|
123
|
+
category: 'vocabulary',
|
|
124
|
+
severity: 'warning',
|
|
125
|
+
message: 'Word that is heavily overused by language models.',
|
|
126
|
+
phrases: [
|
|
127
|
+
['delve into', 'dig into'],
|
|
128
|
+
['delves into', 'digs into'],
|
|
129
|
+
['delving into', 'digging into'],
|
|
130
|
+
['delved into', 'dug into'],
|
|
131
|
+
['delve', 'dig'],
|
|
132
|
+
['delves', 'digs'],
|
|
133
|
+
'tapestry',
|
|
134
|
+
['utilize', 'use'],
|
|
135
|
+
['utilizes', 'uses'],
|
|
136
|
+
['utilizing', 'using'],
|
|
137
|
+
['utilized', 'used'],
|
|
138
|
+
['a plethora of', 'many'],
|
|
139
|
+
['a myriad of', 'many'],
|
|
140
|
+
['myriad', 'many'],
|
|
141
|
+
['embark on', 'start'],
|
|
142
|
+
['embarks on', 'starts'],
|
|
143
|
+
['embarking on', 'starting'],
|
|
144
|
+
'ever-evolving',
|
|
145
|
+
'ever-changing landscape',
|
|
146
|
+
'seamlessly',
|
|
147
|
+
'meticulously',
|
|
148
|
+
'meticulous',
|
|
149
|
+
'bustling',
|
|
150
|
+
'labyrinth',
|
|
151
|
+
'symphony of',
|
|
152
|
+
'treasure trove',
|
|
153
|
+
'beacon of',
|
|
154
|
+
'paradigm shift',
|
|
155
|
+
'synergy',
|
|
156
|
+
'synergies',
|
|
157
|
+
'commendable',
|
|
158
|
+
],
|
|
159
|
+
}),
|
|
160
|
+
phraseRule({
|
|
161
|
+
id: 'en/weasel-vocabulary',
|
|
162
|
+
lang,
|
|
163
|
+
category: 'vocabulary',
|
|
164
|
+
severity: 'info',
|
|
165
|
+
message: 'Vague word that language models lean on. Fine once, suspicious in bulk.',
|
|
166
|
+
phrases: [
|
|
167
|
+
'robust',
|
|
168
|
+
'seamless',
|
|
169
|
+
'pivotal',
|
|
170
|
+
'crucial',
|
|
171
|
+
'intricate',
|
|
172
|
+
'multifaceted',
|
|
173
|
+
'nuanced',
|
|
174
|
+
'vibrant',
|
|
175
|
+
'leverage',
|
|
176
|
+
'leveraging',
|
|
177
|
+
'elevate',
|
|
178
|
+
'elevating',
|
|
179
|
+
'foster',
|
|
180
|
+
'fostering',
|
|
181
|
+
'underscore',
|
|
182
|
+
'underscores',
|
|
183
|
+
'showcase',
|
|
184
|
+
'showcasing',
|
|
185
|
+
'boasts',
|
|
186
|
+
'paramount',
|
|
187
|
+
'transformative',
|
|
188
|
+
'cutting-edge',
|
|
189
|
+
'revolutionize',
|
|
190
|
+
'revolutionizes',
|
|
191
|
+
'holistic',
|
|
192
|
+
'empower',
|
|
193
|
+
'empowers',
|
|
194
|
+
'furthermore',
|
|
195
|
+
'moreover',
|
|
196
|
+
'additionally',
|
|
197
|
+
'realm',
|
|
198
|
+
'landscape',
|
|
199
|
+
'journey',
|
|
200
|
+
],
|
|
201
|
+
}),
|
|
202
|
+
regexRule({
|
|
203
|
+
id: 'en/not-x-but-y',
|
|
204
|
+
lang,
|
|
205
|
+
category: 'structure',
|
|
206
|
+
severity: 'warning',
|
|
207
|
+
message: '"It\'s not X, it\'s Y" construction. One of the strongest AI tells.',
|
|
208
|
+
patterns: [
|
|
209
|
+
/(?<![\p{L}])[\p{L}\p{N}-]+(?:(?:['’]s| is| was) not| isn['’]t| wasn['’]t) (?:just |only |merely |simply |about )?[^.!?\n]{1,80}?[,;.:\u2014]\s*(?:it|this|that|they)(?:['’]s|['’]re| is| was| are)(?![\p{L}])/giu,
|
|
210
|
+
/(?<![\p{L}])not because [^.!?\n]{1,80}?[,;\u2014]?\s*but because(?![\p{L}])/giu,
|
|
211
|
+
],
|
|
212
|
+
}),
|
|
213
|
+
regexRule({
|
|
214
|
+
id: 'en/whether-youre',
|
|
215
|
+
lang,
|
|
216
|
+
category: 'structure',
|
|
217
|
+
severity: 'info',
|
|
218
|
+
message: '"Whether you\'re a X or a Y" opener. Generic audience framing.',
|
|
219
|
+
patterns: [/(?<![\p{L}])whether you(?:['’]re| are)\b[^.!?\n]{1,80}?(?<![\p{L}])or(?![\p{L}])/giu],
|
|
220
|
+
}),
|
|
221
|
+
];
|
package/src/rules/fr.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { phraseRule, regexRule } from './helpers.js';
|
|
2
|
+
|
|
3
|
+
// Règles françaises. Pour ajouter une expression, ajoutez une ligne dans la bonne liste.
|
|
4
|
+
// "expression" la signale, ["expression", "remplacement"] la corrige aussi avec --fix.
|
|
5
|
+
|
|
6
|
+
const lang = 'fr';
|
|
7
|
+
|
|
8
|
+
export default [
|
|
9
|
+
phraseRule({
|
|
10
|
+
id: 'fr/chatbot-artifact',
|
|
11
|
+
lang,
|
|
12
|
+
category: 'chatbot',
|
|
13
|
+
severity: 'error',
|
|
14
|
+
message: "Reste de chatbot. Ce texte a été copié depuis un assistant IA.",
|
|
15
|
+
phrases: [
|
|
16
|
+
"en tant qu'IA",
|
|
17
|
+
"en tant qu'intelligence artificielle",
|
|
18
|
+
'en tant que modèle de langage',
|
|
19
|
+
'en tant que grand modèle de langage',
|
|
20
|
+
'ma date de coupure',
|
|
21
|
+
'mes connaissances s\'arrêtent',
|
|
22
|
+
"je n'ai pas accès à internet",
|
|
23
|
+
"j'espère que ce message vous trouve en bonne santé",
|
|
24
|
+
'je serais ravi de vous aider',
|
|
25
|
+
'je serais ravie de vous aider',
|
|
26
|
+
'excellente question',
|
|
27
|
+
'très bonne question',
|
|
28
|
+
'voici un résumé détaillé',
|
|
29
|
+
'voici une analyse détaillée',
|
|
30
|
+
'voici un aperçu complet',
|
|
31
|
+
],
|
|
32
|
+
}),
|
|
33
|
+
regexRule({
|
|
34
|
+
id: 'fr/sycophantic-opener',
|
|
35
|
+
lang,
|
|
36
|
+
category: 'chatbot',
|
|
37
|
+
severity: 'error',
|
|
38
|
+
message: "Formule d'ouverture typique de chatbot. Supprimée avec --fix.",
|
|
39
|
+
patterns: [/^[ \t]*(?:bien sûr|absolument|avec plaisir|certainement)[ ]?![ \t]*/gimu],
|
|
40
|
+
fix: '',
|
|
41
|
+
}),
|
|
42
|
+
regexRule({
|
|
43
|
+
id: 'fr/chatbot-sign-off',
|
|
44
|
+
lang,
|
|
45
|
+
category: 'chatbot',
|
|
46
|
+
severity: 'error',
|
|
47
|
+
message: 'Formule de fin de chatbot. La phrase entière est supprimée avec --fix.',
|
|
48
|
+
patterns: [
|
|
49
|
+
/(?:(?<=[.!?:])[ \t]+|^[ \t]*)(?:j['’]espère que (?:cela|ça|ceci) (?:vous |t['’])?(?:aide|aidera|sera utile|a aidé)|n['’]hésitez pas (?:si vous avez|à (?:me|nous) (?:contacter|poser|faire signe|solliciter))|si vous avez (?:d['’]autres|des) questions)[^.!?\n]*[.!?]*/gimu,
|
|
50
|
+
],
|
|
51
|
+
fix: '',
|
|
52
|
+
}),
|
|
53
|
+
phraseRule({
|
|
54
|
+
id: 'fr/filler-opener',
|
|
55
|
+
lang,
|
|
56
|
+
category: 'filler',
|
|
57
|
+
severity: 'warning',
|
|
58
|
+
message: 'Formule de remplissage. Supprimez-la et allez droit au but.',
|
|
59
|
+
drop: true,
|
|
60
|
+
phrases: [
|
|
61
|
+
'il est important de noter que',
|
|
62
|
+
'il est important de souligner que',
|
|
63
|
+
'il est important de rappeler que',
|
|
64
|
+
'il est essentiel de noter que',
|
|
65
|
+
'il est essentiel de souligner que',
|
|
66
|
+
'il est intéressant de noter que',
|
|
67
|
+
'il convient de noter que',
|
|
68
|
+
'il convient de souligner que',
|
|
69
|
+
'il est à noter que',
|
|
70
|
+
'force est de constater que',
|
|
71
|
+
"dans le monde d'aujourd'hui,",
|
|
72
|
+
'dans le monde actuel,',
|
|
73
|
+
'dans un monde en constante évolution,',
|
|
74
|
+
'dans un monde en perpétuelle évolution,',
|
|
75
|
+
"à l'ère du numérique,",
|
|
76
|
+
"à l'ère du digital,",
|
|
77
|
+
'sans plus attendre,',
|
|
78
|
+
'en conclusion,',
|
|
79
|
+
'en résumé,',
|
|
80
|
+
'pour conclure,',
|
|
81
|
+
'en définitive,',
|
|
82
|
+
'en somme,',
|
|
83
|
+
],
|
|
84
|
+
}),
|
|
85
|
+
phraseRule({
|
|
86
|
+
id: 'fr/hype-intro',
|
|
87
|
+
lang,
|
|
88
|
+
category: 'filler',
|
|
89
|
+
severity: 'warning',
|
|
90
|
+
message: "Accroche ou transition grandiloquente typique d'un texte généré.",
|
|
91
|
+
phrases: [
|
|
92
|
+
'plongeons dans',
|
|
93
|
+
'plongeons au cœur de',
|
|
94
|
+
'plongeons ensemble',
|
|
95
|
+
'plongeons sans plus attendre',
|
|
96
|
+
'embarquons pour',
|
|
97
|
+
'découvrons ensemble',
|
|
98
|
+
'explorons ensemble',
|
|
99
|
+
'naviguer dans les méandres',
|
|
100
|
+
'naviguer dans le paysage',
|
|
101
|
+
'dans le paysage numérique',
|
|
102
|
+
'dans le paysage actuel',
|
|
103
|
+
'joue un rôle crucial',
|
|
104
|
+
'joue un rôle essentiel',
|
|
105
|
+
'joue un rôle clé',
|
|
106
|
+
'joue un rôle prépondérant',
|
|
107
|
+
'témoigne de',
|
|
108
|
+
'un véritable atout',
|
|
109
|
+
'un atout majeur',
|
|
110
|
+
'change la donne',
|
|
111
|
+
'changer la donne',
|
|
112
|
+
'révolutionne la manière',
|
|
113
|
+
'à un tout autre niveau',
|
|
114
|
+
'passer au niveau supérieur',
|
|
115
|
+
'libérer tout le potentiel',
|
|
116
|
+
'exploiter tout le potentiel',
|
|
117
|
+
'tirer pleinement parti',
|
|
118
|
+
],
|
|
119
|
+
}),
|
|
120
|
+
phraseRule({
|
|
121
|
+
id: 'fr/ai-vocabulary',
|
|
122
|
+
lang,
|
|
123
|
+
category: 'vocabulary',
|
|
124
|
+
severity: 'warning',
|
|
125
|
+
message: 'Mot ou expression très surutilisé par les modèles de langage.',
|
|
126
|
+
phrases: [
|
|
127
|
+
'incontournable',
|
|
128
|
+
'incontournables',
|
|
129
|
+
'pierre angulaire',
|
|
130
|
+
'tapisserie',
|
|
131
|
+
'riche tapisserie',
|
|
132
|
+
'en constante évolution',
|
|
133
|
+
'en perpétuelle évolution',
|
|
134
|
+
'game-changer',
|
|
135
|
+
'game changer',
|
|
136
|
+
'véritable mine d\'or',
|
|
137
|
+
'subtiles nuances',
|
|
138
|
+
'riche et varié',
|
|
139
|
+
'riche et variée',
|
|
140
|
+
["s'avère être", 'est'],
|
|
141
|
+
["s'avèrent être", 'sont'],
|
|
142
|
+
['de manière significative', 'nettement'],
|
|
143
|
+
['de façon significative', 'nettement'],
|
|
144
|
+
['un large éventail de', 'de nombreux'],
|
|
145
|
+
['une pléthore de', 'beaucoup de'],
|
|
146
|
+
['une myriade de', 'beaucoup de'],
|
|
147
|
+
['une multitude de', 'beaucoup de'],
|
|
148
|
+
],
|
|
149
|
+
}),
|
|
150
|
+
phraseRule({
|
|
151
|
+
id: 'fr/weasel-vocabulary',
|
|
152
|
+
lang,
|
|
153
|
+
category: 'vocabulary',
|
|
154
|
+
severity: 'info',
|
|
155
|
+
message: "Mot vague sur lequel s'appuient les IA. Acceptable une fois, suspect en série.",
|
|
156
|
+
phrases: [
|
|
157
|
+
'véritable',
|
|
158
|
+
'crucial',
|
|
159
|
+
'cruciale',
|
|
160
|
+
'cruciaux',
|
|
161
|
+
'au cœur de',
|
|
162
|
+
'fascinant',
|
|
163
|
+
'fascinante',
|
|
164
|
+
'holistique',
|
|
165
|
+
'synergie',
|
|
166
|
+
'levier',
|
|
167
|
+
'leviers',
|
|
168
|
+
'booster',
|
|
169
|
+
'sublimer',
|
|
170
|
+
'propulser',
|
|
171
|
+
'optimal',
|
|
172
|
+
'optimale',
|
|
173
|
+
'robuste',
|
|
174
|
+
'harmonieux',
|
|
175
|
+
'harmonieuse',
|
|
176
|
+
'dynamique',
|
|
177
|
+
'innovant',
|
|
178
|
+
'innovante',
|
|
179
|
+
'novateur',
|
|
180
|
+
'novatrice',
|
|
181
|
+
'en outre',
|
|
182
|
+
'par ailleurs',
|
|
183
|
+
'de surcroît',
|
|
184
|
+
],
|
|
185
|
+
}),
|
|
186
|
+
regexRule({
|
|
187
|
+
id: 'fr/not-x-but-y',
|
|
188
|
+
lang,
|
|
189
|
+
category: 'structure',
|
|
190
|
+
severity: 'warning',
|
|
191
|
+
message: "Construction « ce n'est pas X, c'est Y ». L'un des tics IA les plus reconnaissables.",
|
|
192
|
+
patterns: [
|
|
193
|
+
/(?<![\p{L}])[\p{L}\p{N}-]+\s+n['’](?:est|était)\s+pas\s+(?:seulement\s+|juste\s+|simplement\s+|uniquement\s+|qu['’]une?\s+)?[^.!?\n]{1,80}?[,;.:\u2014]\s*(?:c['’](?:est|était)|il\s+s['’]agit)(?![\p{L}])/giu,
|
|
194
|
+
/(?<![\p{L}])il\s+ne\s+s['’]agit\s+pas\s+(?:seulement\s+|simplement\s+|juste\s+|uniquement\s+)?d(?:e\s+|['’])[^.!?\n]{1,80}?,?\s*mais\s+(?:bien\s+|aussi\s+|surtout\s+)?d(?:e|['’])(?![\p{L}])/giu,
|
|
195
|
+
/(?<![\p{L}])plus\s+qu['’]une?\s+[^.!?\n]{1,60}?,\s*(?:c['’]est|un|une)(?![\p{L}])/giu,
|
|
196
|
+
],
|
|
197
|
+
}),
|
|
198
|
+
regexRule({
|
|
199
|
+
id: 'fr/que-vous-soyez',
|
|
200
|
+
lang,
|
|
201
|
+
category: 'structure',
|
|
202
|
+
severity: 'info',
|
|
203
|
+
message: "Accroche « Que vous soyez X ou Y ». Cadrage d'audience générique.",
|
|
204
|
+
patterns: [/(?<![\p{L}])que\s+vous\s+soyez(?![\p{L}])[^.!?\n]{1,80}?(?<![\p{L}])ou(?![\p{L}])/giu],
|
|
205
|
+
}),
|
|
206
|
+
];
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Small builders so that adding a rule is one line in a list.
|
|
2
|
+
// Every rule is { id, lang, category, severity, message, find(text, ctx) }.
|
|
3
|
+
// find() yields { index, length, fix?, message? }. fix === undefined or null means "no autofix".
|
|
4
|
+
|
|
5
|
+
const BEFORE = '(?<![\\p{L}\\p{N}_])';
|
|
6
|
+
const AFTER = '(?![\\p{L}\\p{N}_])';
|
|
7
|
+
|
|
8
|
+
export function escapeRegex(s) {
|
|
9
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Turns a plain phrase into a tolerant regex source:
|
|
13
|
+
// any apostrophe matches ' or the typographic one, any space matches any whitespace run.
|
|
14
|
+
export function phraseSource(phrase) {
|
|
15
|
+
return escapeRegex(phrase)
|
|
16
|
+
.replace(/['’]/g, "['’]")
|
|
17
|
+
.replace(/\s+/g, '\\s+');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isUpper(ch) {
|
|
21
|
+
return !!ch && ch !== ch.toLowerCase() && ch === ch.toUpperCase();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function matchCase(replacement, original) {
|
|
25
|
+
if (!replacement) return replacement;
|
|
26
|
+
if (isUpper(original[0])) return replacement[0].toUpperCase() + replacement.slice(1);
|
|
27
|
+
return replacement;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A rule built from a list of phrases.
|
|
32
|
+
* Each entry is either "phrase" (report only) or ["phrase", "replacement"].
|
|
33
|
+
* With drop: true, the phrase is removed and the next word is capitalized when the phrase opened a sentence.
|
|
34
|
+
*/
|
|
35
|
+
export function phraseRule({ id, lang, category, severity, message, phrases, drop = false }) {
|
|
36
|
+
const entries = phrases
|
|
37
|
+
.map((p) => (Array.isArray(p) ? { phrase: p[0], replacement: p[1] } : { phrase: p, replacement: undefined }))
|
|
38
|
+
.sort((a, b) => b.phrase.length - a.phrase.length);
|
|
39
|
+
|
|
40
|
+
const lookup = new Map(entries.map((e) => [normalize(e.phrase), e]));
|
|
41
|
+
const body = entries.map((e) => phraseSource(e.phrase)).join('|');
|
|
42
|
+
// In drop mode we also eat the trailing spaces and capture the next letter so we can re-case it.
|
|
43
|
+
const source = drop ? `${BEFORE}(${body})(?:${AFTER})([ \\t]*)(\\p{L})?` : `${BEFORE}(${body})${AFTER}`;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
id,
|
|
47
|
+
lang,
|
|
48
|
+
category,
|
|
49
|
+
severity,
|
|
50
|
+
message,
|
|
51
|
+
*find(text) {
|
|
52
|
+
const re = new RegExp(source, 'giu');
|
|
53
|
+
for (const m of text.matchAll(re)) {
|
|
54
|
+
const matched = m[1];
|
|
55
|
+
const entry = lookup.get(normalize(matched));
|
|
56
|
+
if (drop) {
|
|
57
|
+
const next = m[3] || '';
|
|
58
|
+
const opensSentence = isUpper(matched[0]);
|
|
59
|
+
const fixedNext = opensSentence ? next.toUpperCase() : next;
|
|
60
|
+
// Report only the phrase, but let the fix also re-case the letter that follows it.
|
|
61
|
+
yield { index: m.index, length: matched.length, fixLength: m[0].length, fix: fixedNext, fixLabel: '(remove)', quote: matched };
|
|
62
|
+
} else {
|
|
63
|
+
const fix = entry && entry.replacement !== undefined ? matchCase(entry.replacement, matched) : undefined;
|
|
64
|
+
yield { index: m.index, length: matched.length, fix, quote: matched };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A rule built from one or more regexes. fix can be a string or a function(match, text) returning a string or null. */
|
|
72
|
+
export function regexRule({ id, lang, category, severity, message, patterns, fix }) {
|
|
73
|
+
return {
|
|
74
|
+
id,
|
|
75
|
+
lang,
|
|
76
|
+
category,
|
|
77
|
+
severity,
|
|
78
|
+
message,
|
|
79
|
+
*find(text, ctx) {
|
|
80
|
+
for (const pattern of patterns) {
|
|
81
|
+
const re = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g');
|
|
82
|
+
for (const m of text.matchAll(re)) {
|
|
83
|
+
if (m[0].length === 0) continue;
|
|
84
|
+
const f = typeof fix === 'function' ? fix(m, text, ctx) : fix;
|
|
85
|
+
yield { index: m.index, length: m[0].length, fix: f ?? undefined, quote: m[0].trim() };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalize(s) {
|
|
93
|
+
return s.toLowerCase().replace(/’/g, "'").replace(/\s+/g, ' ');
|
|
94
|
+
}
|