@@ -269,55 +269,124 @@ func extractXMLAttributes(xmlStr, elementName, attrName string) (string, error)
269269
270270// formatXMLPretty formats XML with indentation
271271func formatXMLPretty (xmlStr string ) (string , error ) {
272- var buf bytes.Buffer
273- encoder := json .NewEncoder (& buf )
274- encoder .SetIndent ("" , " " )
272+ // Use a simple but robust approach: tokenize and rebuild
273+ type token struct {
274+ typ string // "decl", "open", "close", "selfclose", "text", "comment"
275+ value string
276+ name string // for tags
277+ }
278+
279+ var tokens []token
280+ i := 0
281+ for i < len (xmlStr ) {
282+ if xmlStr [i ] == '<' {
283+ // Find end of tag
284+ end := i + 1
285+ for end < len (xmlStr ) && xmlStr [end ] != '>' {
286+ end ++
287+ }
288+ if end >= len (xmlStr ) {
289+ break
290+ }
291+ tagContent := xmlStr [i + 1 : end ]
292+ fullTag := xmlStr [i : end + 1 ]
293+
294+ if strings .HasPrefix (tagContent , "?" ) {
295+ tokens = append (tokens , token {typ : "decl" , value : fullTag })
296+ } else if strings .HasPrefix (tagContent , "!--" ) {
297+ tokens = append (tokens , token {typ : "comment" , value : fullTag })
298+ } else if strings .HasPrefix (tagContent , "/" ) {
299+ name := strings .TrimSpace (tagContent [1 :])
300+ tokens = append (tokens , token {typ : "close" , value : fullTag , name : name })
301+ } else if strings .HasSuffix (tagContent , "/" ) {
302+ name := strings .TrimSpace (strings .TrimSuffix (tagContent , "/" ))
303+ if idx := strings .IndexAny (name , " \t " ); idx != - 1 {
304+ name = name [:idx ]
305+ }
306+ tokens = append (tokens , token {typ : "selfclose" , value : fullTag , name : name })
307+ } else {
308+ name := strings .TrimSpace (tagContent )
309+ if idx := strings .IndexAny (name , " \t " ); idx != - 1 {
310+ name = name [:idx ]
311+ }
312+ tokens = append (tokens , token {typ : "open" , value : fullTag , name : name })
313+ }
314+ i = end + 1
315+ } else {
316+ // Text content
317+ start := i
318+ for i < len (xmlStr ) && xmlStr [i ] != '<' {
319+ i ++
320+ }
321+ text := strings .TrimSpace (xmlStr [start :i ])
322+ if text != "" {
323+ tokens = append (tokens , token {typ : "text" , value : text })
324+ }
325+ }
326+ }
275327
276- // Simple pretty printing - parse and re-serialize
277- // In production, use proper XML indentation
328+ // Rebuild with proper indentation
278329 var result strings.Builder
279330 indent := 0
280- inTag := false
331+ for j , tok := range tokens {
332+ switch tok .typ {
333+ case "decl" :
334+ result .WriteString (tok .value )
335+ result .WriteByte ('\n' )
336+ case "comment" :
337+ result .WriteString (strings .Repeat (" " , indent ))
338+ result .WriteString (tok .value )
339+ result .WriteByte ('\n' )
340+ case "open" :
341+ // Check if this is an inline element (text follows and then close)
342+ isInline := false
343+ if j + 2 < len (tokens ) && tokens [j + 1 ].typ == "text" && tokens [j + 2 ].typ == "close" && tokens [j + 2 ].name == tok .name {
344+ isInline = true
345+ }
281346
282- for i := 0 ; i < len (xmlStr ); i ++ {
283- ch := xmlStr [i ]
347+ if ! isInline && result .Len () > 0 && ! strings .HasSuffix (result .String (), "\n " ) {
348+ result .WriteByte ('\n' )
349+ }
350+ if ! isInline {
351+ result .WriteString (strings .Repeat (" " , indent ))
352+ }
353+ result .WriteString (tok .value )
354+ if ! isInline {
355+ indent ++
356+ }
357+ case "close" :
358+ // Check if previous was inline (open + text + close sequence)
359+ wasInline := j >= 2 && tokens [j - 2 ].typ == "open" && tokens [j - 2 ].name == tok .name && tokens [j - 1 ].typ == "text"
284360
285- switch ch {
286- case '<' :
287- if i + 1 < len (xmlStr ) && xmlStr [i + 1 ] == '/' {
288- // Closing tag
361+ if ! wasInline {
289362 indent --
290- if result .Len () > 0 && result .String ()[result .Len ()- 1 ] == '\n' {
291- result .WriteString (strings .Repeat (" " , indent ))
363+ if indent < 0 {
364+ indent = 0
365+ }
366+ if result .Len () > 0 && ! strings .HasSuffix (result .String (), "\n " ) {
367+ result .WriteByte ('\n' )
292368 }
369+ result .WriteString (strings .Repeat (" " , indent ))
293370 }
294- result .WriteByte (ch )
295- inTag = true
296- case '>' :
297- result .WriteByte (ch )
298- inTag = false
299- if i + 1 < len (xmlStr ) && xmlStr [i + 1 ] != '<' && xmlStr [i + 1 ] != ' ' && xmlStr [i + 1 ] != '\t' && xmlStr [i + 1 ] != '\n' {
300- // Content follows
301- } else if i + 1 < len (xmlStr ) && xmlStr [i + 1 ] == '<' && xmlStr [i + 2 ] != '/' {
302- // Opening tag follows
303- indent ++
371+ result .WriteString (tok .value )
372+ if j < len (tokens )- 1 {
304373 result .WriteByte ('\n' )
305- result . WriteString ( strings . Repeat ( " " , indent ))
306- } else if i + 1 < len ( xmlStr ) && xmlStr [ i + 1 ] == '<' && xmlStr [ i + 2 ] == '/' {
307- // Closing tag follows
374+ }
375+ case "selfclose" :
376+ if result . Len () > 0 && ! strings . HasSuffix ( result . String (), " \n " ) {
308377 result .WriteByte ('\n' )
309- result .WriteString (strings .Repeat (" " , indent ))
310378 }
311- case '\n' , '\t' :
312- if ! inTag {
313- // Skip whitespace outside tags
379+ result .WriteString (strings .Repeat (" " , indent ))
380+ result .WriteString (tok .value )
381+ if j < len (tokens )- 1 {
382+ result .WriteByte ('\n' )
314383 }
315- default :
316- result .WriteByte ( ch )
384+ case "text" :
385+ result .WriteString ( tok . value )
317386 }
318387 }
319388
320- return result .String (), nil
389+ return strings . TrimSpace ( result .String () ), nil
321390}
322391
323392// minifyXML removes whitespace from XML
0 commit comments