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,200 @@
|
|
|
1
|
+
{
|
|
2
|
+
"formatVersion": 1,
|
|
3
|
+
"source": {
|
|
4
|
+
"name": "ctotheameron/pi-ste dictionary",
|
|
5
|
+
"repository": "https://github.com/ctotheameron/pi-ste",
|
|
6
|
+
"commit": "18a8cc686be2cc0e680705daf2327fb0d1ef93ce",
|
|
7
|
+
"path": "src/ste/dictionary.gleam"
|
|
8
|
+
},
|
|
9
|
+
"entries": [
|
|
10
|
+
{
|
|
11
|
+
"unapproved": ["initiate", "initiates", "initiated", "initiating"],
|
|
12
|
+
"suggestions": ["start"],
|
|
13
|
+
"partsOfSpeech": ["VERB"]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"unapproved": ["commence", "commences", "commenced", "commencing"],
|
|
17
|
+
"suggestions": ["start"],
|
|
18
|
+
"partsOfSpeech": ["VERB"]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"unapproved": ["utilize", "utilizes", "utilized", "utilizing"],
|
|
22
|
+
"suggestions": ["use"],
|
|
23
|
+
"partsOfSpeech": ["VERB"]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"unapproved": ["utilise", "utilises", "utilised", "utilising"],
|
|
27
|
+
"suggestions": ["use"],
|
|
28
|
+
"partsOfSpeech": ["VERB"]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"unapproved": ["ensure", "ensures", "ensured", "ensuring"],
|
|
32
|
+
"suggestions": ["make sure"],
|
|
33
|
+
"partsOfSpeech": ["VERB"]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"unapproved": ["terminate", "terminates", "terminated", "terminating"],
|
|
37
|
+
"suggestions": ["stop"],
|
|
38
|
+
"partsOfSpeech": ["VERB"]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"unapproved": ["facilitate", "facilitates", "facilitated", "facilitating"],
|
|
42
|
+
"suggestions": ["help"],
|
|
43
|
+
"partsOfSpeech": ["VERB"]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"unapproved": ["locate", "locates", "located", "locating"],
|
|
47
|
+
"suggestions": ["find"],
|
|
48
|
+
"partsOfSpeech": ["VERB"]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"unapproved": ["indicate", "indicates", "indicated", "indicating"],
|
|
52
|
+
"suggestions": ["show"],
|
|
53
|
+
"partsOfSpeech": ["VERB"]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"unapproved": ["require", "requires", "required", "requiring"],
|
|
57
|
+
"suggestions": ["need"],
|
|
58
|
+
"partsOfSpeech": ["VERB"]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"unapproved": ["purchase", "purchases", "purchased", "purchasing"],
|
|
62
|
+
"suggestions": ["buy"],
|
|
63
|
+
"partsOfSpeech": ["VERB"]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"unapproved": ["leverage", "leverages", "leveraged", "leveraging"],
|
|
67
|
+
"suggestions": ["use"],
|
|
68
|
+
"partsOfSpeech": ["VERB"]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"unapproved": ["acquire", "acquires", "acquired", "acquiring"],
|
|
72
|
+
"suggestions": ["get"],
|
|
73
|
+
"partsOfSpeech": ["VERB"]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"unapproved": ["demonstrate", "demonstrates", "demonstrated", "demonstrating"],
|
|
77
|
+
"suggestions": ["show"],
|
|
78
|
+
"partsOfSpeech": ["VERB"]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"unapproved": ["originate", "originates", "originated", "originating"],
|
|
82
|
+
"suggestions": ["start"],
|
|
83
|
+
"partsOfSpeech": ["VERB"]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"unapproved": ["perform", "performs", "performed", "performing"],
|
|
87
|
+
"suggestions": ["do"],
|
|
88
|
+
"partsOfSpeech": ["VERB"]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"unapproved": ["obtain", "obtains", "obtained", "obtaining"],
|
|
92
|
+
"suggestions": ["get"],
|
|
93
|
+
"partsOfSpeech": ["VERB"]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"unapproved": ["attempt", "attempts", "attempted", "attempting"],
|
|
97
|
+
"suggestions": ["try"],
|
|
98
|
+
"partsOfSpeech": ["VERB"]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"unapproved": ["assist", "assists", "assisted", "assisting"],
|
|
102
|
+
"suggestions": ["help"],
|
|
103
|
+
"partsOfSpeech": ["VERB"]
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"unapproved": ["permit", "permits", "permited", "permiting"],
|
|
107
|
+
"suggestions": ["let"],
|
|
108
|
+
"partsOfSpeech": ["VERB"]
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"unapproved": ["modify", "modifies", "modified", "modifying"],
|
|
112
|
+
"suggestions": ["change"],
|
|
113
|
+
"partsOfSpeech": ["VERB"]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"unapproved": ["begin", "begins", "began", "beginning"],
|
|
117
|
+
"suggestions": ["start"]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"unapproved": ["approximately"],
|
|
121
|
+
"suggestions": ["about"]
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"unapproved": ["sufficient"],
|
|
125
|
+
"suggestions": ["enough"]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"unapproved": ["subsequent", "subsequently"],
|
|
129
|
+
"suggestions": ["next"]
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"unapproved": ["prior"],
|
|
133
|
+
"suggestions": ["before"]
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"unapproved": ["additional", "additionally"],
|
|
137
|
+
"suggestions": ["more"]
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"unapproved": ["furthermore", "moreover"],
|
|
141
|
+
"suggestions": ["also"]
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"unapproved": ["comprehensive", "comprehensively"],
|
|
145
|
+
"suggestions": ["complete"]
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"unapproved": ["utilization"],
|
|
149
|
+
"suggestions": ["use"]
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"unapproved": ["aforementioned"],
|
|
153
|
+
"suggestions": ["this"]
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"unapproved": ["whilst"],
|
|
157
|
+
"suggestions": ["while"]
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"unapproved": ["amongst"],
|
|
161
|
+
"suggestions": ["among"]
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"unapproved": ["numerous", "myriad", "plethora"],
|
|
165
|
+
"suggestions": ["many"]
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"unapproved": ["prior to"],
|
|
169
|
+
"suggestions": ["before"]
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"unapproved": ["subsequent to"],
|
|
173
|
+
"suggestions": ["after"]
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"unapproved": ["in order to"],
|
|
177
|
+
"suggestions": ["to"]
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"unapproved": ["a variety of"],
|
|
181
|
+
"suggestions": ["some"]
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"unapproved": ["in the event that"],
|
|
185
|
+
"suggestions": ["if"]
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"unapproved": ["due to the fact that"],
|
|
189
|
+
"suggestions": ["because"]
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"unapproved": ["is able to", "are able to"],
|
|
193
|
+
"suggestions": ["can"]
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"unapproved": ["make use of", "makes use of"],
|
|
197
|
+
"suggestions": ["use"]
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises"
|
|
2
|
+
import { fileURLToPath } from "node:url"
|
|
3
|
+
import { Effect, ParseResult, Schema } from "effect"
|
|
4
|
+
import { type Dictionary, DictionarySchema } from "./schema.ts"
|
|
5
|
+
|
|
6
|
+
export const BUNDLED_DICTIONARY_PATH = fileURLToPath(new URL("./data/pi-ste.json", import.meta.url))
|
|
7
|
+
|
|
8
|
+
export class DictionaryLoadError extends Error {
|
|
9
|
+
constructor(
|
|
10
|
+
readonly path: string,
|
|
11
|
+
readonly reason: string,
|
|
12
|
+
) {
|
|
13
|
+
super(`Cannot load STE dictionary from ${path}: ${reason}`)
|
|
14
|
+
this.name = "DictionaryLoadError"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const formatParseError = (error: ParseResult.ParseError): string => {
|
|
19
|
+
const issue = ParseResult.ArrayFormatter.formatErrorSync(error)[0]
|
|
20
|
+
if (issue === undefined) {
|
|
21
|
+
return "invalid dictionary data"
|
|
22
|
+
}
|
|
23
|
+
const path = issue.path.reduce<string>((result, segment) => {
|
|
24
|
+
if (typeof segment === "number") {
|
|
25
|
+
return `${result}[${segment}]`
|
|
26
|
+
}
|
|
27
|
+
return result === "" ? String(segment) : `${result}.${String(segment)}`
|
|
28
|
+
}, "")
|
|
29
|
+
return path === "" ? `invalid dictionary data: ${issue.message}` : `${path}: ${issue.message}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const decodeDictionary = Schema.decode(Schema.parseJson(DictionarySchema), {
|
|
33
|
+
onExcessProperty: "error",
|
|
34
|
+
errors: "all",
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export const loadDictionary = (
|
|
38
|
+
path = BUNDLED_DICTIONARY_PATH,
|
|
39
|
+
): Effect.Effect<Dictionary, DictionaryLoadError> =>
|
|
40
|
+
Effect.tryPromise({
|
|
41
|
+
try: () => readFile(path, "utf8"),
|
|
42
|
+
catch: (cause) =>
|
|
43
|
+
new DictionaryLoadError(
|
|
44
|
+
path,
|
|
45
|
+
`cannot read file: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
46
|
+
),
|
|
47
|
+
}).pipe(
|
|
48
|
+
Effect.flatMap(decodeDictionary),
|
|
49
|
+
Effect.mapError((cause) =>
|
|
50
|
+
cause instanceof DictionaryLoadError
|
|
51
|
+
? cause
|
|
52
|
+
: new DictionaryLoadError(path, formatParseError(cause)),
|
|
53
|
+
),
|
|
54
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Schema } from "effect"
|
|
2
|
+
import { DICTIONARY_FORM_PATTERN } from "./form.ts"
|
|
3
|
+
|
|
4
|
+
const DictionarySourceSchema = Schema.Struct({
|
|
5
|
+
name: Schema.NonEmptyTrimmedString,
|
|
6
|
+
repository: Schema.NonEmptyTrimmedString,
|
|
7
|
+
commit: Schema.NonEmptyTrimmedString,
|
|
8
|
+
path: Schema.NonEmptyTrimmedString,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const DictionaryFormSchema = Schema.NonEmptyTrimmedString.pipe(
|
|
12
|
+
Schema.pattern(DICTIONARY_FORM_PATTERN),
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const DictionaryEntrySchema = Schema.Struct({
|
|
16
|
+
unapproved: Schema.NonEmptyArray(DictionaryFormSchema),
|
|
17
|
+
suggestions: Schema.NonEmptyArray(Schema.NonEmptyTrimmedString),
|
|
18
|
+
partsOfSpeech: Schema.optional(Schema.NonEmptyArray(Schema.NonEmptyTrimmedString)),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export const DictionarySchema = Schema.Struct({
|
|
22
|
+
formatVersion: Schema.Literal(1),
|
|
23
|
+
source: DictionarySourceSchema,
|
|
24
|
+
entries: Schema.Array(DictionaryEntrySchema),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export type Dictionary = typeof DictionarySchema.Type
|
|
28
|
+
export type DictionaryEntry = typeof DictionaryEntrySchema.Type
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import type { SourceDialect } from "./types.ts"
|
|
2
|
+
|
|
3
|
+
export interface ProseBreak {
|
|
4
|
+
readonly line: number
|
|
5
|
+
readonly column: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ExtractedComments {
|
|
9
|
+
readonly lines: readonly string[]
|
|
10
|
+
readonly contentStarts: readonly number[]
|
|
11
|
+
readonly proseBreaks: readonly ProseBreak[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const blankLine = (line: string): string => " ".repeat(line.length)
|
|
15
|
+
|
|
16
|
+
const consumeSeparator = (line: string, index: number): number =>
|
|
17
|
+
line[index] === " " || line[index] === "\t" ? index + 1 : index
|
|
18
|
+
|
|
19
|
+
const hasEscapedLineBreak = (line: string): boolean => {
|
|
20
|
+
let index = line.endsWith("\r") ? line.length - 1 : line.length
|
|
21
|
+
let backslashes = 0
|
|
22
|
+
while (line[index - 1] === "\\") {
|
|
23
|
+
backslashes++
|
|
24
|
+
index--
|
|
25
|
+
}
|
|
26
|
+
return backslashes % 2 !== 0
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const isRustLifetime = (line: string, index: number): boolean => {
|
|
30
|
+
if (line[index] !== "'" || !/[A-Za-z_]/.test(line[index + 1] ?? "")) return false
|
|
31
|
+
let end = index + 2
|
|
32
|
+
while (/[A-Za-z0-9_]/.test(line[end] ?? "")) end++
|
|
33
|
+
return line[end] !== "'"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type MultilineLiteral =
|
|
37
|
+
| { readonly kind: "escaped"; readonly terminator: string }
|
|
38
|
+
| { readonly kind: "literal"; readonly terminator: string }
|
|
39
|
+
| { readonly kind: "verbatim" }
|
|
40
|
+
|
|
41
|
+
const hasTokenBoundary = (line: string, index: number): boolean =>
|
|
42
|
+
index === 0 || !/[A-Za-z0-9_]/.test(line[index - 1] ?? "")
|
|
43
|
+
|
|
44
|
+
const multilineLiteralAt = (
|
|
45
|
+
line: string,
|
|
46
|
+
index: number,
|
|
47
|
+
): { literal: MultilineLiteral; end: number } | null => {
|
|
48
|
+
if (!hasTokenBoundary(line, index)) return null
|
|
49
|
+
|
|
50
|
+
const csharp = line.slice(index).match(/^(?:\$@|@\$|@)"/)
|
|
51
|
+
if (csharp !== null) {
|
|
52
|
+
return { literal: { kind: "verbatim" }, end: index + csharp[0].length }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const rust = line.slice(index).match(/^(?:br|r)(#{0,255})"/)
|
|
56
|
+
if (rust !== null) {
|
|
57
|
+
return {
|
|
58
|
+
literal: { kind: "literal", terminator: `"${rust[1] as string}` },
|
|
59
|
+
end: index + rust[0].length,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cpp = line.slice(index).match(/^(?:u8|u|U|L)?R"([^ ()\\\t\r\n]{0,16})\(/)
|
|
64
|
+
if (cpp !== null) {
|
|
65
|
+
return {
|
|
66
|
+
literal: { kind: "literal", terminator: `)${cpp[1] as string}"` },
|
|
67
|
+
end: index + cpp[0].length,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function extractSlashComments(text: string): ExtractedComments {
|
|
75
|
+
let inBlock = false
|
|
76
|
+
let multilineLiteral: MultilineLiteral | null = null
|
|
77
|
+
let continuedLineQuote: "'" | '"' | null = null
|
|
78
|
+
let previousComment: "line" | "block" | null = null
|
|
79
|
+
const contentStarts: number[] = []
|
|
80
|
+
const proseBreaks: ProseBreak[] = []
|
|
81
|
+
|
|
82
|
+
const lines = text.split("\n").map((line, lineIndex) => {
|
|
83
|
+
const out = new Array<string>(line.length).fill(" ")
|
|
84
|
+
let contentStart = line.length
|
|
85
|
+
let lineQuote = continuedLineQuote
|
|
86
|
+
continuedLineQuote = null
|
|
87
|
+
let i = 0
|
|
88
|
+
|
|
89
|
+
const markContentStart = (index: number) => {
|
|
90
|
+
contentStart = Math.min(contentStart, index)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const beginComment = (kind: "line" | "block", index: number) => {
|
|
94
|
+
if (previousComment !== null && (kind === "block" || previousComment === "block")) {
|
|
95
|
+
proseBreaks.push({ line: lineIndex, column: index })
|
|
96
|
+
}
|
|
97
|
+
previousComment = kind
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (inBlock) {
|
|
101
|
+
while (line[i] === " " || line[i] === "\t") i++
|
|
102
|
+
if (line[i] === "*" && line[i + 1] !== "/") i++
|
|
103
|
+
i = consumeSeparator(line, i)
|
|
104
|
+
markContentStart(i)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
while (i < line.length) {
|
|
108
|
+
const ch = line[i] as string
|
|
109
|
+
const next = line[i + 1]
|
|
110
|
+
|
|
111
|
+
if (inBlock) {
|
|
112
|
+
if (ch === "*" && next === "/") {
|
|
113
|
+
inBlock = false
|
|
114
|
+
i += 2
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
out[i] = ch
|
|
118
|
+
i++
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (multilineLiteral !== null) {
|
|
123
|
+
if (multilineLiteral.kind === "escaped" && ch === "\\") {
|
|
124
|
+
i += 2
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
if (multilineLiteral.kind === "verbatim") {
|
|
128
|
+
if (line.startsWith('""', i)) {
|
|
129
|
+
i += 2
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
if (ch === '"') {
|
|
133
|
+
multilineLiteral = null
|
|
134
|
+
i++
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
i++
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
if (line.startsWith(multilineLiteral.terminator, i)) {
|
|
141
|
+
i += multilineLiteral.terminator.length
|
|
142
|
+
multilineLiteral = null
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
i++
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (lineQuote !== null) {
|
|
150
|
+
if (ch === "\\") {
|
|
151
|
+
i += 2
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
if (ch === lineQuote) lineQuote = null
|
|
155
|
+
i++
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const boundedLiteral = multilineLiteralAt(line, i)
|
|
160
|
+
if (boundedLiteral !== null) {
|
|
161
|
+
multilineLiteral = boundedLiteral.literal
|
|
162
|
+
i = boundedLiteral.end
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
if (line.startsWith('"""', i)) {
|
|
166
|
+
multilineLiteral = { kind: "literal", terminator: '"""' }
|
|
167
|
+
i += 3
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
if (ch === "`") {
|
|
171
|
+
multilineLiteral = { kind: "escaped", terminator: "`" }
|
|
172
|
+
i++
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
if (ch === '"' || (ch === "'" && !isRustLifetime(line, i))) {
|
|
176
|
+
lineQuote = ch
|
|
177
|
+
i++
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
if (ch === "/" && next === "/") {
|
|
181
|
+
i += 2
|
|
182
|
+
while (line[i] === "/" || line[i] === "!") i++
|
|
183
|
+
i = consumeSeparator(line, i)
|
|
184
|
+
beginComment("line", i)
|
|
185
|
+
markContentStart(i)
|
|
186
|
+
for (; i < line.length; i++) out[i] = line[i] as string
|
|
187
|
+
break
|
|
188
|
+
}
|
|
189
|
+
if (ch === "/" && next === "*") {
|
|
190
|
+
inBlock = true
|
|
191
|
+
i += 2
|
|
192
|
+
while (line[i] === "*" && line[i + 1] !== "/") i++
|
|
193
|
+
i = consumeSeparator(line, i)
|
|
194
|
+
beginComment("block", i)
|
|
195
|
+
markContentStart(i)
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
i++
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (lineQuote !== null && hasEscapedLineBreak(line)) continuedLineQuote = lineQuote
|
|
202
|
+
contentStarts.push(contentStart)
|
|
203
|
+
return out.join("")
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
return { lines, contentStarts, proseBreaks }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
interface Heredoc {
|
|
210
|
+
readonly delimiter: string
|
|
211
|
+
readonly stripTabs: boolean
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
interface ParsedHeredoc extends Heredoc {
|
|
215
|
+
readonly end: number
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const parseHeredoc = (line: string, start: number): ParsedHeredoc | null => {
|
|
219
|
+
if (!line.startsWith("<<", start) || line[start + 2] === "<") return null
|
|
220
|
+
let index = start + 2
|
|
221
|
+
let stripTabs = false
|
|
222
|
+
if (line[index] === "-") {
|
|
223
|
+
stripTabs = true
|
|
224
|
+
index++
|
|
225
|
+
}
|
|
226
|
+
while (line[index] === " " || line[index] === "\t") index++
|
|
227
|
+
|
|
228
|
+
let delimiter = ""
|
|
229
|
+
while (index < line.length && !/[\s;&|()<>]/.test(line[index] as string)) {
|
|
230
|
+
const ch = line[index] as string
|
|
231
|
+
if (ch === "'" || ch === '"') {
|
|
232
|
+
const quote = ch
|
|
233
|
+
const close = line.indexOf(quote, index + 1)
|
|
234
|
+
if (close === -1) return null
|
|
235
|
+
delimiter += line.slice(index + 1, close)
|
|
236
|
+
index = close + 1
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
if (ch === "\\") {
|
|
240
|
+
if (index + 1 >= line.length) return null
|
|
241
|
+
delimiter += line[index + 1] as string
|
|
242
|
+
index += 2
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
delimiter += ch
|
|
246
|
+
index++
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return delimiter === "" ? null : { delimiter, stripTabs, end: index }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const isShellCommentStart = (line: string, index: number): boolean =>
|
|
253
|
+
index === 0 || /[\s;|&()]/.test(line[index - 1] ?? "")
|
|
254
|
+
|
|
255
|
+
export function extractHashComments(
|
|
256
|
+
text: string,
|
|
257
|
+
dialect: SourceDialect = "general",
|
|
258
|
+
): ExtractedComments {
|
|
259
|
+
let multilineQuote: "'''" | '"""' | null = null
|
|
260
|
+
let shellQuote: "'" | '"' | null = null
|
|
261
|
+
let continuedLineQuote: "'" | '"' | null = null
|
|
262
|
+
let parameterDepth = 0
|
|
263
|
+
let arithmeticDepth = 0
|
|
264
|
+
const heredocs: Heredoc[] = []
|
|
265
|
+
const contentStarts: number[] = []
|
|
266
|
+
const shell = dialect === "shell"
|
|
267
|
+
|
|
268
|
+
const lines = text.split("\n").map((line, lineIndex) => {
|
|
269
|
+
const activeHeredoc = heredocs[0]
|
|
270
|
+
if (activeHeredoc !== undefined) {
|
|
271
|
+
const normalized = line.endsWith("\r") ? line.slice(0, -1) : line
|
|
272
|
+
const candidate = activeHeredoc.stripTabs ? normalized.replace(/^\t+/, "") : normalized
|
|
273
|
+
if (candidate === activeHeredoc.delimiter) heredocs.shift()
|
|
274
|
+
contentStarts.push(line.length)
|
|
275
|
+
return blankLine(line)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (lineIndex === 0 && line.startsWith("#!")) {
|
|
279
|
+
contentStarts.push(line.length)
|
|
280
|
+
return blankLine(line)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const out = new Array<string>(line.length).fill(" ")
|
|
284
|
+
const pendingHeredocs: Heredoc[] = []
|
|
285
|
+
let contentStart = line.length
|
|
286
|
+
let lineQuote = continuedLineQuote
|
|
287
|
+
continuedLineQuote = null
|
|
288
|
+
let i = 0
|
|
289
|
+
|
|
290
|
+
while (i < line.length) {
|
|
291
|
+
const ch = line[i] as string
|
|
292
|
+
|
|
293
|
+
if (multilineQuote !== null) {
|
|
294
|
+
if (line.startsWith(multilineQuote, i)) {
|
|
295
|
+
i += multilineQuote.length
|
|
296
|
+
multilineQuote = null
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
299
|
+
i++
|
|
300
|
+
continue
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (shellQuote !== null) {
|
|
304
|
+
if (ch === "\\" && shellQuote === '"') {
|
|
305
|
+
i += 2
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
if (ch === shellQuote) shellQuote = null
|
|
309
|
+
i++
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (lineQuote !== null) {
|
|
314
|
+
if (ch === "\\") {
|
|
315
|
+
i += 2
|
|
316
|
+
continue
|
|
317
|
+
}
|
|
318
|
+
if (ch === lineQuote) lineQuote = null
|
|
319
|
+
i++
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!shell && (line.startsWith("'''", i) || line.startsWith('"""', i))) {
|
|
324
|
+
multilineQuote = line.slice(i, i + 3) as "'''" | '"""'
|
|
325
|
+
i += 3
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
if (ch === '"' || ch === "'") {
|
|
329
|
+
if (shell) shellQuote = ch
|
|
330
|
+
else lineQuote = ch
|
|
331
|
+
i++
|
|
332
|
+
continue
|
|
333
|
+
}
|
|
334
|
+
if (shell && line.startsWith("${", i)) {
|
|
335
|
+
parameterDepth++
|
|
336
|
+
i += 2
|
|
337
|
+
continue
|
|
338
|
+
}
|
|
339
|
+
if (shell && parameterDepth > 0 && ch === "}") {
|
|
340
|
+
parameterDepth--
|
|
341
|
+
i++
|
|
342
|
+
continue
|
|
343
|
+
}
|
|
344
|
+
if (shell && line.startsWith("$((", i)) {
|
|
345
|
+
arithmeticDepth++
|
|
346
|
+
i += 3
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
if (shell && arithmeticDepth > 0 && line.startsWith("))", i)) {
|
|
350
|
+
arithmeticDepth--
|
|
351
|
+
i += 2
|
|
352
|
+
continue
|
|
353
|
+
}
|
|
354
|
+
if (shell && parameterDepth === 0 && arithmeticDepth === 0 && line.startsWith("<<", i)) {
|
|
355
|
+
const heredoc = parseHeredoc(line, i)
|
|
356
|
+
if (heredoc !== null) {
|
|
357
|
+
pendingHeredocs.push({ delimiter: heredoc.delimiter, stripTabs: heredoc.stripTabs })
|
|
358
|
+
i = heredoc.end
|
|
359
|
+
continue
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (
|
|
363
|
+
ch === "#" &&
|
|
364
|
+
parameterDepth === 0 &&
|
|
365
|
+
arithmeticDepth === 0 &&
|
|
366
|
+
line[i - 1] !== "$" &&
|
|
367
|
+
(!shell || isShellCommentStart(line, i))
|
|
368
|
+
) {
|
|
369
|
+
let j = i + 1
|
|
370
|
+
while (line[j] === "#") j++
|
|
371
|
+
j = consumeSeparator(line, j)
|
|
372
|
+
contentStart = j
|
|
373
|
+
for (; j < line.length; j++) out[j] = line[j] as string
|
|
374
|
+
break
|
|
375
|
+
}
|
|
376
|
+
i++
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (lineQuote !== null && hasEscapedLineBreak(line)) continuedLineQuote = lineQuote
|
|
380
|
+
heredocs.push(...pendingHeredocs)
|
|
381
|
+
contentStarts.push(contentStart)
|
|
382
|
+
return out.join("")
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
return { lines, contentStarts, proseBreaks: [] }
|
|
386
|
+
}
|