agent-simple-english 0.1.0
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/.claude-plugin/marketplace.json +23 -0
- package/.claude-plugin/plugin.json +12 -0
- package/LICENSE +21 -0
- package/README.md +435 -0
- package/THIRD_PARTY_NOTICES.md +13 -0
- package/commands/ste.md +13 -0
- package/hooks/hooks.json +58 -0
- package/package.json +64 -0
- package/src/adapter/commit-message.ts +472 -0
- package/src/adapter/feedback.ts +40 -0
- package/src/adapter/rule-summary.ts +79 -0
- package/src/cli/hook.ts +681 -0
- package/src/cli/main.ts +201 -0
- package/src/cli/session-command.ts +78 -0
- package/src/cli/session-state.ts +214 -0
- package/src/config/load.ts +85 -0
- package/src/config/merge.ts +20 -0
- package/src/config/schema.ts +69 -0
- package/src/dictionary/README.md +18 -0
- package/src/dictionary/data/pi-ste.json +200 -0
- package/src/dictionary/form.ts +6 -0
- package/src/dictionary/load.ts +54 -0
- package/src/dictionary/schema.ts +28 -0
- package/src/engine/comments.ts +386 -0
- package/src/engine/diff.ts +328 -0
- package/src/engine/identifiers.ts +20 -0
- package/src/engine/kinds.ts +43 -0
- package/src/engine/lint.ts +530 -0
- package/src/engine/markdown.ts +338 -0
- package/src/engine/paragraphs.ts +105 -0
- package/src/engine/rules/contraction.ts +19 -0
- package/src/engine/rules/dictionary.ts +281 -0
- package/src/engine/rules/hedging.ts +27 -0
- package/src/engine/rules/marketing.ts +71 -0
- package/src/engine/rules/paragraph-length.ts +23 -0
- package/src/engine/rules/phrasal-verb.ts +57 -0
- package/src/engine/rules/registry.ts +15 -0
- package/src/engine/rules/semicolon.ts +14 -0
- package/src/engine/rules/sentence-length.ts +24 -0
- package/src/engine/rules/verb-form.ts +76 -0
- package/src/engine/scan.ts +15 -0
- package/src/engine/sentences.ts +285 -0
- package/src/engine/tagger.ts +8 -0
- package/src/engine/tokens.ts +2 -0
- package/src/engine/types.ts +45 -0
- package/src/extension/index.ts +755 -0
- package/src/tagger/wink.ts +44 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
const OPENING_FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/
|
|
2
|
+
const CLOSING_FENCE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/
|
|
3
|
+
const INDENTED = /^(?: {4,}|\t)/
|
|
4
|
+
const ATX_HEADING = /^ {0,3}#{1,6}(?:[ \t]+|$)/
|
|
5
|
+
const LIST_MARKER = /^( {0,3})(?:[-+*]|\d{1,9}[.)])(?:([ \t]{1,4})(?![ \t])|[ \t])/
|
|
6
|
+
|
|
7
|
+
const blankLine = (line: string): string => " ".repeat(line.length)
|
|
8
|
+
|
|
9
|
+
interface Container {
|
|
10
|
+
readonly quoteDepth: number
|
|
11
|
+
readonly listIndent: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface MarkdownContent {
|
|
15
|
+
readonly text: string
|
|
16
|
+
readonly start: number
|
|
17
|
+
readonly container: Container
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const consumeBlockquotes = (
|
|
21
|
+
line: string,
|
|
22
|
+
initial: number,
|
|
23
|
+
requiredDepth?: number,
|
|
24
|
+
): { index: number; depth: number } => {
|
|
25
|
+
let index = initial
|
|
26
|
+
let depth = 0
|
|
27
|
+
while (index < line.length && (requiredDepth === undefined || depth < requiredDepth)) {
|
|
28
|
+
let marker = index
|
|
29
|
+
let spaces = 0
|
|
30
|
+
while (spaces < 3 && line[marker] === " ") {
|
|
31
|
+
marker++
|
|
32
|
+
spaces++
|
|
33
|
+
}
|
|
34
|
+
if (line[marker] !== ">") break
|
|
35
|
+
depth++
|
|
36
|
+
index = marker + 1
|
|
37
|
+
if (line[index] === " " || line[index] === "\t") index++
|
|
38
|
+
}
|
|
39
|
+
return { index, depth }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const markdownContent = (line: string, contentStart: number): MarkdownContent => {
|
|
43
|
+
const base = Math.min(contentStart, line.length)
|
|
44
|
+
const blockquotes = consumeBlockquotes(line, base)
|
|
45
|
+
let index = blockquotes.index
|
|
46
|
+
let listIndent = 0
|
|
47
|
+
|
|
48
|
+
while (index < line.length) {
|
|
49
|
+
const match = line.slice(index).match(LIST_MARKER)
|
|
50
|
+
if (match === null) break
|
|
51
|
+
const width = match[0].length
|
|
52
|
+
listIndent += width
|
|
53
|
+
index += width
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
text: line.slice(index),
|
|
58
|
+
start: index,
|
|
59
|
+
container: { quoteDepth: blockquotes.depth, listIndent },
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const contentWithin = (
|
|
64
|
+
line: string,
|
|
65
|
+
contentStart: number,
|
|
66
|
+
container: Container,
|
|
67
|
+
): MarkdownContent | null => {
|
|
68
|
+
const base = Math.min(contentStart, line.length)
|
|
69
|
+
if (container.quoteDepth === 0 && container.listIndent === 0) {
|
|
70
|
+
return { text: line.slice(base), start: base, container }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const blockquotes = consumeBlockquotes(line, base, container.quoteDepth)
|
|
74
|
+
if (blockquotes.depth !== container.quoteDepth) return null
|
|
75
|
+
|
|
76
|
+
let index = blockquotes.index
|
|
77
|
+
if (line.slice(index).trim() === "") {
|
|
78
|
+
return { text: "", start: line.length, container }
|
|
79
|
+
}
|
|
80
|
+
let remaining = container.listIndent
|
|
81
|
+
while (remaining > 0 && (line[index] === " " || line[index] === "\t")) {
|
|
82
|
+
index++
|
|
83
|
+
remaining--
|
|
84
|
+
}
|
|
85
|
+
if (remaining > 0) return null
|
|
86
|
+
|
|
87
|
+
return { text: line.slice(index), start: index, container }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const fenceLine = (line: string): string => (line.endsWith("\r") ? line.slice(0, -1) : line)
|
|
91
|
+
|
|
92
|
+
const openingFence = (line: string): string | null => {
|
|
93
|
+
const match = fenceLine(line).match(OPENING_FENCE)
|
|
94
|
+
if (match === null) return null
|
|
95
|
+
const marker = match[1] as string
|
|
96
|
+
const info = match[2] as string
|
|
97
|
+
return marker[0] === "`" && info.includes("`") ? null : marker
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const closesFence = (line: string, fence: string): boolean => {
|
|
101
|
+
const marker = fenceLine(line).match(CLOSING_FENCE)?.[1]
|
|
102
|
+
return marker !== undefined && marker[0] === fence[0] && marker.length >= fence.length
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface BacktickRun {
|
|
106
|
+
readonly start: number
|
|
107
|
+
readonly end: number
|
|
108
|
+
readonly length: number
|
|
109
|
+
readonly escaped: boolean
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const blankInlineCodeSpans = (text: string): string => {
|
|
113
|
+
const output = text.split("")
|
|
114
|
+
const runs: BacktickRun[] = []
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < text.length; ) {
|
|
117
|
+
if (text[i] !== "`") {
|
|
118
|
+
i++
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
let end = i + 1
|
|
122
|
+
while (text[end] === "`") end++
|
|
123
|
+
let backslashes = 0
|
|
124
|
+
for (let j = i - 1; j >= 0 && text[j] === "\\"; j--) backslashes++
|
|
125
|
+
runs.push({ start: i, end, length: end - i, escaped: backslashes % 2 !== 0 })
|
|
126
|
+
i = end
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const nextMatchingRun = new Array<number | undefined>(runs.length)
|
|
130
|
+
const previousByLength = new Map<number, number>()
|
|
131
|
+
for (let i = 0; i < runs.length; i++) {
|
|
132
|
+
const run = runs[i] as BacktickRun
|
|
133
|
+
const previous = previousByLength.get(run.length)
|
|
134
|
+
if (previous !== undefined) nextMatchingRun[previous] = i
|
|
135
|
+
previousByLength.set(run.length, i)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < runs.length; ) {
|
|
139
|
+
const opener = runs[i] as BacktickRun
|
|
140
|
+
const closerIndex = nextMatchingRun[i]
|
|
141
|
+
if (opener.escaped || closerIndex === undefined) {
|
|
142
|
+
i++
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
const closer = runs[closerIndex] as BacktickRun
|
|
146
|
+
for (let j = opener.start; j < closer.end; j++) {
|
|
147
|
+
if (output[j] !== "\n") output[j] = " "
|
|
148
|
+
}
|
|
149
|
+
i = closerIndex + 1
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return output.join("")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interface FenceState {
|
|
156
|
+
readonly marker: string
|
|
157
|
+
readonly container: Container
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface MarkdownCodeResult {
|
|
161
|
+
readonly lines: string[]
|
|
162
|
+
readonly structuralLines: string[]
|
|
163
|
+
readonly structuralBlanks: boolean[]
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function blankMarkdownCodeWithStructure(
|
|
167
|
+
inputLines: readonly string[],
|
|
168
|
+
contentStarts: readonly number[] = inputLines.map(() => 0),
|
|
169
|
+
): MarkdownCodeResult {
|
|
170
|
+
let fence: FenceState | null = null
|
|
171
|
+
let activeList: Container | null = null
|
|
172
|
+
let paragraphCanContinue = false
|
|
173
|
+
let inIndented = false
|
|
174
|
+
const inlineEligible: boolean[] = []
|
|
175
|
+
const structuralLines: string[] = []
|
|
176
|
+
const structuralBlanks: boolean[] = []
|
|
177
|
+
const lines = inputLines.map((line, index) => {
|
|
178
|
+
const contentStart = contentStarts[index] ?? 0
|
|
179
|
+
|
|
180
|
+
if (fence !== null) {
|
|
181
|
+
const contained = contentWithin(line, contentStart, fence.container)
|
|
182
|
+
if (contained !== null) {
|
|
183
|
+
if (closesFence(contained.text, fence.marker)) {
|
|
184
|
+
fence = null
|
|
185
|
+
paragraphCanContinue = false
|
|
186
|
+
inIndented = false
|
|
187
|
+
}
|
|
188
|
+
inlineEligible.push(false)
|
|
189
|
+
structuralLines.push(blankLine(line))
|
|
190
|
+
structuralBlanks.push(true)
|
|
191
|
+
return blankLine(line)
|
|
192
|
+
}
|
|
193
|
+
fence = null
|
|
194
|
+
paragraphCanContinue = false
|
|
195
|
+
inIndented = false
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let content = markdownContent(line, contentStart)
|
|
199
|
+
if (content.container.listIndent > 0) {
|
|
200
|
+
activeList = content.container
|
|
201
|
+
} else if (content.text.trim() !== "" && activeList !== null) {
|
|
202
|
+
const continued = contentWithin(line, contentStart, activeList)
|
|
203
|
+
if (continued === null) {
|
|
204
|
+
activeList = null
|
|
205
|
+
} else {
|
|
206
|
+
content = continued
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const visibleLine = `${" ".repeat(content.start)}${line.slice(content.start)}`
|
|
211
|
+
const marker = openingFence(content.text)
|
|
212
|
+
if (marker !== null) {
|
|
213
|
+
fence = { marker, container: content.container }
|
|
214
|
+
paragraphCanContinue = false
|
|
215
|
+
inIndented = false
|
|
216
|
+
inlineEligible.push(false)
|
|
217
|
+
structuralLines.push(blankLine(line))
|
|
218
|
+
structuralBlanks.push(true)
|
|
219
|
+
return blankLine(line)
|
|
220
|
+
}
|
|
221
|
+
if (content.text.trim() === "") {
|
|
222
|
+
paragraphCanContinue = false
|
|
223
|
+
inIndented = false
|
|
224
|
+
inlineEligible.push(false)
|
|
225
|
+
structuralLines.push(line)
|
|
226
|
+
structuralBlanks.push(true)
|
|
227
|
+
return visibleLine
|
|
228
|
+
}
|
|
229
|
+
if (INDENTED.test(content.text) && (!paragraphCanContinue || inIndented)) {
|
|
230
|
+
paragraphCanContinue = false
|
|
231
|
+
inIndented = true
|
|
232
|
+
inlineEligible.push(false)
|
|
233
|
+
structuralLines.push(blankLine(line))
|
|
234
|
+
structuralBlanks.push(true)
|
|
235
|
+
return blankLine(line)
|
|
236
|
+
}
|
|
237
|
+
paragraphCanContinue = !ATX_HEADING.test(content.text)
|
|
238
|
+
inIndented = false
|
|
239
|
+
inlineEligible.push(true)
|
|
240
|
+
structuralLines.push(line)
|
|
241
|
+
structuralBlanks.push(false)
|
|
242
|
+
return visibleLine
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
const blankEligibleInlineCode = (target: string[]) => {
|
|
246
|
+
let start = 0
|
|
247
|
+
while (start < target.length) {
|
|
248
|
+
if (!inlineEligible[start]) {
|
|
249
|
+
start++
|
|
250
|
+
continue
|
|
251
|
+
}
|
|
252
|
+
let end = start + 1
|
|
253
|
+
while (end < target.length && inlineEligible[end]) end++
|
|
254
|
+
const blanked = blankInlineCodeSpans(target.slice(start, end).join("\n")).split("\n")
|
|
255
|
+
for (let i = start; i < end; i++) target[i] = blanked[i - start] as string
|
|
256
|
+
start = end
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
blankEligibleInlineCode(lines)
|
|
261
|
+
blankEligibleInlineCode(structuralLines)
|
|
262
|
+
|
|
263
|
+
return { lines, structuralLines, structuralBlanks }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function blankMarkdownCode(
|
|
267
|
+
inputLines: readonly string[],
|
|
268
|
+
contentStarts: readonly number[] = inputLines.map(() => 0),
|
|
269
|
+
): string[] {
|
|
270
|
+
return blankMarkdownCodeWithStructure(inputLines, contentStarts).lines
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function maskMarkdownCode(text: string): string {
|
|
274
|
+
return blankMarkdownCode(text.split("\n")).join("\n")
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
interface InlineBacktickRun {
|
|
278
|
+
readonly start: number
|
|
279
|
+
readonly length: number
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function blankInlineCodeLine(line: string): string {
|
|
283
|
+
const runs: InlineBacktickRun[] = []
|
|
284
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
285
|
+
if (line[index] !== "`") continue
|
|
286
|
+
const start = index
|
|
287
|
+
while (line[index + 1] === "`") index += 1
|
|
288
|
+
runs.push({ start, length: index - start + 1 })
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const nextMatchingRun = new Int32Array(runs.length).fill(-1)
|
|
292
|
+
const latestRunByLength = new Map<number, number>()
|
|
293
|
+
for (let index = runs.length - 1; index >= 0; index -= 1) {
|
|
294
|
+
const run = runs[index]
|
|
295
|
+
if (!run) continue
|
|
296
|
+
nextMatchingRun[index] = latestRunByLength.get(run.length) ?? -1
|
|
297
|
+
latestRunByLength.set(run.length, index)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const masked = line.split("")
|
|
301
|
+
for (let index = 0; index < runs.length; index += 1) {
|
|
302
|
+
const closingIndex = nextMatchingRun[index] ?? -1
|
|
303
|
+
if (closingIndex < 0) continue
|
|
304
|
+
const opening = runs[index]
|
|
305
|
+
const closing = runs[closingIndex]
|
|
306
|
+
if (!opening || !closing) continue
|
|
307
|
+
|
|
308
|
+
masked.fill(" ", opening.start, closing.start + closing.length)
|
|
309
|
+
index = closingIndex
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return masked.join("")
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function blankInlineCode(lines: readonly string[]): string[] {
|
|
316
|
+
return lines.map(blankInlineCodeLine)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function proseVisibility(text: string): Uint8Array {
|
|
320
|
+
const visibility = new Uint8Array(text.length)
|
|
321
|
+
const sourceLines = text.split("\n")
|
|
322
|
+
const proseLines = blankMarkdownCode(sourceLines)
|
|
323
|
+
let offset = 0
|
|
324
|
+
|
|
325
|
+
for (let index = 0; index < sourceLines.length; index++) {
|
|
326
|
+
const line = sourceLines[index] ?? ""
|
|
327
|
+
if (line === proseLines[index]) {
|
|
328
|
+
visibility.fill(1, offset, offset + line.length)
|
|
329
|
+
}
|
|
330
|
+
offset += line.length
|
|
331
|
+
if (offset < text.length) {
|
|
332
|
+
visibility[offset] = 1
|
|
333
|
+
offset++
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return visibility
|
|
338
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export interface Paragraph {
|
|
2
|
+
readonly lines: readonly string[]
|
|
3
|
+
readonly line: number
|
|
4
|
+
readonly column: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const LIST_MARKER = /^(?:[-*+]|\d+[.)])\s+/
|
|
8
|
+
|
|
9
|
+
type LineKind = "blank" | "block-boundary" | "blockquote" | "list-item" | "prose"
|
|
10
|
+
type ParagraphKind = "blockquote" | "blockquote-list-item" | "list-item" | "prose"
|
|
11
|
+
|
|
12
|
+
const ATX_HEADING = /^ {0,3}#{1,6}(?:[ \t]+|$)/
|
|
13
|
+
const BLOCKQUOTE = /^ {0,3}>[ \t]?/
|
|
14
|
+
|
|
15
|
+
function listItemContent(line: string): string | undefined {
|
|
16
|
+
const trimmed = line.trimStart()
|
|
17
|
+
const marker = LIST_MARKER.exec(trimmed)
|
|
18
|
+
return marker ? trimmed.slice(marker[0].length) : undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function blockquoteContent(line: string): string | undefined {
|
|
22
|
+
const marker = BLOCKQUOTE.exec(line)
|
|
23
|
+
return marker ? line.slice(marker[0].length) : undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function classify(line: string): LineKind {
|
|
27
|
+
const trimmed = line.trim()
|
|
28
|
+
if (trimmed === "") return "blank"
|
|
29
|
+
if (trimmed.startsWith("|") || ATX_HEADING.test(line)) return "block-boundary"
|
|
30
|
+
if (blockquoteContent(line) !== undefined) return "blockquote"
|
|
31
|
+
if (listItemContent(line) !== undefined) return "list-item"
|
|
32
|
+
return "prose"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function segmentParagraphs(
|
|
36
|
+
lines: readonly string[],
|
|
37
|
+
columns: readonly number[] = lines.map(() => 1),
|
|
38
|
+
): Paragraph[] {
|
|
39
|
+
const paragraphs: Paragraph[] = []
|
|
40
|
+
let open: { line: number; column: number; lines: string[]; kind: ParagraphKind } | null = null
|
|
41
|
+
|
|
42
|
+
const close = () => {
|
|
43
|
+
if (open) {
|
|
44
|
+
paragraphs.push({ lines: open.lines, line: open.line, column: open.column })
|
|
45
|
+
open = null
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
lines.forEach((raw, index) => {
|
|
50
|
+
switch (classify(raw)) {
|
|
51
|
+
case "blank":
|
|
52
|
+
case "block-boundary":
|
|
53
|
+
close()
|
|
54
|
+
break
|
|
55
|
+
case "blockquote": {
|
|
56
|
+
const content = blockquoteContent(raw) ?? ""
|
|
57
|
+
const contentKind = classify(content)
|
|
58
|
+
if (contentKind === "blank" || contentKind === "block-boundary") {
|
|
59
|
+
close()
|
|
60
|
+
break
|
|
61
|
+
}
|
|
62
|
+
if (contentKind === "list-item") {
|
|
63
|
+
close()
|
|
64
|
+
open = {
|
|
65
|
+
line: index + 1,
|
|
66
|
+
column: columns[index] ?? 1,
|
|
67
|
+
lines: [listItemContent(content) ?? ""],
|
|
68
|
+
kind: "blockquote-list-item",
|
|
69
|
+
}
|
|
70
|
+
break
|
|
71
|
+
}
|
|
72
|
+
if (open?.kind !== "blockquote" && open?.kind !== "blockquote-list-item") close()
|
|
73
|
+
if (!open) {
|
|
74
|
+
open = { line: index + 1, column: columns[index] ?? 1, lines: [], kind: "blockquote" }
|
|
75
|
+
}
|
|
76
|
+
open.lines.push(open.kind === "blockquote-list-item" ? content.trimStart() : content)
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
case "list-item":
|
|
80
|
+
close()
|
|
81
|
+
open = {
|
|
82
|
+
line: index + 1,
|
|
83
|
+
column: columns[index] ?? 1,
|
|
84
|
+
lines: [listItemContent(raw) ?? ""],
|
|
85
|
+
kind: "list-item",
|
|
86
|
+
}
|
|
87
|
+
break
|
|
88
|
+
case "prose": {
|
|
89
|
+
if (!open) {
|
|
90
|
+
open = {
|
|
91
|
+
line: index + 1,
|
|
92
|
+
column: columns[index] ?? 1,
|
|
93
|
+
lines: [],
|
|
94
|
+
kind: "prose",
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
open.lines.push(open.kind === "list-item" ? raw.trimStart() : raw)
|
|
98
|
+
break
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
close()
|
|
103
|
+
|
|
104
|
+
return paragraphs
|
|
105
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { scanLines } from "../scan.ts"
|
|
2
|
+
import type { Violation } from "../types.ts"
|
|
3
|
+
|
|
4
|
+
const CONTRACTION = /\b\w+['’](?:t|re|ve|ll|d|m)\b/gi
|
|
5
|
+
|
|
6
|
+
// `'s` is the only ambiguous case: "repo's" is a possessive, not a
|
|
7
|
+
// contraction, so only this fixed set of stems forms a real `'s` contraction.
|
|
8
|
+
const CONTRACTION_S =
|
|
9
|
+
/\b(?:it|he|she|that|what|who|there|here|let|one|where|how|everyone|everybody|something|nothing|somebody|nobody)['’]s\b/gi
|
|
10
|
+
|
|
11
|
+
export function contraction(lines: readonly string[]): Violation[] {
|
|
12
|
+
return [...scanLines(lines, CONTRACTION), ...scanLines(lines, CONTRACTION_S)].map((match) => ({
|
|
13
|
+
ruleId: "contraction",
|
|
14
|
+
severity: "hard" as const,
|
|
15
|
+
message: `Do not use a contraction. Write the words in full. Found "${match.found}".`,
|
|
16
|
+
line: match.line,
|
|
17
|
+
column: match.column,
|
|
18
|
+
}))
|
|
19
|
+
}
|