( rootNode: unknown, command: string, )
| 222 | * sync path's extractHeredocs({ quotedOnly: true }). |
| 223 | */ |
| 224 | export function extractQuoteContext( |
| 225 | rootNode: unknown, |
| 226 | command: string, |
| 227 | ): QuoteContext { |
| 228 | // Single walk collects all quote span types at once. |
| 229 | const spans: QuoteSpans = { raw: [], ansiC: [], double: [], heredoc: [] } |
| 230 | collectQuoteSpans(rootNode as TreeSitterNode, spans, false) |
| 231 | const singleQuoteSpans = spans.raw |
| 232 | const ansiCSpans = spans.ansiC |
| 233 | const doubleQuoteSpans = spans.double |
| 234 | const quotedHeredocSpans = spans.heredoc |
| 235 | const allQuoteSpans = [ |
| 236 | ...singleQuoteSpans, |
| 237 | ...ansiCSpans, |
| 238 | ...doubleQuoteSpans, |
| 239 | ...quotedHeredocSpans, |
| 240 | ] |
| 241 | |
| 242 | // Build a set of positions that should be excluded for each output variant. |
| 243 | // For withDoubleQuotes: remove single-quoted spans entirely, plus the |
| 244 | // opening/closing `"` delimiters of double-quoted spans (but keep the |
| 245 | // content between them). This matches the regex extractQuotedContent() |
| 246 | // semantics where `"` toggles quote state but content is still emitted. |
| 247 | const singleQuoteSet = buildPositionSet([ |
| 248 | ...singleQuoteSpans, |
| 249 | ...ansiCSpans, |
| 250 | ...quotedHeredocSpans, |
| 251 | ]) |
| 252 | const doubleQuoteDelimSet = new Set<number>() |
| 253 | for (const [start, end] of doubleQuoteSpans) { |
| 254 | doubleQuoteDelimSet.add(start) // opening " |
| 255 | doubleQuoteDelimSet.add(end - 1) // closing " |
| 256 | } |
| 257 | let withDoubleQuotes = '' |
| 258 | for (let i = 0; i < command.length; i++) { |
| 259 | if (singleQuoteSet.has(i)) continue |
| 260 | if (doubleQuoteDelimSet.has(i)) continue |
| 261 | withDoubleQuotes += command[i] |
| 262 | } |
| 263 | |
| 264 | // fullyUnquoted: remove all quoted content |
| 265 | const fullyUnquoted = removeSpans(command, allQuoteSpans) |
| 266 | |
| 267 | // unquotedKeepQuoteChars: remove content but keep delimiter chars |
| 268 | const spansWithQuoteChars: Array<[number, number, string, string]> = [] |
| 269 | for (const [start, end] of singleQuoteSpans) { |
| 270 | spansWithQuoteChars.push([start, end, "'", "'"]) |
| 271 | } |
| 272 | for (const [start, end] of ansiCSpans) { |
| 273 | // ansi_c_string spans include the leading $; preserve it so this |
| 274 | // matches the regex path, which treats $ as unquoted preceding '. |
| 275 | spansWithQuoteChars.push([start, end, "$'", "'"]) |
| 276 | } |
| 277 | for (const [start, end] of doubleQuoteSpans) { |
| 278 | spansWithQuoteChars.push([start, end, '"', '"']) |
| 279 | } |
| 280 | for (const [start, end] of quotedHeredocSpans) { |
| 281 | // Heredoc redirect spans have no inline quote delimiters — strip entirely. |
no test coverage detected