@@ -19,6 +19,11 @@ type hclEncoder struct {
1919 prefs HclPreferences
2020}
2121
22+ // commentPathSep is used to join path segments when collecting comments.
23+ // It uses a rarely used ASCII control character to avoid collisions with
24+ // normal key names (including dots).
25+ const commentPathSep = "\x1e "
26+
2227// NewHclEncoder creates a new HCL encoder
2328func NewHclEncoder (prefs HclPreferences ) Encoder {
2429 return & hclEncoder {prefs : prefs }
@@ -84,7 +89,7 @@ func (he *hclEncoder) collectComments(node *CandidateNode, prefix string, commen
8489 if node .Kind == MappingNode {
8590 // Collect root-level head comment if at root (prefix is empty)
8691 if prefix == "" && node .HeadComment != "" {
87- commentMap [". head" ] = node .HeadComment
92+ commentMap [joinCommentPath ( "__root__" , " head") ] = node .HeadComment
8893 }
8994
9095 for i := 0 ; i < len (node .Content ); i += 2 {
@@ -93,21 +98,18 @@ func (he *hclEncoder) collectComments(node *CandidateNode, prefix string, commen
9398 key := keyNode .Value
9499
95100 // Create a path for this key
96- path := key
97- if prefix != "" {
98- path = prefix + "." + key
99- }
101+ path := joinCommentPath (prefix , key )
100102
101103 // Store comments from the key (head comments appear before the attribute)
102104 if keyNode .HeadComment != "" {
103- commentMap [path + ". head" ] = keyNode .HeadComment
105+ commentMap [joinCommentPath ( path , " head") ] = keyNode .HeadComment
104106 }
105107 // Store comments from the value (line comments appear after the value)
106108 if valueNode .LineComment != "" {
107- commentMap [path + ". line" ] = valueNode .LineComment
109+ commentMap [joinCommentPath ( path , " line") ] = valueNode .LineComment
108110 }
109111 if valueNode .FootComment != "" {
110- commentMap [path + ". foot" ] = valueNode .FootComment
112+ commentMap [joinCommentPath ( path , " foot") ] = valueNode .FootComment
111113 }
112114
113115 // Recurse into nested mappings
@@ -118,14 +120,22 @@ func (he *hclEncoder) collectComments(node *CandidateNode, prefix string, commen
118120 }
119121}
120122
123+ // joinCommentPath concatenates path segments using commentPathSep, safely handling empty prefixes.
124+ func joinCommentPath (prefix , segment string ) string {
125+ if prefix == "" {
126+ return segment
127+ }
128+ return prefix + commentPathSep + segment
129+ }
130+
121131// injectComments adds collected comments back into the HCL output
122132func (he * hclEncoder ) injectComments (output []byte , commentMap map [string ]string ) []byte {
123133 // Convert output to string for easier manipulation
124134 result := string (output )
125135
126- // Root-level head comment (stored as ". head" )
136+ // Root-level head comment (stored on the synthetic __root__/ head path )
127137 for path , comment := range commentMap {
128- if path == ". head" {
138+ if path == joinCommentPath ( "__root__" , " head") {
129139 trimmed := strings .TrimSpace (comment )
130140 if trimmed != "" && ! strings .HasPrefix (result , trimmed ) {
131141 result = trimmed + "\n " + result
@@ -135,13 +145,13 @@ func (he *hclEncoder) injectComments(output []byte, commentMap map[string]string
135145
136146 // Attribute head comments: insert above matching assignment
137147 for path , comment := range commentMap {
138- parts := strings .Split (path , "." )
139- if len (parts ) != 2 {
148+ parts := strings .Split (path , commentPathSep )
149+ if len (parts ) < 2 {
140150 continue
141151 }
142152
143- key := parts [0 ]
144- commentType := parts [1 ]
153+ commentType := parts [len ( parts ) - 1 ]
154+ key := parts [len ( parts ) - 2 ]
145155 if commentType != "head" || key == "" {
146156 continue
147157 }
@@ -271,7 +281,6 @@ func isHCLIdentifierPart(r rune) bool {
271281 return (r >= 'a' && r <= 'z' ) || (r >= 'A' && r <= 'Z' ) || (r >= '0' && r <= '9' ) || r == '_' || r == '-'
272282}
273283
274- // isValidHCLIdentifier checks if a string is a valid HCL identifier (unquoted)
275284func isValidHCLIdentifier (s string ) bool {
276285 if s == "" {
277286 return false
0 commit comments