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.
Files changed (47) hide show
  1. package/.claude-plugin/marketplace.json +23 -0
  2. package/.claude-plugin/plugin.json +12 -0
  3. package/LICENSE +21 -0
  4. package/README.md +435 -0
  5. package/THIRD_PARTY_NOTICES.md +13 -0
  6. package/commands/ste.md +13 -0
  7. package/hooks/hooks.json +58 -0
  8. package/package.json +64 -0
  9. package/src/adapter/commit-message.ts +472 -0
  10. package/src/adapter/feedback.ts +40 -0
  11. package/src/adapter/rule-summary.ts +79 -0
  12. package/src/cli/hook.ts +681 -0
  13. package/src/cli/main.ts +201 -0
  14. package/src/cli/session-command.ts +78 -0
  15. package/src/cli/session-state.ts +214 -0
  16. package/src/config/load.ts +85 -0
  17. package/src/config/merge.ts +20 -0
  18. package/src/config/schema.ts +69 -0
  19. package/src/dictionary/README.md +18 -0
  20. package/src/dictionary/data/pi-ste.json +200 -0
  21. package/src/dictionary/form.ts +6 -0
  22. package/src/dictionary/load.ts +54 -0
  23. package/src/dictionary/schema.ts +28 -0
  24. package/src/engine/comments.ts +386 -0
  25. package/src/engine/diff.ts +328 -0
  26. package/src/engine/identifiers.ts +20 -0
  27. package/src/engine/kinds.ts +43 -0
  28. package/src/engine/lint.ts +530 -0
  29. package/src/engine/markdown.ts +338 -0
  30. package/src/engine/paragraphs.ts +105 -0
  31. package/src/engine/rules/contraction.ts +19 -0
  32. package/src/engine/rules/dictionary.ts +281 -0
  33. package/src/engine/rules/hedging.ts +27 -0
  34. package/src/engine/rules/marketing.ts +71 -0
  35. package/src/engine/rules/paragraph-length.ts +23 -0
  36. package/src/engine/rules/phrasal-verb.ts +57 -0
  37. package/src/engine/rules/registry.ts +15 -0
  38. package/src/engine/rules/semicolon.ts +14 -0
  39. package/src/engine/rules/sentence-length.ts +24 -0
  40. package/src/engine/rules/verb-form.ts +76 -0
  41. package/src/engine/scan.ts +15 -0
  42. package/src/engine/sentences.ts +285 -0
  43. package/src/engine/tagger.ts +8 -0
  44. package/src/engine/tokens.ts +2 -0
  45. package/src/engine/types.ts +45 -0
  46. package/src/extension/index.ts +755 -0
  47. 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,6 @@
1
+ export const DICTIONARY_TOKEN_SOURCE = String.raw`[\p{L}\p{N}]+(?:['’\u2010\u2011-][\p{L}\p{N}]+)*`
2
+
3
+ export const DICTIONARY_FORM_PATTERN = new RegExp(
4
+ `^${DICTIONARY_TOKEN_SOURCE}(?:[\t ]+${DICTIONARY_TOKEN_SOURCE})*$`,
5
+ "u",
6
+ )
@@ -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
+ }