@@ -42,10 +42,26 @@ function Get-RawJsonBlock {
4242 return $null
4343 }
4444
45- $escapedIndent = [regex ]::Escape($startIndent )
46- $endIndex = -1
45+ # Use brace-depth tracking to find the closing brace
46+ $endIndex = -1
47+ $depth = 1 # We're starting inside the opening brace
4748 for ($i = ($startIndex + 1 ); $i -lt $JsonLines.Count ; $i ++ ) {
48- if ($JsonLines [$i ] -match " ^$escapedIndent \}" ) {
49+ $line = $JsonLines [$i ]
50+
51+ # Count braces in this line, ignoring those in strings
52+ $inString = $false
53+ $chars = $line.ToCharArray ()
54+ for ($k = 0 ; $k -lt $chars.Count ; $k ++ ) {
55+ if ($chars [$k ] -eq ' "' -and ($k -eq 0 -or $chars [$k - 1 ] -ne ' \' )) {
56+ $inString = -not $inString
57+ } elseif (-not $inString ) {
58+ if ($chars [$k ] -eq ' {' ) { $depth ++ }
59+ elseif ($chars [$k ] -eq ' }' ) { $depth -- }
60+ }
61+ }
62+
63+ # Found the closing brace of the item
64+ if ($depth -eq 0 ) {
4965 $endIndex = $i
5066 break
5167 }
@@ -121,34 +137,86 @@ function Add-LinkAttributeToJson {
121137 }
122138 if ($startIdx -eq -1 ) { continue }
123139
124- # Derive indentation: propIndent is one level deeper than the item start.
125- # Used to target only top-level properties and skip nested object braces.
126- $null = $lines [$startIdx ] -match ' ^(\s*)'
127- $propIndent = $matches [1 ] + ' '
128- $propIndentLen = $propIndent.Length
129- $escapedPropIndent = [regex ]::Escape($propIndent )
140+ # Derive indentation used by top-level properties in the item.
141+ # Prefer existing property indentation to avoid inheriting bad key indentation.
142+ $null = $lines [$startIdx ] -match ' ^(\s*)'
143+ $propIndent = $matches [1 ] + ' '
144+
145+ $depthProbe = 1
146+ for ($p = $startIdx + 1 ; $p -lt $lines.Count ; $p ++ ) {
147+ $probeLine = $lines [$p ]
148+
149+ if ($depthProbe -eq 1 -and $probeLine -match ' ^(\s*)"[^"]+"\s*:' ) {
150+ $propIndent = $matches [1 ]
151+ break
152+ }
153+
154+ $inStringProbe = $false
155+ $probeChars = $probeLine.ToCharArray ()
156+ for ($q = 0 ; $q -lt $probeChars.Count ; $q ++ ) {
157+ if ($probeChars [$q ] -eq ' "' -and ($q -eq 0 -or $probeChars [$q - 1 ] -ne ' \' )) {
158+ $inStringProbe = -not $inStringProbe
159+ } elseif (-not $inStringProbe ) {
160+ if ($probeChars [$q ] -eq ' {' ) { $depthProbe ++ }
161+ elseif ($probeChars [$q ] -eq ' }' ) { $depthProbe -- }
162+ }
163+ }
164+
165+ if ($depthProbe -eq 0 ) { break }
166+ }
130167
131- # Scan forward: update existing "link" or find the closing brace to insert one.
132- # Closing brace is matched by indent <= propIndentLen to handle inconsistent formatting.
133- $linkUpdated = $false
168+ # Scan forward: remove any existing "link" property and find the closing brace.
169+ # Use brace-depth tracking to properly handle nested structures like arrays.
134170 $closeBraceIdx = -1
171+ $depth = 1 # We're starting inside the opening brace of the item
172+ $linesToRemove = @ ()
173+
135174 for ($j = $startIdx + 1 ; $j -lt $lines.Count ; $j ++ ) {
136- if ($lines [$j ] -match " ^$escapedPropIndent `" link`" \s*:" ) {
137- $lines [$j ] = $lines [$j ] -replace ' "link"\s*:\s*"[^"]*"' , " `" link`" : `" $newLink `" "
138- $linkUpdated = $true
175+ $line = $lines [$j ]
176+
177+ # Check for existing "link" property at top-level (depth 1 before processing braces on this line)
178+ # Match at any indentation level (user may have manually changed indentation)
179+ if ($depth -eq 1 -and $line -match ' ^\s*"link"\s*:' ) {
180+ # Mark this line for removal
181+ $linesToRemove += $j
182+ }
183+
184+ # Count braces in this line, ignoring those in strings
185+ $inString = $false
186+ $chars = $line.ToCharArray ()
187+ for ($k = 0 ; $k -lt $chars.Count ; $k ++ ) {
188+ if ($chars [$k ] -eq ' "' -and ($k -eq 0 -or $chars [$k - 1 ] -ne ' \' )) {
189+ $inString = -not $inString
190+ } elseif (-not $inString ) {
191+ if ($chars [$k ] -eq ' {' ) { $depth ++ }
192+ elseif ($chars [$k ] -eq ' }' ) { $depth -- }
193+ }
194+ }
195+
196+ # Found the closing brace of the item
197+ if ($depth -eq 0 ) {
198+ $closeBraceIdx = $j
139199 break
140200 }
141- if ($lines [$j ] -match ' ^\s*\}' ) {
142- $null = $lines [$j ] -match ' ^(\s*)'
143- if ($matches [1 ].Length -le $propIndentLen ) {
144- $closeBraceIdx = $j
145- break
201+ }
202+
203+ # Remove old "link" lines in reverse order to preserve indices
204+ foreach ($idx in ($linesToRemove | Sort-Object - Descending)) {
205+ # If the line before had a trailing comma (from the link property), remove it
206+ if ($idx -gt $startIdx ) {
207+ $prevLine = $lines [$idx - 1 ]
208+ if ($prevLine -match ' ,\s*$' -and $lines [$idx ].Trim() -match ' ^}' ) {
209+ $lines [$idx - 1 ] = $prevLine -replace ' ,\s*$' , ' '
146210 }
147211 }
212+ $lines.RemoveAt ($idx )
213+ if ($idx -lt $closeBraceIdx ) {
214+ $closeBraceIdx --
215+ }
148216 }
149217
150- if ( -not $linkUpdated -and $closeBraceIdx -ne -1 ) {
151- # Insert "link" before the closing brace
218+ # Now insert "link" before the closing brace (consistent position for all items)
219+ if ( $closeBraceIdx -ne -1 ) {
152220 $prevPropIdx = $closeBraceIdx - 1
153221 while ($prevPropIdx -gt $startIdx -and $lines [$prevPropIdx ].Trim() -eq ' ' ) { $prevPropIdx -- }
154222
0 commit comments