slopless 0.2.26 → 0.2.28
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/dist/adapters/textlint/units.js +7 -3
- package/dist/presets/everything.js +1 -0
- package/dist/registries/words.js +2 -0
- package/dist/rules/semantic-thinness/patterns/something-shifted.json +125 -7
- package/dist/rules/syntactic-patterns/lead-ins/generic-signposting.js +2 -1
- package/dist/rules/syntactic-patterns/lead-ins/private/discourse-evaluation.js +81 -2
- package/dist/rules/words/actually-overuse.js +8 -26
- package/dist/rules/words/private/token-density-rule.js +31 -0
- package/dist/rules/words/quietly-overuse.js +22 -0
- package/dist/shared/text/document.js +39 -4
- package/dist/shared/text/sections.js +4 -18
- package/dist/shared/text/traverse.js +48 -3
- package/package.json +3 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { normalizeForMatch } from "../../shared/text/normalize.js";
|
|
2
2
|
import { allParagraphSentences, allParagraphs, sectionFirstSentences, sectionLastSentences } from "../../shared/text/sections.js";
|
|
3
|
-
import { documentText } from "../../shared/text/document.js";
|
|
3
|
+
import { documentSourceText, documentText } from "../../shared/text/document.js";
|
|
4
4
|
import { sourceText } from "../../shared/text/traverse.js";
|
|
5
5
|
function isParentNode(node) {
|
|
6
6
|
return "children" in node;
|
|
@@ -44,14 +44,18 @@ function sentenceUnit(id, item) {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
export function documentUnit(document) {
|
|
47
|
-
const
|
|
47
|
+
const source = documentSourceText(document);
|
|
48
|
+
const { text } = source;
|
|
48
49
|
return {
|
|
49
50
|
id: "document:0",
|
|
50
51
|
kind: "document",
|
|
51
52
|
node: document,
|
|
52
53
|
normalizedText: normalizeForMatch(text),
|
|
53
54
|
range: { end: text.length, start: 0 },
|
|
54
|
-
sourceRangeFor: (range) =>
|
|
55
|
+
sourceRangeFor: (range) => ({
|
|
56
|
+
end: source.originalEndFor(range.end),
|
|
57
|
+
start: source.originalStartFor(range.start)
|
|
58
|
+
}),
|
|
55
59
|
text
|
|
56
60
|
};
|
|
57
61
|
}
|
package/dist/registries/words.js
CHANGED
|
@@ -3,6 +3,7 @@ import hedgeStacking from "../rules/words/hedge-stacking.js";
|
|
|
3
3
|
import llmVocabularyDensity from "../rules/words/llm-vocabulary-density.js";
|
|
4
4
|
import llmVocabulary from "../rules/words/llm-vocabulary.js";
|
|
5
5
|
import prohibitedWords from "../rules/words/prohibited-words.js";
|
|
6
|
+
import quietlyOveruse from "../rules/words/quietly-overuse.js";
|
|
6
7
|
import simplicity from "../rules/words/simplicity.js";
|
|
7
8
|
export const wordRules = {
|
|
8
9
|
"actually-overuse": actuallyOveruse,
|
|
@@ -10,6 +11,7 @@ export const wordRules = {
|
|
|
10
11
|
"llm-vocabulary-density": llmVocabularyDensity,
|
|
11
12
|
"llm-vocabulary": llmVocabulary,
|
|
12
13
|
"prohibited-words": prohibitedWords,
|
|
14
|
+
"quietly-overuse": quietlyOveruse,
|
|
13
15
|
simplicity
|
|
14
16
|
};
|
|
15
17
|
//# sourceMappingURL=words.js.map
|
|
@@ -2,14 +2,36 @@
|
|
|
2
2
|
"id": "something-shifted",
|
|
3
3
|
"class": "vague-change",
|
|
4
4
|
"purpose": "Catch vague change declarations that say something shifted, changed, moved, or crossed a line without naming the concrete change.",
|
|
5
|
-
"matchMode": "
|
|
5
|
+
"matchMode": "full",
|
|
6
6
|
"maxTokens": 80,
|
|
7
7
|
"templates": [
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
{
|
|
9
|
+
"text": "{genericSubject} {changeVerb}.",
|
|
10
|
+
"matchMode": "contains"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"text": "{genericSubject} had {changeVerb}.",
|
|
14
|
+
"matchMode": "contains"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"text": "{genericSubject} in {abstractContainer} had {changeVerb}.",
|
|
18
|
+
"matchMode": "contains"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"text": "{concreteSubject} did not {changeVerb}, but {genericSubject} in it did.",
|
|
22
|
+
"matchMode": "contains"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"text": "{genericSubject} {changeVerb} when {genericSubject} {abstractAction}.",
|
|
26
|
+
"matchMode": "contains"
|
|
27
|
+
},
|
|
28
|
+
"{diffuseSubject} {weakMannerAdverb} {finiteChange}.",
|
|
29
|
+
"{diffuseSubject} {progressiveAuxiliary} {weakMannerAdverb} {progressiveChange}.",
|
|
30
|
+
"{diffuseSubject} {perfectAuxiliary} {weakMannerAdverb} {pastChange}.",
|
|
31
|
+
"{diffuseSubject} {finiteChange} {weakMannerAdverb}.",
|
|
32
|
+
"{deicticSubject} {weakMannerAdverb} {emptyChangeObject}.",
|
|
33
|
+
"{shiftDeterminer} {shiftModifier} shift {emptyShiftPredicate}.",
|
|
34
|
+
"{deicticSubject} {signalVerb} {shiftDeterminer} {shiftModifier} shift."
|
|
13
35
|
],
|
|
14
36
|
"slots": {
|
|
15
37
|
"genericSubject": [
|
|
@@ -45,7 +67,103 @@
|
|
|
45
67
|
"chose differently",
|
|
46
68
|
"named the truth",
|
|
47
69
|
"let go"
|
|
48
|
-
]
|
|
70
|
+
],
|
|
71
|
+
"diffuseSubject": [
|
|
72
|
+
"the market",
|
|
73
|
+
"the industry",
|
|
74
|
+
"search",
|
|
75
|
+
"the web",
|
|
76
|
+
"the landscape",
|
|
77
|
+
"the platform",
|
|
78
|
+
"the system",
|
|
79
|
+
"the process",
|
|
80
|
+
"the conversation",
|
|
81
|
+
"the work",
|
|
82
|
+
"the strategy",
|
|
83
|
+
"the model",
|
|
84
|
+
"the product",
|
|
85
|
+
"the category",
|
|
86
|
+
"the space"
|
|
87
|
+
],
|
|
88
|
+
"progressiveAuxiliary": ["is", "was", "has been", "had been"],
|
|
89
|
+
"perfectAuxiliary": ["has", "had"],
|
|
90
|
+
"weakMannerAdverb": [
|
|
91
|
+
"quietly",
|
|
92
|
+
"silently",
|
|
93
|
+
"subtly",
|
|
94
|
+
"gradually",
|
|
95
|
+
"steadily",
|
|
96
|
+
"almost invisibly",
|
|
97
|
+
"under the surface"
|
|
98
|
+
],
|
|
99
|
+
"finiteChange": [
|
|
100
|
+
"changes",
|
|
101
|
+
"shifts",
|
|
102
|
+
"evolves",
|
|
103
|
+
"transforms",
|
|
104
|
+
"reshapes",
|
|
105
|
+
"redefines",
|
|
106
|
+
"moves",
|
|
107
|
+
"changed",
|
|
108
|
+
"shifted",
|
|
109
|
+
"evolved",
|
|
110
|
+
"transformed",
|
|
111
|
+
"reshaped",
|
|
112
|
+
"redefined",
|
|
113
|
+
"moved"
|
|
114
|
+
],
|
|
115
|
+
"pastChange": [
|
|
116
|
+
"changed",
|
|
117
|
+
"shifted",
|
|
118
|
+
"evolved",
|
|
119
|
+
"transformed",
|
|
120
|
+
"reshaped",
|
|
121
|
+
"redefined",
|
|
122
|
+
"moved"
|
|
123
|
+
],
|
|
124
|
+
"progressiveChange": [
|
|
125
|
+
"changing",
|
|
126
|
+
"shifting",
|
|
127
|
+
"evolving",
|
|
128
|
+
"transforming",
|
|
129
|
+
"reshaping",
|
|
130
|
+
"redefining",
|
|
131
|
+
"moving"
|
|
132
|
+
],
|
|
133
|
+
"deicticSubject": ["this", "that", "it"],
|
|
134
|
+
"emptyChangeObject": [
|
|
135
|
+
"changes everything",
|
|
136
|
+
"changes the game",
|
|
137
|
+
"changes the equation",
|
|
138
|
+
"reshapes everything",
|
|
139
|
+
"redefines everything"
|
|
140
|
+
],
|
|
141
|
+
"shiftDeterminer": ["a", "the", "this", "that"],
|
|
142
|
+
"shiftModifier": [
|
|
143
|
+
"quiet",
|
|
144
|
+
"subtle",
|
|
145
|
+
"broader",
|
|
146
|
+
"meaningful",
|
|
147
|
+
"important",
|
|
148
|
+
"major",
|
|
149
|
+
"fundamental",
|
|
150
|
+
"seismic",
|
|
151
|
+
"structural",
|
|
152
|
+
"profound",
|
|
153
|
+
"significant",
|
|
154
|
+
"notable"
|
|
155
|
+
],
|
|
156
|
+
"emptyShiftPredicate": [
|
|
157
|
+
"is underway",
|
|
158
|
+
"is happening",
|
|
159
|
+
"has begun",
|
|
160
|
+
"is already here",
|
|
161
|
+
"is easy to miss",
|
|
162
|
+
"is hard to ignore",
|
|
163
|
+
"matters",
|
|
164
|
+
"changes everything"
|
|
165
|
+
],
|
|
166
|
+
"signalVerb": ["marks", "signals", "represents", "reflects"]
|
|
49
167
|
},
|
|
50
168
|
"rejectIf": [
|
|
51
169
|
"sentence names what changed",
|
|
@@ -159,7 +159,7 @@ function matchWhatFrame(words) {
|
|
|
159
159
|
function matchAbstractFrame(text) {
|
|
160
160
|
const words = tokens(text);
|
|
161
161
|
return (matchRelativeDiscourseFrame(text, words) ??
|
|
162
|
-
matchExpandedDiscourseFrame(words) ??
|
|
162
|
+
matchExpandedDiscourseFrame(text, words) ??
|
|
163
163
|
matchModifiedAbstractFrame(words) ??
|
|
164
164
|
matchDiscourseEvaluationFrame(words) ??
|
|
165
165
|
matchPointIsToFrame(words) ??
|
|
@@ -221,6 +221,7 @@ function matchSignposting(sentence) {
|
|
|
221
221
|
const generatedFormula = matchGeneratedFormula(stripped);
|
|
222
222
|
if (abstract !== undefined &&
|
|
223
223
|
(abstract === "what-matters-is" ||
|
|
224
|
+
abstract.startsWith("deictic-evaluative-") ||
|
|
224
225
|
abstract.startsWith("relative-") ||
|
|
225
226
|
!concreteImplementation)) {
|
|
226
227
|
return { kind: "abstract-evaluation-frame", signal: abstract };
|
|
@@ -5,6 +5,53 @@ const DISCOURSE_WORK_TAILS = [
|
|
|
5
5
|
["load", "bearing"]
|
|
6
6
|
];
|
|
7
7
|
const DETERMINERS = new Set(["a", "an", "the", "this", "that"]);
|
|
8
|
+
const DEICTIC_OPENERS = new Set(["here", "this", "that"]);
|
|
9
|
+
const DEICTIC_CONTRACTIONS = new Map([
|
|
10
|
+
["here's", "here"],
|
|
11
|
+
["this's", "this"],
|
|
12
|
+
["that's", "that"]
|
|
13
|
+
]);
|
|
14
|
+
const DEICTIC_EVALUATIVE_ADJECTIVES = new Set([
|
|
15
|
+
"best",
|
|
16
|
+
"better",
|
|
17
|
+
"biggest",
|
|
18
|
+
"central",
|
|
19
|
+
"core",
|
|
20
|
+
"crucial",
|
|
21
|
+
"funny",
|
|
22
|
+
"good",
|
|
23
|
+
"great",
|
|
24
|
+
"hard",
|
|
25
|
+
"important",
|
|
26
|
+
"interesting",
|
|
27
|
+
"key",
|
|
28
|
+
"main",
|
|
29
|
+
"neat",
|
|
30
|
+
"nice",
|
|
31
|
+
"odd",
|
|
32
|
+
"obvious",
|
|
33
|
+
"remarkable",
|
|
34
|
+
"strange",
|
|
35
|
+
"surprising",
|
|
36
|
+
"tricky",
|
|
37
|
+
"useful",
|
|
38
|
+
"weird",
|
|
39
|
+
"wild"
|
|
40
|
+
]);
|
|
41
|
+
const DEICTIC_DISCOURSE_NOUNS = new Set([
|
|
42
|
+
"angle",
|
|
43
|
+
"aspect",
|
|
44
|
+
"bit",
|
|
45
|
+
"catch",
|
|
46
|
+
"detail",
|
|
47
|
+
"element",
|
|
48
|
+
"idea",
|
|
49
|
+
"part",
|
|
50
|
+
"piece",
|
|
51
|
+
"point",
|
|
52
|
+
"thing",
|
|
53
|
+
"twist"
|
|
54
|
+
]);
|
|
8
55
|
const FRAME_ADJECTIVES = new Set([
|
|
9
56
|
"basic",
|
|
10
57
|
"best",
|
|
@@ -202,13 +249,45 @@ function matchEvaluativeFrame(words) {
|
|
|
202
249
|
? `the-${adjective}-${noun}-${verb}`
|
|
203
250
|
: undefined;
|
|
204
251
|
}
|
|
252
|
+
function matchDeicticEvaluativeFrame(text, words) {
|
|
253
|
+
const contractedOpener = DEICTIC_CONTRACTIONS.get(words[0] ?? "");
|
|
254
|
+
const opener = contractedOpener ?? words[0];
|
|
255
|
+
const determinerIndex = contractedOpener === undefined ? 2 : 1;
|
|
256
|
+
if (opener === undefined ||
|
|
257
|
+
!DEICTIC_OPENERS.has(opener) ||
|
|
258
|
+
(contractedOpener === undefined &&
|
|
259
|
+
!["is", "was"].includes(words[1] ?? "")) ||
|
|
260
|
+
!["a", "an", "the"].includes(words[determinerIndex] ?? "")) {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
const adjective = words[determinerIndex + 1];
|
|
264
|
+
const noun = words[determinerIndex + 2];
|
|
265
|
+
const frame = adjective !== undefined && noun !== undefined
|
|
266
|
+
? words.slice(0, determinerIndex + 3).join(" ")
|
|
267
|
+
: undefined;
|
|
268
|
+
const boundary = frame === undefined ? undefined : text.at(frame.length);
|
|
269
|
+
return adjective !== undefined &&
|
|
270
|
+
noun !== undefined &&
|
|
271
|
+
(boundary === undefined ||
|
|
272
|
+
boundary === "." ||
|
|
273
|
+
boundary === "!" ||
|
|
274
|
+
boundary === "?" ||
|
|
275
|
+
boundary === ":" ||
|
|
276
|
+
boundary === ";" ||
|
|
277
|
+
boundary === ",") &&
|
|
278
|
+
DEICTIC_EVALUATIVE_ADJECTIVES.has(adjective) &&
|
|
279
|
+
DEICTIC_DISCOURSE_NOUNS.has(noun)
|
|
280
|
+
? `deictic-evaluative-${opener}-${adjective}-${noun}`
|
|
281
|
+
: undefined;
|
|
282
|
+
}
|
|
205
283
|
export function isAbstractAuditFrame(words) {
|
|
206
284
|
return words[0] === "the" && frameNounIndex(words) > 0
|
|
207
285
|
? words[frameNounIndex(words)] === "audit"
|
|
208
286
|
: false;
|
|
209
287
|
}
|
|
210
|
-
export function matchExpandedDiscourseFrame(words) {
|
|
211
|
-
return (
|
|
288
|
+
export function matchExpandedDiscourseFrame(text, words) {
|
|
289
|
+
return (matchDeicticEvaluativeFrame(text, words) ??
|
|
290
|
+
matchWorthAttentionFrame(words) ??
|
|
212
291
|
matchVagueFrameLocation(words) ??
|
|
213
292
|
matchEvaluativeFrame(words));
|
|
214
293
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { defineTextlintRule } from "../../adapters/textlint/rule.js";
|
|
2
|
-
import { documentUnit } from "../../adapters/textlint/units.js";
|
|
3
1
|
import { ERROR_SEVERITY } from "../../reporting/density.js";
|
|
4
|
-
import {
|
|
2
|
+
import { defineExactTokenDensityRule } from "./private/token-density-rule.js";
|
|
5
3
|
// The rule only detects occurrences of "actually" - one detection per use, no counting,
|
|
6
4
|
// rate, or severity. The density-rate report policy below lets the reporter judge the
|
|
7
5
|
// occurrences against the document word count and decide none / warning / error.
|
|
@@ -9,20 +7,8 @@ import { wordTokens } from "../../shared/text/tokens.js";
|
|
|
9
7
|
// density, which is the reporter's concern, not the detector's. See src/reporting/density.ts.
|
|
10
8
|
const TARGET = "actually";
|
|
11
9
|
const RULE_ID = "words:actually-overuse";
|
|
12
|
-
const rule =
|
|
13
|
-
|
|
14
|
-
detect: ({ units }) => units.flatMap((unit) => wordTokens(unit.text)
|
|
15
|
-
.filter((token) => token.normalized === TARGET)
|
|
16
|
-
.map((token) => ({
|
|
17
|
-
evidence: TARGET,
|
|
18
|
-
label: TARGET,
|
|
19
|
-
range: { end: token.end, start: token.start },
|
|
20
|
-
ruleId: RULE_ID,
|
|
21
|
-
unitId: unit.id
|
|
22
|
-
}))),
|
|
23
|
-
family: "words",
|
|
24
|
-
id: RULE_ID
|
|
25
|
-
},
|
|
10
|
+
const rule = defineExactTokenDensityRule({
|
|
11
|
+
errorPerUnit: 2,
|
|
26
12
|
formatMessage: (report) => {
|
|
27
13
|
const count = report.metric?.["count"] ?? report.detections.length;
|
|
28
14
|
const perUnit = report.metric?.["perUnit"] ?? 0;
|
|
@@ -31,15 +17,11 @@ const rule = defineTextlintRule({
|
|
|
31
17
|
: "above the 1-per-1,000-word warning threshold";
|
|
32
18
|
return `"actually" used ${count} times (${perUnit} per 1,000 words), ${threshold}. Cut the filler uses; keep at most about one per 1,000 words.`;
|
|
33
19
|
},
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
warningPerUnit: 1,
|
|
40
|
-
wordsPerUnit: 1000
|
|
41
|
-
},
|
|
42
|
-
units: (document) => [documentUnit(document)]
|
|
20
|
+
minimumOccurrences: 2,
|
|
21
|
+
ruleId: RULE_ID,
|
|
22
|
+
target: TARGET,
|
|
23
|
+
warningPerUnit: 1,
|
|
24
|
+
wordsPerUnit: 1000
|
|
43
25
|
});
|
|
44
26
|
export default rule;
|
|
45
27
|
//# sourceMappingURL=actually-overuse.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineTextlintRule } from "../../../adapters/textlint/rule.js";
|
|
2
|
+
import { documentUnit } from "../../../adapters/textlint/units.js";
|
|
3
|
+
import { wordTokens } from "../../../shared/text/tokens.js";
|
|
4
|
+
export function defineExactTokenDensityRule(config) {
|
|
5
|
+
return defineTextlintRule({
|
|
6
|
+
detector: {
|
|
7
|
+
detect: ({ units }) => units.flatMap((unit) => wordTokens(unit.text)
|
|
8
|
+
.filter((token) => token.normalized === config.target)
|
|
9
|
+
.map((token) => ({
|
|
10
|
+
evidence: config.target,
|
|
11
|
+
label: config.target,
|
|
12
|
+
range: { end: token.end, start: token.start },
|
|
13
|
+
ruleId: config.ruleId,
|
|
14
|
+
unitId: unit.id
|
|
15
|
+
}))),
|
|
16
|
+
family: "words",
|
|
17
|
+
id: config.ruleId
|
|
18
|
+
},
|
|
19
|
+
formatMessage: config.formatMessage,
|
|
20
|
+
reportPolicy: {
|
|
21
|
+
errorPerUnit: config.errorPerUnit,
|
|
22
|
+
kind: "density-rate",
|
|
23
|
+
minimumOccurrences: config.minimumOccurrences,
|
|
24
|
+
scope: "document",
|
|
25
|
+
warningPerUnit: config.warningPerUnit,
|
|
26
|
+
wordsPerUnit: config.wordsPerUnit
|
|
27
|
+
},
|
|
28
|
+
units: (document) => [documentUnit(document)]
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=token-density-rule.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ERROR_SEVERITY } from "../../reporting/density.js";
|
|
2
|
+
import { defineExactTokenDensityRule } from "./private/token-density-rule.js";
|
|
3
|
+
const TARGET = "quietly";
|
|
4
|
+
const RULE_ID = "words:quietly-overuse";
|
|
5
|
+
const rule = defineExactTokenDensityRule({
|
|
6
|
+
errorPerUnit: 2,
|
|
7
|
+
formatMessage: (report) => {
|
|
8
|
+
const count = report.metric?.["count"] ?? report.detections.length;
|
|
9
|
+
const perUnit = report.metric?.["perUnit"] ?? 0;
|
|
10
|
+
const threshold = report.severity === ERROR_SEVERITY
|
|
11
|
+
? "above the 2-per-1,000-word error threshold"
|
|
12
|
+
: "above the 1-per-1,000-word warning threshold";
|
|
13
|
+
return `"quietly" used ${count} times (${perUnit} per 1,000 words), ${threshold}. Cut the filler uses; keep at most about one per 1,000 words.`;
|
|
14
|
+
},
|
|
15
|
+
minimumOccurrences: 4,
|
|
16
|
+
ruleId: RULE_ID,
|
|
17
|
+
target: TARGET,
|
|
18
|
+
warningPerUnit: 1,
|
|
19
|
+
wordsPerUnit: 1000
|
|
20
|
+
});
|
|
21
|
+
export default rule;
|
|
22
|
+
//# sourceMappingURL=quietly-overuse.js.map
|
|
@@ -1,8 +1,43 @@
|
|
|
1
1
|
import { allParagraphs } from "./sections.js";
|
|
2
|
+
function trimStartLength(text) {
|
|
3
|
+
return text.length - text.trimStart().length;
|
|
4
|
+
}
|
|
5
|
+
export function documentSourceText(document) {
|
|
6
|
+
const paragraphs = [];
|
|
7
|
+
const textParts = [];
|
|
8
|
+
let outputStart = 0;
|
|
9
|
+
for (const paragraph of allParagraphs(document)) {
|
|
10
|
+
const text = paragraph.text.trim();
|
|
11
|
+
if (text.length === 0) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
paragraphs.push({
|
|
15
|
+
outputEnd: outputStart + text.length,
|
|
16
|
+
outputStart,
|
|
17
|
+
paragraph,
|
|
18
|
+
trimStart: trimStartLength(paragraph.text)
|
|
19
|
+
});
|
|
20
|
+
textParts.push(text);
|
|
21
|
+
outputStart += text.length + 2;
|
|
22
|
+
}
|
|
23
|
+
const originalOffsetFor = (offset, isEnd) => {
|
|
24
|
+
const mapped = paragraphs.find((paragraph) => offset >= paragraph.outputStart && offset <= paragraph.outputEnd);
|
|
25
|
+
if (mapped === undefined) {
|
|
26
|
+
return offset;
|
|
27
|
+
}
|
|
28
|
+
const paragraphOffset = mapped.trimStart + offset - mapped.outputStart;
|
|
29
|
+
const localOffset = isEnd
|
|
30
|
+
? mapped.paragraph.source.originalEndFor(paragraphOffset)
|
|
31
|
+
: mapped.paragraph.source.originalStartFor(paragraphOffset);
|
|
32
|
+
return mapped.paragraph.paragraph.range[0] + localOffset;
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
originalEndFor: (end) => originalOffsetFor(end, true),
|
|
36
|
+
originalStartFor: (start) => originalOffsetFor(start, false),
|
|
37
|
+
text: textParts.join("\n\n")
|
|
38
|
+
};
|
|
39
|
+
}
|
|
2
40
|
export function documentText(document) {
|
|
3
|
-
return
|
|
4
|
-
.map((paragraph) => paragraph.text.trim())
|
|
5
|
-
.filter((text) => text.length > 0)
|
|
6
|
-
.join("\n\n");
|
|
41
|
+
return documentSourceText(document).text;
|
|
7
42
|
}
|
|
8
43
|
//# sourceMappingURL=document.js.map
|
|
@@ -1,26 +1,11 @@
|
|
|
1
1
|
import { splitSentences } from "./sentences.js";
|
|
2
|
-
import { sourceText } from "./traverse.js";
|
|
2
|
+
import { proseSourceText, sourceText } from "./traverse.js";
|
|
3
3
|
function isParagraphNode(node) {
|
|
4
4
|
return node.type === "Paragraph";
|
|
5
5
|
}
|
|
6
6
|
function isParentNode(node) {
|
|
7
7
|
return "children" in node;
|
|
8
8
|
}
|
|
9
|
-
function hasStringValue(node) {
|
|
10
|
-
return "value" in node && typeof node.value === "string";
|
|
11
|
-
}
|
|
12
|
-
function plainText(node) {
|
|
13
|
-
if (hasStringValue(node)) {
|
|
14
|
-
return node.value;
|
|
15
|
-
}
|
|
16
|
-
if (node.type === "Break") {
|
|
17
|
-
return " ";
|
|
18
|
-
}
|
|
19
|
-
if (!isParentNode(node)) {
|
|
20
|
-
return "";
|
|
21
|
-
}
|
|
22
|
-
return node.children.map((child) => plainText(child)).join("");
|
|
23
|
-
}
|
|
24
9
|
function collectParagraphs(node, paragraphs) {
|
|
25
10
|
if (isParagraphNode(node)) {
|
|
26
11
|
paragraphs.push(node);
|
|
@@ -79,10 +64,11 @@ export function allParagraphs(document) {
|
|
|
79
64
|
const paragraphs = [];
|
|
80
65
|
for (const section of documentSections(document)) {
|
|
81
66
|
for (const paragraph of sectionParagraphs(section)) {
|
|
67
|
+
const source = proseSourceText(paragraph);
|
|
82
68
|
paragraphs.push({
|
|
83
69
|
paragraph,
|
|
84
|
-
source
|
|
85
|
-
text:
|
|
70
|
+
source,
|
|
71
|
+
text: source.text
|
|
86
72
|
});
|
|
87
73
|
}
|
|
88
74
|
}
|
|
@@ -1,13 +1,58 @@
|
|
|
1
1
|
import { StringSource } from "textlint-util-to-string";
|
|
2
|
-
|
|
2
|
+
function isParentNode(node) {
|
|
3
|
+
return "children" in node;
|
|
4
|
+
}
|
|
5
|
+
function isHtmlNode(node) {
|
|
6
|
+
return (node.type === "Html" && "value" in node && typeof node.value === "string");
|
|
7
|
+
}
|
|
8
|
+
function normalizeNode(node, includeImageAlt) {
|
|
9
|
+
if (node.type === "Break") {
|
|
10
|
+
return {
|
|
11
|
+
...node,
|
|
12
|
+
type: "Str",
|
|
13
|
+
value: " "
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
if ((node.type === "Image" || node.type === "ImageReference") &&
|
|
17
|
+
!includeImageAlt) {
|
|
18
|
+
return {
|
|
19
|
+
...node,
|
|
20
|
+
type: "Str",
|
|
21
|
+
value: ""
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (isHtmlNode(node) && !includeImageAlt) {
|
|
25
|
+
return {
|
|
26
|
+
...node,
|
|
27
|
+
type: "Str",
|
|
28
|
+
value: node.value
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (!isParentNode(node)) {
|
|
32
|
+
return node;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
...node,
|
|
36
|
+
// type-coverage:ignore-next-line
|
|
37
|
+
children: node.children.map((child) => normalizeNode(child, includeImageAlt))
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function mappedSourceText(node, includeImageAlt) {
|
|
3
41
|
// textlint-util-to-string has not updated its public type for textlint 15.7's
|
|
4
42
|
// readonly children, but it only reads the node at runtime.
|
|
5
|
-
const source = new StringSource(
|
|
6
|
-
|
|
43
|
+
const source = new StringSource(
|
|
44
|
+
// type-coverage:ignore-next-line
|
|
45
|
+
normalizeNode(node, includeImageAlt));
|
|
7
46
|
return {
|
|
8
47
|
originalEndFor: (end) => source.originalIndexFromIndex(end, true) ?? end,
|
|
9
48
|
originalStartFor: (start) => source.originalIndexFromIndex(start) ?? start,
|
|
10
49
|
text: source.toString()
|
|
11
50
|
};
|
|
12
51
|
}
|
|
52
|
+
export function proseSourceText(node) {
|
|
53
|
+
return mappedSourceText(node, false);
|
|
54
|
+
}
|
|
55
|
+
export function sourceText(node) {
|
|
56
|
+
return mappedSourceText(node, true);
|
|
57
|
+
}
|
|
13
58
|
//# sourceMappingURL=traverse.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slopless",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.28",
|
|
4
4
|
"description": "Deterministic textlint rules and CLI for catching prose slop in English Markdown.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"textlint",
|
|
@@ -111,7 +111,8 @@
|
|
|
111
111
|
"./rules/words/llm-vocabulary-density": "./dist/rules/words/llm-vocabulary-density.js",
|
|
112
112
|
"./rules/words/llm-vocabulary": "./dist/rules/words/llm-vocabulary.js",
|
|
113
113
|
"./rules/words/simplicity": "./dist/rules/words/simplicity.js",
|
|
114
|
-
"./rules/words/prohibited-words": "./dist/rules/words/prohibited-words.js"
|
|
114
|
+
"./rules/words/prohibited-words": "./dist/rules/words/prohibited-words.js",
|
|
115
|
+
"./rules/words/quietly-overuse": "./dist/rules/words/quietly-overuse.js"
|
|
115
116
|
},
|
|
116
117
|
"dependencies": {
|
|
117
118
|
"@textlint/ast-node-types": "15.7.1",
|