@@ -48,44 +48,93 @@ function fixQuotes(content: string, preferred: 'single' | 'double', filePath: st
4848 if ( ! isCodeFileExt ( filePath ) )
4949 return content
5050
51- // Mask all strings to avoid converting quotes inside other quote types
51+ // OPTIMIZATION: Single-pass quote conversion with position tracking
5252 const lines = content . split ( '\n' )
5353 const result : string [ ] = [ ]
5454
5555 for ( const line of lines ) {
56- let output = line
57-
58- if ( preferred === 'single' ) {
59- // Only convert double-quoted strings (not quotes inside single/template strings)
60- // Match standalone double-quoted strings
61- output = output . replace ( / " ( [ ^ " \\ ] | \\ .) * " / g, ( match ) => {
62- // Check if this quote is inside a single-quoted or template string
63- // Simple heuristic: count quotes before this position
64- const pos = output . indexOf ( match )
65- const before = output . slice ( 0 , pos )
66- const singleCount = ( before . match ( / ' / g) || [ ] ) . length
67- const templateCount = ( before . match ( / ` / g) || [ ] ) . length
68-
69- // If odd number of single quotes or backticks before, we're inside one
70- if ( singleCount % 2 === 1 || templateCount % 2 === 1 )
71- return match
72-
73- return convertDoubleToSingle ( match )
74- } )
56+ let output = ''
57+ let i = 0
58+ let inString : 'single' | 'double' | 'template' | null = null
59+ let escaped = false
60+ let stringStart = 0
61+
62+ while ( i < line . length ) {
63+ const ch = line [ i ]
64+
65+ if ( escaped ) {
66+ output += ch
67+ escaped = false
68+ i ++
69+ continue
70+ }
71+
72+ if ( ch === '\\' && inString ) {
73+ escaped = true
74+ output += ch
75+ i ++
76+ continue
77+ }
78+
79+ // Check for string boundaries
80+ if ( ! inString ) {
81+ if ( ch === '"' ) {
82+ inString = 'double'
83+ stringStart = i
84+ i ++
85+ continue
86+ }
87+ if ( ch === '\'' ) {
88+ inString = 'single'
89+ stringStart = i
90+ i ++
91+ continue
92+ }
93+ if ( ch === '`' ) {
94+ inString = 'template'
95+ output += ch
96+ i ++
97+ continue
98+ }
99+ output += ch
100+ i ++
101+ }
102+ else {
103+ // Inside a string - check if we're exiting
104+ if ( ( inString === 'double' && ch === '"' ) || ( inString === 'single' && ch === '\'' ) ) {
105+ // Found closing quote - convert if needed
106+ const stringContent = line . slice ( stringStart + 1 , i )
107+ if ( inString === 'double' && preferred === 'single' ) {
108+ // Convert double to single
109+ output += convertDoubleToSingle ( `"${ stringContent } "` )
110+ }
111+ else if ( inString === 'single' && preferred === 'double' ) {
112+ // Convert single to double
113+ output += convertSingleToDouble ( `'${ stringContent } '` )
114+ }
115+ else {
116+ // Keep as is
117+ output += line [ stringStart ] + stringContent + ch
118+ }
119+ inString = null
120+ i ++
121+ continue
122+ }
123+ if ( inString === 'template' && ch === '`' ) {
124+ // Template literal end
125+ output += ch
126+ inString = null
127+ i ++
128+ continue
129+ }
130+ // Still inside string, buffer it
131+ i ++
132+ }
75133 }
76- else {
77- // Only convert single-quoted strings
78- output = output . replace ( / ' ( [ ^ ' \\ ] | \\ .) * ' / g, ( match ) => {
79- const pos = output . indexOf ( match )
80- const before = output . slice ( 0 , pos )
81- const doubleCount = ( before . match ( / " / g) || [ ] ) . length
82- const templateCount = ( before . match ( / ` / g) || [ ] ) . length
83-
84- if ( doubleCount % 2 === 1 || templateCount % 2 === 1 )
85- return match
86-
87- return convertSingleToDouble ( match )
88- } )
134+
135+ // Handle unclosed strings (keep as is)
136+ if ( inString && inString !== 'template' ) {
137+ output += line . slice ( stringStart )
89138 }
90139
91140 result . push ( output )
@@ -147,15 +196,35 @@ export function formatCode(src: string, cfg: PickierConfig, filePath: string): s
147196 // Check for imports BEFORE any processing to ensure consistent final newline policy
148197 const _hadImports = / ^ \s * i m p o r t \b / m. test ( src )
149198
150- // normalize newlines and trim trailing whitespace per line if enabled
199+ // OPTIMIZATION: Normalize newlines and trim trailing whitespace in one pass
151200 const rawLines = src . replace ( / \r \n / g, '\n' ) . split ( '\n' )
152- const trimmed = cfg . format . trimTrailingWhitespace
153- ? rawLines . map ( l => l . replace ( / [ \t ] + $ / g, '' ) )
154- : rawLines . slice ( )
201+ let lines : string [ ]
202+
203+ if ( cfg . format . trimTrailingWhitespace ) {
204+ // Combine trimming and blank line collapsing in one pass
205+ lines = [ ]
206+ let blank = 0
207+ const maxConsecutive = Math . max ( 0 , cfg . format . maxConsecutiveBlankLines )
208+
209+ for ( const l of rawLines ) {
210+ const trimmed = l . replace ( / [ \t ] + $ / g, '' )
211+ if ( trimmed === '' ) {
212+ blank ++
213+ if ( blank <= maxConsecutive )
214+ lines . push ( '' )
215+ }
216+ else {
217+ blank = 0
218+ lines . push ( trimmed )
219+ }
220+ }
221+ }
222+ else {
223+ // Just collapse blank lines
224+ lines = collapseBlankLines ( rawLines , Math . max ( 0 , cfg . format . maxConsecutiveBlankLines ) )
225+ }
155226
156- // collapse blank lines
157- const collapsed = collapseBlankLines ( trimmed , Math . max ( 0 , cfg . format . maxConsecutiveBlankLines ) )
158- let joined = collapsed . join ( '\n' )
227+ let joined = lines . join ( '\n' )
159228 // Remove any leading blank lines at the top of the file
160229 joined = joined . replace ( / ^ (?: [ \t ] * \n ) + / , '' )
161230
@@ -170,15 +239,18 @@ export function formatCode(src: string, cfg: PickierConfig, filePath: string): s
170239 joined = sorted
171240 }
172241
173- // quotes first (independent of indentation)
174- joined = fixQuotes ( joined , cfg . format . quotes , filePath )
175- // indentation only for code files (ts/js)
242+ // OPTIMIZATION: Combine quote fixing, indentation, spacing, and semicolon removal for code files
176243 if ( isCodeFileExt ( filePath ) ) {
244+ joined = fixQuotes ( joined , cfg . format . quotes , filePath )
177245 joined = fixIndentation ( joined , cfg . format . indent , cfg )
178246 joined = normalizeCodeSpacing ( joined )
179247 if ( cfg . format . semi === true )
180248 joined = removeStylisticSemicolons ( joined )
181249 }
250+ else {
251+ // For non-code files, just do quotes
252+ joined = fixQuotes ( joined , cfg . format . quotes , filePath )
253+ }
182254
183255 // ensure final newline policy
184256 if ( cfg . format . finalNewline === 'none' ) {
0 commit comments