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.
@@ -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 { allParagraphs } from "./sections.js";
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 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,27 +1,24 @@
1
1
  import { splitSentences } from "./sentences.js";
2
- import { sourceText } from "./traverse.js";
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 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 " ";
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 collectParagraphs(node, paragraphs) {
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
- collectParagraphs(child, paragraphs);
30
+ collectDocumentParagraphs(child, paragraphs);
34
31
  }
35
32
  }
36
- function sectionParagraphs(section) {
33
+ function sectionRuleParagraphs(section) {
37
34
  const paragraphs = [];
38
35
  for (const node of section) {
39
- collectParagraphs(node, paragraphs);
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 = sourceText(paragraph);
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 sectionParagraphs(section)) {
76
+ for (const paragraph of sectionRuleParagraphs(section)) {
73
77
  sentences.push(...paragraphSentences(paragraph));
74
78
  }
75
79
  }
76
80
  return sentences;
77
81
  }
78
- export function allParagraphs(document) {
82
+ function mappedParagraphs(document, select) {
79
83
  const paragraphs = [];
80
84
  for (const section of documentSections(document)) {
81
- for (const paragraph of sectionParagraphs(section)) {
85
+ for (const paragraph of select(section)) {
86
+ const source = proseSourceText(paragraph);
82
87
  paragraphs.push({
83
88
  paragraph,
84
- source: sourceText(paragraph),
85
- text: plainText(paragraph)
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 = sectionParagraphs(section).at(0);
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 = sectionParagraphs(section).at(-1);
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
- 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
+ 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(node // type-coverage:ignore-line
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.27",
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",