slopless 0.2.27 → 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.
@@ -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 text = documentText(document);
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) => range,
55
+ sourceRangeFor: (range) => ({
56
+ end: source.originalEndFor(range.end),
57
+ start: source.originalStartFor(range.start)
58
+ }),
55
59
  text
56
60
  };
57
61
  }
@@ -42,6 +42,7 @@ export const everything = {
42
42
  "perception-verb-density": true,
43
43
  "prohibited-phrases": true,
44
44
  "prohibited-words": true,
45
+ "quietly-overuse": true,
45
46
  "recommended-terms": true,
46
47
  redundancy: true,
47
48
  "repeated-predicate-end": true,
@@ -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": "contains",
5
+ "matchMode": "full",
6
6
  "maxTokens": 80,
7
7
  "templates": [
8
- "{genericSubject} {changeVerb}.",
9
- "{genericSubject} had {changeVerb}.",
10
- "{genericSubject} in {abstractContainer} had {changeVerb}.",
11
- "{concreteSubject} did not {changeVerb}, but {genericSubject} in it did.",
12
- "{genericSubject} {changeVerb} when {genericSubject} {abstractAction}."
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",
@@ -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 { wordTokens } from "../../shared/text/tokens.js";
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 = defineTextlintRule({
13
- detector: {
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
- reportPolicy: {
35
- errorPerUnit: 2,
36
- kind: "density-rate",
37
- minimumOccurrences: 2,
38
- scope: "document",
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 allParagraphs(document)
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: sourceText(paragraph),
85
- text: plainText(paragraph)
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
- export function sourceText(node) {
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(node // type-coverage:ignore-line
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.27",
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",