slopless 0.2.22 → 0.2.24

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,285 +1,7 @@
1
- import { hasConcreteInventorySubjects } from "../../../shared/matchers/concrete-evidence.js";
2
- import { splitSentences } from "../../../shared/text/sentences.js";
3
- import { splitWhitespace } from "../../../shared/text/whitespace.js";
4
1
  import { oneToOneRule } from "../../private/textlint-rule-builders.js";
5
- const MAX_FRAGMENT_WORDS = 6;
6
- const MAX_PAYOFF_WORDS = 28;
7
- const SUBJECT_WORDS = new Set([
8
- "i",
9
- "you",
10
- "we",
11
- "they",
12
- "he",
13
- "she",
14
- "it",
15
- "this",
16
- "that",
17
- "these",
18
- "those"
19
- ]);
20
- const OBJECT_WORDS = new Set([
21
- "me",
22
- "him",
23
- "her",
24
- "us",
25
- "them",
26
- "it",
27
- "the",
28
- "a",
29
- "an",
30
- "my",
31
- "your",
32
- "our",
33
- "their"
34
- ]);
35
- const FINITE_VERBS = new Set([
36
- "is",
37
- "are",
38
- "was",
39
- "were",
40
- "am",
41
- "be",
42
- "been",
43
- "being",
44
- "have",
45
- "has",
46
- "had",
47
- "do",
48
- "does",
49
- "did",
50
- "can",
51
- "could",
52
- "will",
53
- "would",
54
- "should",
55
- "may",
56
- "might",
57
- "must",
58
- "shall"
59
- ]);
60
- const FRAGMENT_LEADS = new Set([
61
- "too",
62
- "most",
63
- "more",
64
- "less",
65
- "deeply",
66
- "completely",
67
- "possibly",
68
- "probably",
69
- "maybe",
70
- "weird",
71
- "strange",
72
- "odd",
73
- "pure",
74
- "total",
75
- "incredibly"
76
- ]);
77
- const PAYOFF_STARTS = ["more like ", "most ", "then ", "instead "];
78
- const IMPERATIVE_STARTS = new Set([
79
- "feed",
80
- "leave",
81
- "notice",
82
- "stop",
83
- "start",
84
- "take",
85
- "keep",
86
- "get",
87
- "look",
88
- "think",
89
- "try",
90
- "make",
91
- "let",
92
- "give",
93
- "accept",
94
- "hold",
95
- "reduce"
96
- ]);
97
- const SIMPLE_PAST_VERBS = new Set([
98
- "ran",
99
- "went",
100
- "came",
101
- "felt",
102
- "heard",
103
- "found",
104
- "made",
105
- "took",
106
- "kept",
107
- "left",
108
- "thought",
109
- "knew",
110
- "got",
111
- "put",
112
- "said",
113
- "told",
114
- "held",
115
- "stood",
116
- "sat",
117
- "became",
118
- "wrote",
119
- "spoke",
120
- "won",
121
- "lost",
122
- "paid",
123
- "met",
124
- "read",
125
- "saw",
126
- "grew",
127
- "fell",
128
- "broke"
129
- ]);
130
- function isAlphanumeric(character) {
131
- const lower = character.toLocaleLowerCase("en");
132
- const upper = character.toLocaleUpperCase("en");
133
- return (character >= "0" && character <= "9") || lower !== upper;
134
- }
135
- function cleanWord(word) {
136
- const normalized = word.toLocaleLowerCase("en").replaceAll("\u2019", "'");
137
- let start = 0;
138
- let end = normalized.length;
139
- while (start < end &&
140
- normalized[start] !== "'" &&
141
- !isAlphanumeric(normalized[start] ?? "")) {
142
- start += 1;
143
- }
144
- while (end > start &&
145
- normalized[end - 1] !== "'" &&
146
- !isAlphanumeric(normalized[end - 1] ?? "")) {
147
- end -= 1;
148
- }
149
- return normalized.slice(start, end);
150
- }
151
- function sentenceWords(sentence) {
152
- return splitWhitespace(sentence).map(cleanWord);
153
- }
154
- function isFunctionWord(word) {
155
- return (word === "the" ||
156
- word === "a" ||
157
- word === "an" ||
158
- word === "and" ||
159
- word === "or" ||
160
- word === "but" ||
161
- word === "to" ||
162
- word === "of" ||
163
- word === "in" ||
164
- word === "on" ||
165
- word === "at" ||
166
- word === "for");
167
- }
168
- function looksLikeModifierPhrase(first, second) {
169
- return (first.endsWith("ly") ||
170
- second.endsWith("ive") ||
171
- second.endsWith("ous") ||
172
- second.endsWith("al") ||
173
- second.endsWith("ful") ||
174
- second.endsWith("less"));
175
- }
176
- function looksLikeSubjectDrop(first, second) {
177
- return (((first.endsWith("ed") && !first.endsWith("eed")) ||
178
- first.endsWith("ing")) &&
179
- !isFunctionWord(second));
180
- }
181
- function looksLikeSimpleClause(first, second) {
182
- return (!isFunctionWord(first) &&
183
- (second.endsWith("ed") || SIMPLE_PAST_VERBS.has(second)));
184
- }
185
- function looksLikeBriefImperative(first, second) {
186
- return (IMPERATIVE_STARTS.has(first) &&
187
- (OBJECT_WORDS.has(second) || second.endsWith("er") || second.endsWith("ly")));
188
- }
189
- function classifyFragment(sentence) {
190
- const words = sentenceWords(sentence.text);
191
- if (words.length < 2 || words.length > MAX_FRAGMENT_WORDS) {
192
- return undefined;
193
- }
194
- if (words.some((word) => word === "")) {
195
- return undefined;
196
- }
197
- if (words.some((word) => SUBJECT_WORDS.has(word))) {
198
- return undefined;
199
- }
200
- if (words.some((word) => FINITE_VERBS.has(word))) {
201
- return undefined;
202
- }
203
- const first = words[0];
204
- const second = words[1];
205
- if (first === undefined || second === undefined) {
206
- return undefined;
207
- }
208
- if (FRAGMENT_LEADS.has(first) || looksLikeModifierPhrase(first, second)) {
209
- return "modifier-fragment";
210
- }
211
- if (looksLikeSimpleClause(first, second) ||
212
- looksLikeBriefImperative(first, second)) {
213
- return undefined;
214
- }
215
- if (looksLikeSubjectDrop(first, second)) {
216
- return "subject-drop";
217
- }
218
- return "noun-fragment";
219
- }
220
- function looksLikePayoffSentence(sentence) {
221
- const words = sentenceWords(sentence.text);
222
- if (words.length <= 2 || words.length > MAX_PAYOFF_WORDS) {
223
- return false;
224
- }
225
- const lowered = sentence.text.toLocaleLowerCase("en");
226
- if (!PAYOFF_STARTS.some((start) => lowered.startsWith(start))) {
227
- return false;
228
- }
229
- return !words.some((word) => SUBJECT_WORDS.has(word));
230
- }
231
- function findFragmentStacks(text) {
232
- const sentences = splitSentences(text);
233
- const classifications = sentences.map(classifyFragment);
234
- const matches = [];
235
- let sentenceIndex = 0;
236
- while (sentenceIndex < sentences.length) {
237
- const firstType = classifications[sentenceIndex];
238
- const firstSentence = sentences[sentenceIndex];
239
- if (firstType === undefined || firstSentence === undefined) {
240
- sentenceIndex += 1;
241
- continue;
242
- }
243
- const runSentences = [firstSentence];
244
- const fragmentTypes = [firstType];
245
- let cursor = sentenceIndex + 1;
246
- while (cursor < sentences.length) {
247
- const sentence = sentences[cursor];
248
- const nextType = classifications[cursor];
249
- if (sentence === undefined) {
250
- break;
251
- }
252
- if (nextType !== undefined) {
253
- runSentences.push(sentence);
254
- fragmentTypes.push(nextType);
255
- cursor += 1;
256
- continue;
257
- }
258
- if (looksLikePayoffSentence(sentence)) {
259
- runSentences.push(sentence);
260
- cursor += 1;
261
- }
262
- break;
263
- }
264
- if (fragmentTypes.length >= 2 &&
265
- runSentences.length >= 3 &&
266
- !hasConcreteInventorySubjects(runSentences.map((sentence) => sentence.text))) {
267
- const last = runSentences[runSentences.length - 1];
268
- if (last !== undefined) {
269
- matches.push({
270
- end: last.end,
271
- fragmentTypes,
272
- sentences: runSentences.map((sentence) => sentence.text),
273
- start: firstSentence.start
274
- });
275
- }
276
- }
277
- sentenceIndex = Math.max(cursor, sentenceIndex + 1);
278
- }
279
- return matches;
280
- }
2
+ import { findFragmentMatches } from "./private/fragment-stack-detector.js";
281
3
  const rule = oneToOneRule({
282
- detect: (unit) => findFragmentStacks(unit.text).map((match) => ({
4
+ detect: (unit) => findFragmentMatches(unit.text).map((match) => ({
283
5
  evidence: match.sentences.join(" "),
284
6
  label: match.fragmentTypes.join(","),
285
7
  range: { start: match.start, end: match.end }
@@ -0,0 +1,316 @@
1
+ import { hasConcreteInventorySubjects } from "../../../../shared/matchers/concrete-evidence.js";
2
+ import { splitSentences } from "../../../../shared/text/sentences.js";
3
+ import { splitWhitespace } from "../../../../shared/text/whitespace.js";
4
+ const MAX_FRAGMENT_WORDS = 6;
5
+ const MAX_NO_FRAGMENT_WORDS = 5;
6
+ const MAX_PAYOFF_WORDS = 28;
7
+ const SUBJECT_WORDS = new Set([
8
+ "i",
9
+ "you",
10
+ "we",
11
+ "they",
12
+ "he",
13
+ "she",
14
+ "it",
15
+ "this",
16
+ "that",
17
+ "these",
18
+ "those"
19
+ ]);
20
+ const OBJECT_WORDS = new Set([
21
+ "me",
22
+ "him",
23
+ "her",
24
+ "us",
25
+ "them",
26
+ "it",
27
+ "the",
28
+ "a",
29
+ "an",
30
+ "my",
31
+ "your",
32
+ "our",
33
+ "their"
34
+ ]);
35
+ const FINITE_VERBS = new Set([
36
+ "is",
37
+ "are",
38
+ "was",
39
+ "were",
40
+ "am",
41
+ "be",
42
+ "been",
43
+ "being",
44
+ "have",
45
+ "has",
46
+ "had",
47
+ "do",
48
+ "does",
49
+ "did",
50
+ "can",
51
+ "could",
52
+ "will",
53
+ "would",
54
+ "should",
55
+ "may",
56
+ "might",
57
+ "must",
58
+ "shall"
59
+ ]);
60
+ const FRAGMENT_LEADS = new Set([
61
+ "too",
62
+ "most",
63
+ "more",
64
+ "less",
65
+ "deeply",
66
+ "completely",
67
+ "possibly",
68
+ "probably",
69
+ "maybe",
70
+ "weird",
71
+ "strange",
72
+ "odd",
73
+ "pure",
74
+ "total",
75
+ "incredibly"
76
+ ]);
77
+ const PAYOFF_STARTS = ["more like ", "most ", "then ", "instead "];
78
+ const IMPERATIVE_STARTS = new Set([
79
+ "feed",
80
+ "leave",
81
+ "notice",
82
+ "stop",
83
+ "start",
84
+ "take",
85
+ "keep",
86
+ "get",
87
+ "look",
88
+ "think",
89
+ "try",
90
+ "make",
91
+ "let",
92
+ "give",
93
+ "accept",
94
+ "hold",
95
+ "reduce"
96
+ ]);
97
+ const SIMPLE_PAST_VERBS = new Set([
98
+ "ran",
99
+ "went",
100
+ "came",
101
+ "felt",
102
+ "heard",
103
+ "found",
104
+ "made",
105
+ "took",
106
+ "kept",
107
+ "left",
108
+ "thought",
109
+ "knew",
110
+ "got",
111
+ "put",
112
+ "said",
113
+ "told",
114
+ "held",
115
+ "stood",
116
+ "sat",
117
+ "became",
118
+ "wrote",
119
+ "spoke",
120
+ "won",
121
+ "lost",
122
+ "paid",
123
+ "met",
124
+ "read",
125
+ "saw",
126
+ "grew",
127
+ "fell",
128
+ "broke"
129
+ ]);
130
+ function isAlphanumeric(character) {
131
+ const lower = character.toLocaleLowerCase("en");
132
+ const upper = character.toLocaleUpperCase("en");
133
+ return (character >= "0" && character <= "9") || lower !== upper;
134
+ }
135
+ function cleanWord(word) {
136
+ const normalized = word.toLocaleLowerCase("en").replaceAll("\u2019", "'");
137
+ let start = 0;
138
+ let end = normalized.length;
139
+ while (start < end &&
140
+ normalized[start] !== "'" &&
141
+ !isAlphanumeric(normalized[start] ?? "")) {
142
+ start += 1;
143
+ }
144
+ while (end > start &&
145
+ normalized[end - 1] !== "'" &&
146
+ !isAlphanumeric(normalized[end - 1] ?? "")) {
147
+ end -= 1;
148
+ }
149
+ return normalized.slice(start, end);
150
+ }
151
+ function sentenceWords(sentence) {
152
+ return splitWhitespace(sentence).map(cleanWord);
153
+ }
154
+ function isFunctionWord(word) {
155
+ return (word === "the" ||
156
+ word === "a" ||
157
+ word === "an" ||
158
+ word === "and" ||
159
+ word === "or" ||
160
+ word === "but" ||
161
+ word === "to" ||
162
+ word === "of" ||
163
+ word === "in" ||
164
+ word === "on" ||
165
+ word === "at" ||
166
+ word === "for");
167
+ }
168
+ function looksLikeModifierPhrase(first, second) {
169
+ return (first.endsWith("ly") ||
170
+ second.endsWith("ive") ||
171
+ second.endsWith("ous") ||
172
+ second.endsWith("al") ||
173
+ second.endsWith("ful") ||
174
+ second.endsWith("less"));
175
+ }
176
+ function looksLikeSubjectDrop(first, second) {
177
+ return (((first.endsWith("ed") && !first.endsWith("eed")) ||
178
+ first.endsWith("ing")) &&
179
+ !isFunctionWord(second));
180
+ }
181
+ function looksLikeSimpleClause(first, second) {
182
+ return (!isFunctionWord(first) &&
183
+ (second.endsWith("ed") || SIMPLE_PAST_VERBS.has(second)));
184
+ }
185
+ function looksLikeBriefImperative(first, second) {
186
+ return (IMPERATIVE_STARTS.has(first) &&
187
+ (OBJECT_WORDS.has(second) || second.endsWith("er") || second.endsWith("ly")));
188
+ }
189
+ function classifyFragment(sentence) {
190
+ const words = sentenceWords(sentence.text);
191
+ if (words.length < 2 || words.length > MAX_FRAGMENT_WORDS) {
192
+ return undefined;
193
+ }
194
+ if (words.some((word) => word === "")) {
195
+ return undefined;
196
+ }
197
+ if (words.some((word) => SUBJECT_WORDS.has(word))) {
198
+ return undefined;
199
+ }
200
+ if (words.some((word) => FINITE_VERBS.has(word))) {
201
+ return undefined;
202
+ }
203
+ const first = words[0];
204
+ const second = words[1];
205
+ if (first === undefined || second === undefined) {
206
+ return undefined;
207
+ }
208
+ if (FRAGMENT_LEADS.has(first) || looksLikeModifierPhrase(first, second)) {
209
+ return "modifier-fragment";
210
+ }
211
+ if (looksLikeSimpleClause(first, second) ||
212
+ looksLikeBriefImperative(first, second)) {
213
+ return undefined;
214
+ }
215
+ if (looksLikeSubjectDrop(first, second)) {
216
+ return "subject-drop";
217
+ }
218
+ return "noun-fragment";
219
+ }
220
+ function looksLikePayoffSentence(sentence) {
221
+ const words = sentenceWords(sentence.text);
222
+ if (words.length <= 2 || words.length > MAX_PAYOFF_WORDS) {
223
+ return false;
224
+ }
225
+ const lowered = sentence.text.toLocaleLowerCase("en");
226
+ if (!PAYOFF_STARTS.some((start) => lowered.startsWith(start))) {
227
+ return false;
228
+ }
229
+ return !words.some((word) => SUBJECT_WORDS.has(word));
230
+ }
231
+ function isNoFragment(sentence) {
232
+ const words = sentenceWords(sentence.text);
233
+ return (words.length >= 2 &&
234
+ words.length <= MAX_NO_FRAGMENT_WORDS &&
235
+ words[0] === "no" &&
236
+ !words.some((word) => FINITE_VERBS.has(word)) &&
237
+ classifyFragment(sentence) === "noun-fragment");
238
+ }
239
+ function findNoFragmentPairs(text) {
240
+ const sentences = splitSentences(text);
241
+ const matches = [];
242
+ for (let index = 0; index < sentences.length - 1; index += 1) {
243
+ const first = sentences[index];
244
+ const second = sentences[index + 1];
245
+ if (first === undefined || second === undefined) {
246
+ continue;
247
+ }
248
+ const pair = [first.text, second.text];
249
+ if (!isNoFragment(first) ||
250
+ !isNoFragment(second) ||
251
+ hasConcreteInventorySubjects(pair)) {
252
+ continue;
253
+ }
254
+ matches.push({
255
+ end: second.end,
256
+ fragmentTypes: ["no-fragment", "no-fragment"],
257
+ sentences: pair,
258
+ start: first.start
259
+ });
260
+ }
261
+ return matches;
262
+ }
263
+ function findFragmentStacks(text) {
264
+ const sentences = splitSentences(text);
265
+ const classifications = sentences.map(classifyFragment);
266
+ const matches = [];
267
+ let sentenceIndex = 0;
268
+ while (sentenceIndex < sentences.length) {
269
+ const firstType = classifications[sentenceIndex];
270
+ const firstSentence = sentences[sentenceIndex];
271
+ if (firstType === undefined || firstSentence === undefined) {
272
+ sentenceIndex += 1;
273
+ continue;
274
+ }
275
+ const runSentences = [firstSentence];
276
+ const fragmentTypes = [firstType];
277
+ let cursor = sentenceIndex + 1;
278
+ while (cursor < sentences.length) {
279
+ const sentence = sentences[cursor];
280
+ const nextType = classifications[cursor];
281
+ if (sentence === undefined) {
282
+ break;
283
+ }
284
+ if (nextType !== undefined) {
285
+ runSentences.push(sentence);
286
+ fragmentTypes.push(nextType);
287
+ cursor += 1;
288
+ continue;
289
+ }
290
+ if (looksLikePayoffSentence(sentence)) {
291
+ runSentences.push(sentence);
292
+ cursor += 1;
293
+ }
294
+ break;
295
+ }
296
+ if (fragmentTypes.length >= 2 &&
297
+ runSentences.length >= 3 &&
298
+ !hasConcreteInventorySubjects(runSentences.map((sentence) => sentence.text))) {
299
+ const last = runSentences[runSentences.length - 1];
300
+ if (last !== undefined) {
301
+ matches.push({
302
+ end: last.end,
303
+ fragmentTypes,
304
+ sentences: runSentences.map((sentence) => sentence.text),
305
+ start: firstSentence.start
306
+ });
307
+ }
308
+ }
309
+ sentenceIndex = Math.max(cursor, sentenceIndex + 1);
310
+ }
311
+ return matches;
312
+ }
313
+ export function findFragmentMatches(text) {
314
+ return [...findNoFragmentPairs(text), ...findFragmentStacks(text)];
315
+ }
316
+ //# sourceMappingURL=fragment-stack-detector.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopless",
3
- "version": "0.2.22",
3
+ "version": "0.2.24",
4
4
  "description": "Deterministic textlint rules and CLI for catching prose slop in English Markdown.",
5
5
  "keywords": [
6
6
  "textlint",