slopless 0.2.27 → 0.2.29
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 +2 -0
- package/dist/registries/words.js +4 -0
- package/dist/reporting/reports.js +3 -0
- package/dist/rules/private/textlint-rule-builders.js +4 -1
- package/dist/rules/semantic-thinness/patterns/something-shifted.json +125 -7
- package/dist/rules/words/actually-overuse.js +8 -26
- package/dist/rules/words/data/quietly-context.json +692 -0
- package/dist/rules/words/private/quietly-clause.js +60 -0
- package/dist/rules/words/private/quietly-context.js +163 -0
- package/dist/rules/words/private/quietly-evidence.js +68 -0
- package/dist/rules/words/private/token-density-rule.js +31 -0
- package/dist/rules/words/quietly-filler.js +21 -0
- package/dist/rules/words/quietly-overuse.js +22 -0
- package/dist/shared/text/document.js +40 -5
- package/dist/shared/text/sections.js +35 -24
- package/dist/shared/text/traverse.js +57 -3
- package/package.json +4 -2
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const BOUNDARY_WORDS = new Set([
|
|
2
|
+
"after",
|
|
3
|
+
"although",
|
|
4
|
+
"because",
|
|
5
|
+
"before",
|
|
6
|
+
"but",
|
|
7
|
+
"if",
|
|
8
|
+
"once",
|
|
9
|
+
"since",
|
|
10
|
+
"so",
|
|
11
|
+
"that",
|
|
12
|
+
"though",
|
|
13
|
+
"unless",
|
|
14
|
+
"when",
|
|
15
|
+
"whenever",
|
|
16
|
+
"where",
|
|
17
|
+
"whereas",
|
|
18
|
+
"while",
|
|
19
|
+
"which",
|
|
20
|
+
"who"
|
|
21
|
+
]);
|
|
22
|
+
function hasBoundaryPunctuation(text, left, right) {
|
|
23
|
+
const between = text.slice(left.end, right.start);
|
|
24
|
+
return (between.includes(";") ||
|
|
25
|
+
between.includes(":") ||
|
|
26
|
+
between.includes(".") ||
|
|
27
|
+
between.includes("!") ||
|
|
28
|
+
between.includes("?"));
|
|
29
|
+
}
|
|
30
|
+
export function quietlyClauseFor(text, tokens, index) {
|
|
31
|
+
let start = 0;
|
|
32
|
+
for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
|
|
33
|
+
const token = tokens[cursor];
|
|
34
|
+
const next = tokens[cursor + 1];
|
|
35
|
+
if (token === undefined ||
|
|
36
|
+
next === undefined ||
|
|
37
|
+
BOUNDARY_WORDS.has(token.normalized) ||
|
|
38
|
+
hasBoundaryPunctuation(text, token, next)) {
|
|
39
|
+
start = cursor + 1;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
let end = tokens.length;
|
|
44
|
+
for (let cursor = index + 1; cursor < tokens.length; cursor += 1) {
|
|
45
|
+
const previous = tokens[cursor - 1];
|
|
46
|
+
const token = tokens[cursor];
|
|
47
|
+
if (previous === undefined ||
|
|
48
|
+
token === undefined ||
|
|
49
|
+
BOUNDARY_WORDS.has(token.normalized) ||
|
|
50
|
+
hasBoundaryPunctuation(text, previous, token)) {
|
|
51
|
+
end = cursor;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
index: index - start,
|
|
57
|
+
tokens: tokens.slice(start, end)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=quietly-clause.js.map
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { wordTokens } from "../../../shared/text/tokens.js";
|
|
2
|
+
import quietlyContextVocabulary from "../data/quietly-context.json" with { type: "json" };
|
|
3
|
+
import { quietlyClauseFor } from "./quietly-clause.js";
|
|
4
|
+
import { hasDetachedCompanion, hasInformativeChangeEvidence, hasLinkedTechnicalEvidence, hasSpecifiedTechnicalEvidence } from "./quietly-evidence.js";
|
|
5
|
+
const TARGET = "quietly";
|
|
6
|
+
const DETACHED_EXPLANATION_WORDS = new Set(quietlyContextVocabulary.detachedExplanationWords);
|
|
7
|
+
const DETACHED_RESULT_WORDS = new Set(quietlyContextVocabulary.detachedResultWords);
|
|
8
|
+
const AUXILIARIES = new Set(quietlyContextVocabulary.auxiliaries);
|
|
9
|
+
const EVALUATIVE_WORDS = new Set(quietlyContextVocabulary.evaluativeWords);
|
|
10
|
+
const ABSTRACT_CHANGE_WORDS = new Set(quietlyContextVocabulary.abstractChangeWords);
|
|
11
|
+
const STRONG_ABSTRACT_CHANGE_WORDS = new Set(quietlyContextVocabulary.strongAbstractChangeWords);
|
|
12
|
+
const ABSTRACT_SUBJECT_WORDS = new Set(quietlyContextVocabulary.abstractSubjectWords);
|
|
13
|
+
const HIDDEN_HARM_WORDS = new Set(quietlyContextVocabulary.hiddenHarmWords);
|
|
14
|
+
const BACKGROUND_ACTION_WORDS = new Set(quietlyContextVocabulary.backgroundActionWords);
|
|
15
|
+
const BACKGROUND_SIGNIFICANCE_WORDS = new Set(quietlyContextVocabulary.backgroundSignificanceWords);
|
|
16
|
+
const TREND_ACTION_WORDS = new Set(quietlyContextVocabulary.trendActionWords);
|
|
17
|
+
const TREND_SUBJECT_WORDS = new Set(quietlyContextVocabulary.trendSubjectWords);
|
|
18
|
+
const TITLE_CONTEXT_WORDS = new Set(quietlyContextVocabulary.titleContextWords);
|
|
19
|
+
const NORMAL_MANNER_WORDS = new Set(quietlyContextVocabulary.normalMannerWords);
|
|
20
|
+
const PRIVATE_ACTION_WORDS = new Set(quietlyContextVocabulary.privateActionWords);
|
|
21
|
+
const SPECIFIED_TECHNICAL_WORDS = new Set(quietlyContextVocabulary.specifiedTechnicalWords);
|
|
22
|
+
function containsAny(words, candidates) {
|
|
23
|
+
return words.some((word) => candidates.has(word));
|
|
24
|
+
}
|
|
25
|
+
function governingWords(tokens, index) {
|
|
26
|
+
const words = [];
|
|
27
|
+
const after = nextMeaningfulWord(tokens, index);
|
|
28
|
+
if (after !== undefined) {
|
|
29
|
+
words.push(after);
|
|
30
|
+
}
|
|
31
|
+
for (let cursor = index - 1; cursor >= Math.max(0, index - 3); cursor -= 1) {
|
|
32
|
+
const word = tokens[cursor]?.normalized;
|
|
33
|
+
if (word === undefined || AUXILIARIES.has(word)) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
words.push(word);
|
|
37
|
+
}
|
|
38
|
+
return words;
|
|
39
|
+
}
|
|
40
|
+
function wordsNear(tokens, index, radius) {
|
|
41
|
+
return tokens
|
|
42
|
+
.slice(Math.max(0, index - radius), index + radius + 1)
|
|
43
|
+
.map((token) => token.normalized);
|
|
44
|
+
}
|
|
45
|
+
function matchesPairGroup(localWords, allWords, groups) {
|
|
46
|
+
return groups.some(({ actions, markers }) => actions.some((word) => localWords.includes(word)) &&
|
|
47
|
+
markers.some((word) => allWords.includes(word)));
|
|
48
|
+
}
|
|
49
|
+
function matchesRequiredPairGroup(localWords, allWords, groups) {
|
|
50
|
+
return groups.some(({ actions, markers }) => actions.some((word) => localWords.includes(word)) &&
|
|
51
|
+
markers.every((word) => allWords.includes(word)));
|
|
52
|
+
}
|
|
53
|
+
function isFirstPersonBuild(tokens, index, localWords, sentenceWords) {
|
|
54
|
+
const before = tokens.slice(0, index).map((token) => token.normalized);
|
|
55
|
+
return (containsAny(localWords, TREND_ACTION_WORDS) &&
|
|
56
|
+
containsAny(sentenceWords, TREND_SUBJECT_WORDS) &&
|
|
57
|
+
(before.includes("i") ||
|
|
58
|
+
before.includes("i've") ||
|
|
59
|
+
before.includes("my") ||
|
|
60
|
+
before.includes("we") ||
|
|
61
|
+
before.includes("we've")));
|
|
62
|
+
}
|
|
63
|
+
function nextMeaningfulWord(tokens, index) {
|
|
64
|
+
for (let cursor = index + 1; cursor < tokens.length; cursor += 1) {
|
|
65
|
+
const word = tokens[cursor]?.normalized;
|
|
66
|
+
if (word !== undefined && !AUXILIARIES.has(word)) {
|
|
67
|
+
return word;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
function isAbstractChangeContext(tokens, index, localWords, contextWords, sentenceWords, sentenceTokens, sentenceIndex) {
|
|
73
|
+
const hasChangePattern = containsAny(localWords, STRONG_ABSTRACT_CHANGE_WORDS) ||
|
|
74
|
+
(containsAny(localWords, ABSTRACT_CHANGE_WORDS) &&
|
|
75
|
+
containsAny(contextWords, ABSTRACT_SUBJECT_WORDS)) ||
|
|
76
|
+
matchesRequiredPairGroup(localWords, sentenceWords, quietlyContextVocabulary.abstractChangePairGroups);
|
|
77
|
+
return (hasChangePattern &&
|
|
78
|
+
!hasInformativeChangeEvidence(tokens, index, sentenceTokens, sentenceIndex));
|
|
79
|
+
}
|
|
80
|
+
function isNormalUse(text, tokens, index, label, sentenceTokens, sentenceIndex) {
|
|
81
|
+
const allWords = tokens.map((token) => token.normalized);
|
|
82
|
+
if (containsAny(wordsNear(tokens, index, 5), TITLE_CONTEXT_WORDS)) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
const localWords = governingWords(tokens, index);
|
|
86
|
+
if ((label === undefined && containsAny(localWords, NORMAL_MANNER_WORDS)) ||
|
|
87
|
+
containsAny(localWords, PRIVATE_ACTION_WORDS)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return (containsAny(localWords, SPECIFIED_TECHNICAL_WORDS) &&
|
|
91
|
+
(hasSpecifiedTechnicalEvidence(allWords) ||
|
|
92
|
+
hasLinkedTechnicalEvidence(text, sentenceTokens, sentenceIndex)));
|
|
93
|
+
}
|
|
94
|
+
function contextClassFor(text, tokens, index, sentenceTokens, sentenceIndex) {
|
|
95
|
+
const words = tokens.map((token) => token.normalized);
|
|
96
|
+
const sentenceWords = sentenceTokens.map((token) => token.normalized);
|
|
97
|
+
if (sentenceTokens.length === 1) {
|
|
98
|
+
return "detached-emphasis";
|
|
99
|
+
}
|
|
100
|
+
if (hasDetachedCompanion(text, tokens, index) &&
|
|
101
|
+
(tokens.length <= 8 || containsAny(words, DETACHED_EXPLANATION_WORDS))) {
|
|
102
|
+
return "detached-emphasis";
|
|
103
|
+
}
|
|
104
|
+
const localWords = governingWords(tokens, index);
|
|
105
|
+
const contextWords = wordsNear(tokens, index, 5);
|
|
106
|
+
const token = tokens[index];
|
|
107
|
+
const isTrailingResult = token !== undefined &&
|
|
108
|
+
containsAny(localWords, DETACHED_RESULT_WORDS) &&
|
|
109
|
+
(index === tokens.length - 1 ||
|
|
110
|
+
text.slice(token.end).trimStart().startsWith(":"));
|
|
111
|
+
if (isTrailingResult) {
|
|
112
|
+
return "detached-emphasis";
|
|
113
|
+
}
|
|
114
|
+
const nextWord = nextMeaningfulWord(tokens, index);
|
|
115
|
+
if (nextWord !== undefined && EVALUATIVE_WORDS.has(nextWord)) {
|
|
116
|
+
return "evaluative-intensifier";
|
|
117
|
+
}
|
|
118
|
+
if (containsAny(localWords, HIDDEN_HARM_WORDS) ||
|
|
119
|
+
matchesPairGroup(localWords, wordsNear(tokens, index, 8), quietlyContextVocabulary.hiddenHarmPairGroups)) {
|
|
120
|
+
return "hidden-harm";
|
|
121
|
+
}
|
|
122
|
+
if (isAbstractChangeContext(tokens, index, localWords, contextWords, words, sentenceTokens, sentenceIndex)) {
|
|
123
|
+
return "abstract-change";
|
|
124
|
+
}
|
|
125
|
+
if (containsAny(localWords, BACKGROUND_ACTION_WORDS) &&
|
|
126
|
+
containsAny(contextWords, BACKGROUND_SIGNIFICANCE_WORDS)) {
|
|
127
|
+
return "background-significance";
|
|
128
|
+
}
|
|
129
|
+
if (matchesRequiredPairGroup(localWords, words, quietlyContextVocabulary.backgroundPhraseGroups)) {
|
|
130
|
+
return "background-significance";
|
|
131
|
+
}
|
|
132
|
+
if ((containsAny(words, TREND_SUBJECT_WORDS) &&
|
|
133
|
+
(containsAny(localWords, TREND_ACTION_WORDS) ||
|
|
134
|
+
(index <= 2 && containsAny(words, TREND_ACTION_WORDS)))) ||
|
|
135
|
+
isFirstPersonBuild(tokens, index, localWords, sentenceWords) ||
|
|
136
|
+
matchesRequiredPairGroup(localWords, words, quietlyContextVocabulary.trendPhraseGroups)) {
|
|
137
|
+
return "unannounced-trend";
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
export function findQuietlyContextMatches(text) {
|
|
142
|
+
const tokens = wordTokens(text);
|
|
143
|
+
const matches = [];
|
|
144
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
145
|
+
const token = tokens[index];
|
|
146
|
+
if (token?.normalized !== TARGET) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const clause = quietlyClauseFor(text, tokens, index);
|
|
150
|
+
const label = contextClassFor(text, clause.tokens, clause.index, tokens, index);
|
|
151
|
+
if (label === undefined ||
|
|
152
|
+
isNormalUse(text, clause.tokens, clause.index, label, tokens, index)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
matches.push({
|
|
156
|
+
evidence: token.text,
|
|
157
|
+
label,
|
|
158
|
+
range: { end: token.end, start: token.start }
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return matches;
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=quietly-context.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import quietlyContextVocabulary from "../data/quietly-context.json" with { type: "json" };
|
|
2
|
+
const ABSTRACT_CHANGE_WORDS = new Set(quietlyContextVocabulary.abstractChangeWords);
|
|
3
|
+
const STRONG_ABSTRACT_CHANGE_WORDS = new Set(quietlyContextVocabulary.strongAbstractChangeWords);
|
|
4
|
+
const DETACHED_COMPANION_WORDS = new Set(quietlyContextVocabulary.detachedCompanionWords);
|
|
5
|
+
const TECHNICAL_EVIDENCE_WORDS = new Set(quietlyContextVocabulary.technicalEvidenceWords);
|
|
6
|
+
function startsWithDigit(word) {
|
|
7
|
+
const first = word.at(0);
|
|
8
|
+
return first !== undefined && first >= "0" && first <= "9";
|
|
9
|
+
}
|
|
10
|
+
export function hasSpecifiedTechnicalEvidence(words) {
|
|
11
|
+
const evidenceCount = words.filter((word) => TECHNICAL_EVIDENCE_WORDS.has(word)).length;
|
|
12
|
+
return (words.some(startsWithDigit) ||
|
|
13
|
+
words.includes("because") ||
|
|
14
|
+
evidenceCount >= 2);
|
|
15
|
+
}
|
|
16
|
+
export function hasLinkedTechnicalEvidence(text, tokens, index) {
|
|
17
|
+
const following = tokens.slice(index + 1);
|
|
18
|
+
const boundaryIndex = following.findIndex((token) => ["after", "because", "when"].includes(token.normalized));
|
|
19
|
+
const quietToken = tokens[index];
|
|
20
|
+
const colonIndex = quietToken === undefined ? -1 : text.indexOf(":", quietToken.end);
|
|
21
|
+
const evidence = boundaryIndex >= 0
|
|
22
|
+
? following.slice(boundaryIndex + 1)
|
|
23
|
+
: colonIndex >= 0
|
|
24
|
+
? following.filter((token) => token.start > colonIndex)
|
|
25
|
+
: [];
|
|
26
|
+
return ((following[boundaryIndex]?.normalized === "because" &&
|
|
27
|
+
evidence.length >= 3) ||
|
|
28
|
+
hasSpecifiedTechnicalEvidence(evidence.map((token) => token.normalized)));
|
|
29
|
+
}
|
|
30
|
+
export function hasDetachedCompanion(text, tokens, index) {
|
|
31
|
+
const companion = tokens
|
|
32
|
+
.slice(0, index)
|
|
33
|
+
.findLast((token) => DETACHED_COMPANION_WORDS.has(token.normalized));
|
|
34
|
+
const target = tokens[index];
|
|
35
|
+
return (companion !== undefined &&
|
|
36
|
+
target !== undefined &&
|
|
37
|
+
text.slice(companion.end, target.start).includes(","));
|
|
38
|
+
}
|
|
39
|
+
export function hasInformativeChangeEvidence(clauseTokens, clauseIndex, sentenceTokens, sentenceIndex) {
|
|
40
|
+
const clauseWords = clauseTokens.map((token) => token.normalized);
|
|
41
|
+
if (clauseWords.includes("from") && clauseWords.includes("to")) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
const actionIndices = clauseWords.flatMap((word, index) => ABSTRACT_CHANGE_WORDS.has(word) || STRONG_ABSTRACT_CHANGE_WORDS.has(word)
|
|
45
|
+
? [index]
|
|
46
|
+
: []);
|
|
47
|
+
const actionIndex = actionIndices.reduce((nearest, candidate) => nearest === undefined ||
|
|
48
|
+
Math.abs(candidate - clauseIndex) < Math.abs(nearest - clauseIndex)
|
|
49
|
+
? candidate
|
|
50
|
+
: nearest, undefined);
|
|
51
|
+
if (actionIndex !== undefined &&
|
|
52
|
+
clauseWords.slice(actionIndex + 1, actionIndex + 7).some(startsWithDigit)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (actionIndex === undefined) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const sentenceActionIndex = sentenceTokens.findIndex((token) => token.start === clauseTokens[actionIndex]?.start);
|
|
59
|
+
if (sentenceActionIndex < 0) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const explanation = sentenceTokens.slice(sentenceActionIndex + 1);
|
|
63
|
+
const boundaryIndex = explanation.findIndex((token) => token.normalized === "after" || token.normalized === "because");
|
|
64
|
+
return (sentenceActionIndex <= sentenceIndex &&
|
|
65
|
+
boundaryIndex >= 0 &&
|
|
66
|
+
explanation.length - boundaryIndex > 4);
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=quietly-evidence.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,21 @@
|
|
|
1
|
+
import { oneToOneRule } from "../private/textlint-rule-builders.js";
|
|
2
|
+
import { findQuietlyContextMatches } from "./private/quietly-context.js";
|
|
3
|
+
const RULE_ID = "words:quietly-filler";
|
|
4
|
+
const rule = oneToOneRule({
|
|
5
|
+
detect: (unit) => findQuietlyContextMatches(unit.text).map((match) => ({
|
|
6
|
+
data: { contextClass: match.label },
|
|
7
|
+
evidence: match.evidence,
|
|
8
|
+
label: match.label,
|
|
9
|
+
range: match.range
|
|
10
|
+
})),
|
|
11
|
+
family: "words",
|
|
12
|
+
formatMessage: (report) => {
|
|
13
|
+
const contextClass = report.detections[0]?.data?.["contextClass"];
|
|
14
|
+
return `"quietly" adds vague hidden significance (${contextClass}). Name the concrete change or remove the adverb.`;
|
|
15
|
+
},
|
|
16
|
+
ruleId: RULE_ID,
|
|
17
|
+
severity: 1,
|
|
18
|
+
unitKind: "sentence"
|
|
19
|
+
});
|
|
20
|
+
export default rule;
|
|
21
|
+
//# sourceMappingURL=quietly-filler.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
|
-
import {
|
|
1
|
+
import { allDocumentParagraphs } 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 allDocumentParagraphs(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,27 +1,24 @@
|
|
|
1
1
|
import { splitSentences } from "./sentences.js";
|
|
2
|
-
import {
|
|
2
|
+
import { proseSourceText } 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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (hasStringValue(node)) {
|
|
14
|
-
return node.value;
|
|
15
|
-
}
|
|
16
|
-
if (node.type === "Break") {
|
|
17
|
-
return " ";
|
|
9
|
+
function collectRuleParagraphs(node, paragraphs) {
|
|
10
|
+
if (isParagraphNode(node)) {
|
|
11
|
+
paragraphs.push(node);
|
|
12
|
+
return;
|
|
18
13
|
}
|
|
19
14
|
if (!isParentNode(node)) {
|
|
20
|
-
return
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
for (const child of node.children) {
|
|
18
|
+
collectRuleParagraphs(child, paragraphs);
|
|
21
19
|
}
|
|
22
|
-
return node.children.map((child) => plainText(child)).join("");
|
|
23
20
|
}
|
|
24
|
-
function
|
|
21
|
+
function collectDocumentParagraphs(node, paragraphs) {
|
|
25
22
|
if (isParagraphNode(node)) {
|
|
26
23
|
paragraphs.push(node);
|
|
27
24
|
return;
|
|
@@ -30,13 +27,20 @@ function collectParagraphs(node, paragraphs) {
|
|
|
30
27
|
return;
|
|
31
28
|
}
|
|
32
29
|
for (const child of node.children) {
|
|
33
|
-
|
|
30
|
+
collectDocumentParagraphs(child, paragraphs);
|
|
34
31
|
}
|
|
35
32
|
}
|
|
36
|
-
function
|
|
33
|
+
function sectionRuleParagraphs(section) {
|
|
37
34
|
const paragraphs = [];
|
|
38
35
|
for (const node of section) {
|
|
39
|
-
|
|
36
|
+
collectRuleParagraphs(node, paragraphs);
|
|
37
|
+
}
|
|
38
|
+
return paragraphs;
|
|
39
|
+
}
|
|
40
|
+
function sectionDocumentParagraphs(section) {
|
|
41
|
+
const paragraphs = [];
|
|
42
|
+
for (const node of section) {
|
|
43
|
+
collectDocumentParagraphs(node, paragraphs);
|
|
40
44
|
}
|
|
41
45
|
return paragraphs;
|
|
42
46
|
}
|
|
@@ -59,7 +63,7 @@ function documentSections(document) {
|
|
|
59
63
|
return sections;
|
|
60
64
|
}
|
|
61
65
|
function paragraphSentences(paragraph) {
|
|
62
|
-
const source =
|
|
66
|
+
const source = proseSourceText(paragraph);
|
|
63
67
|
return splitSentences(source.text).map((sentence) => ({
|
|
64
68
|
paragraph,
|
|
65
69
|
sentence,
|
|
@@ -69,29 +73,36 @@ function paragraphSentences(paragraph) {
|
|
|
69
73
|
export function allParagraphSentences(document) {
|
|
70
74
|
const sentences = [];
|
|
71
75
|
for (const section of documentSections(document)) {
|
|
72
|
-
for (const paragraph of
|
|
76
|
+
for (const paragraph of sectionRuleParagraphs(section)) {
|
|
73
77
|
sentences.push(...paragraphSentences(paragraph));
|
|
74
78
|
}
|
|
75
79
|
}
|
|
76
80
|
return sentences;
|
|
77
81
|
}
|
|
78
|
-
|
|
82
|
+
function mappedParagraphs(document, select) {
|
|
79
83
|
const paragraphs = [];
|
|
80
84
|
for (const section of documentSections(document)) {
|
|
81
|
-
for (const paragraph of
|
|
85
|
+
for (const paragraph of select(section)) {
|
|
86
|
+
const source = proseSourceText(paragraph);
|
|
82
87
|
paragraphs.push({
|
|
83
88
|
paragraph,
|
|
84
|
-
source
|
|
85
|
-
text:
|
|
89
|
+
source,
|
|
90
|
+
text: source.text
|
|
86
91
|
});
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
return paragraphs;
|
|
90
95
|
}
|
|
96
|
+
export function allDocumentParagraphs(document) {
|
|
97
|
+
return mappedParagraphs(document, sectionDocumentParagraphs);
|
|
98
|
+
}
|
|
99
|
+
export function allParagraphs(document) {
|
|
100
|
+
return mappedParagraphs(document, sectionRuleParagraphs);
|
|
101
|
+
}
|
|
91
102
|
export function sectionFirstSentences(document) {
|
|
92
103
|
const sentences = [];
|
|
93
104
|
for (const section of documentSections(document)) {
|
|
94
|
-
const firstParagraph =
|
|
105
|
+
const firstParagraph = sectionRuleParagraphs(section).at(0);
|
|
95
106
|
if (firstParagraph === undefined) {
|
|
96
107
|
continue;
|
|
97
108
|
}
|
|
@@ -105,7 +116,7 @@ export function sectionFirstSentences(document) {
|
|
|
105
116
|
export function sectionLastSentences(document) {
|
|
106
117
|
const sentences = [];
|
|
107
118
|
for (const section of documentSections(document)) {
|
|
108
|
-
const lastParagraph =
|
|
119
|
+
const lastParagraph = sectionRuleParagraphs(section).at(-1);
|
|
109
120
|
if (lastParagraph === undefined) {
|
|
110
121
|
continue;
|
|
111
122
|
}
|
|
@@ -1,13 +1,67 @@
|
|
|
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
|
+
const alt = "alt" in node && typeof node.alt === "string" ? node.alt : "";
|
|
19
|
+
return {
|
|
20
|
+
...node,
|
|
21
|
+
type: "Str",
|
|
22
|
+
value: " ".repeat(alt.length)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (node.type === "Code" && !includeImageAlt) {
|
|
26
|
+
const value = "value" in node && typeof node.value === "string" ? node.value : "";
|
|
27
|
+
return {
|
|
28
|
+
...node,
|
|
29
|
+
type: "Str",
|
|
30
|
+
value: " ".repeat(value.length)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (isHtmlNode(node) && !includeImageAlt) {
|
|
34
|
+
return {
|
|
35
|
+
...node,
|
|
36
|
+
type: "Str",
|
|
37
|
+
value: node.value
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (!isParentNode(node)) {
|
|
41
|
+
return node;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
...node,
|
|
45
|
+
// type-coverage:ignore-next-line
|
|
46
|
+
children: node.children.map((child) => normalizeNode(child, includeImageAlt))
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function mappedSourceText(node, includeImageAlt) {
|
|
3
50
|
// textlint-util-to-string has not updated its public type for textlint 15.7's
|
|
4
51
|
// readonly children, but it only reads the node at runtime.
|
|
5
|
-
const source = new StringSource(
|
|
6
|
-
|
|
52
|
+
const source = new StringSource(
|
|
53
|
+
// type-coverage:ignore-next-line
|
|
54
|
+
normalizeNode(node, includeImageAlt));
|
|
7
55
|
return {
|
|
8
56
|
originalEndFor: (end) => source.originalIndexFromIndex(end, true) ?? end,
|
|
9
57
|
originalStartFor: (start) => source.originalIndexFromIndex(start) ?? start,
|
|
10
58
|
text: source.toString()
|
|
11
59
|
};
|
|
12
60
|
}
|
|
61
|
+
export function proseSourceText(node) {
|
|
62
|
+
return mappedSourceText(node, false);
|
|
63
|
+
}
|
|
64
|
+
export function sourceText(node) {
|
|
65
|
+
return mappedSourceText(node, true);
|
|
66
|
+
}
|
|
13
67
|
//# 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.29",
|
|
4
4
|
"description": "Deterministic textlint rules and CLI for catching prose slop in English Markdown.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"textlint",
|
|
@@ -111,7 +111,9 @@
|
|
|
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-filler": "./dist/rules/words/quietly-filler.js",
|
|
116
|
+
"./rules/words/quietly-overuse": "./dist/rules/words/quietly-overuse.js"
|
|
115
117
|
},
|
|
116
118
|
"dependencies": {
|
|
117
119
|
"@textlint/ast-node-types": "15.7.1",
|