@@ -3,374 +3,24 @@ package config
33import (
44 "encoding/json"
55 "fmt"
6- "regexp"
7- "strings"
8- )
9-
10- // JSON5Preprocessor handles JSON5 syntax and converts it to valid JSON
11- type JSON5Preprocessor struct {}
12-
13- // NewJSON5Preprocessor creates a new JSON5 preprocessor
14- func NewJSON5Preprocessor () * JSON5Preprocessor {
15- return & JSON5Preprocessor {}
16- }
17-
18- // Process converts JSON5 syntax to valid JSON
19- func (p * JSON5Preprocessor ) Process (input []byte ) ([]byte , error ) {
20- content := string (input )
21-
22- // Step 1: Remove comments
23- content = p .removeComments (content )
24-
25- // Step 2: Quote unquoted keys
26- content = p .quoteKeys (content )
27-
28- // Step 3: Handle trailing commas
29- content = p .removeTrailingCommas (content )
30-
31- // Step 4: Validate the result is valid JSON
32- var test interface {}
33- if err := json .Unmarshal ([]byte (content ), & test ); err != nil {
34- return nil , fmt .Errorf ("preprocessed JSON5 is not valid JSON: %w" , err )
35- }
36-
37- return []byte (content ), nil
38- }
39-
40- // removeComments removes both single-line (//) and multi-line (/* */) comments
41- func (p * JSON5Preprocessor ) removeComments (content string ) string {
42- var result strings.Builder
43- lines := strings .Split (content , "\n " )
44-
45- inMultiLineComment := false
46-
47- for _ , line := range lines {
48- processedLine := p .processLineComments (line , & inMultiLineComment )
49- if processedLine != "" || ! inMultiLineComment {
50- result .WriteString (processedLine )
51- result .WriteString ("\n " )
52- }
53- }
54-
55- return result .String ()
56- }
57-
58- // processLineComments handles comment removal for a single line
59- func (p * JSON5Preprocessor ) processLineComments (line string , inMultiLineComment * bool ) string {
60- var result strings.Builder
61- inString := false
62- escaped := false
63- i := 0
64-
65- for i < len (line ) {
66- char := line [i ]
67-
68- // Handle escape sequences in strings
69- if escaped {
70- result .WriteByte (char )
71- escaped = false
72- i ++
73- continue
74- }
75-
76- // Handle string boundaries
77- if char == '"' && ! * inMultiLineComment {
78- inString = ! inString
79- result .WriteByte (char )
80- i ++
81- continue
82- }
83-
84- // Handle escape character
85- if char == '\\' && inString {
86- escaped = true
87- result .WriteByte (char )
88- i ++
89- continue
90- }
91-
92- // Skip processing if we're inside a string
93- if inString {
94- result .WriteByte (char )
95- i ++
96- continue
97- }
986
99- // Handle multi-line comment start
100- if ! * inMultiLineComment && i < len (line )- 1 && line [i :i + 2 ] == "/*" {
101- * inMultiLineComment = true
102- i += 2
103- continue
104- }
105-
106- // Handle multi-line comment end
107- if * inMultiLineComment && i < len (line )- 1 && line [i :i + 2 ] == "*/" {
108- * inMultiLineComment = false
109- i += 2
110- continue
111- }
112-
113- // Skip characters inside multi-line comments
114- if * inMultiLineComment {
115- i ++
116- continue
117- }
118-
119- // Handle single-line comment
120- if i < len (line )- 1 && line [i :i + 2 ] == "//" {
121- // Rest of line is a comment
122- break
123- }
124-
125- // Regular character
126- result .WriteByte (char )
127- i ++
128- }
129-
130- return strings .TrimSpace (result .String ())
131- }
132-
133- // quoteKeys adds quotes around unquoted object keys
134- func (p * JSON5Preprocessor ) quoteKeys (content string ) string {
135- // Regex to match unquoted keys: word characters followed by colon
136- // This is a simplified approach - a full parser would be more robust
137- keyRegex := regexp .MustCompile (`(\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:` )
138-
139- return keyRegex .ReplaceAllStringFunc (content , func (match string ) string {
140- // Extract the key and surrounding whitespace
141- parts := keyRegex .FindStringSubmatch (match )
142- if len (parts ) >= 3 {
143- whitespace := parts [1 ]
144- key := parts [2 ]
145-
146- // Don't quote if it's already quoted or if it's a reserved word in a string context
147- if p .isReservedWord (key ) {
148- return match // Keep as-is for true, false, null
149- }
150-
151- return fmt .Sprintf (`%s"%s":` , whitespace , key )
152- }
153- return match
154- })
155- }
156-
157- // isReservedWord checks if a word should not be quoted (true, false, null)
158- func (p * JSON5Preprocessor ) isReservedWord (word string ) bool {
159- reserved := map [string ]bool {
160- "true" : true ,
161- "false" : true ,
162- "null" : true ,
163- }
164- return reserved [word ]
165- }
166-
167- // removeTrailingCommas removes trailing commas before closing brackets/braces
168- func (p * JSON5Preprocessor ) removeTrailingCommas (content string ) string {
169- // Remove trailing commas before closing braces or brackets
170- // This handles cases like: { "key": "value", } or [ "item", ]
171-
172- // Remove comma before closing brace
173- braceRegex := regexp .MustCompile (`,(\s*})` )
174- content = braceRegex .ReplaceAllString (content , `$1` )
175-
176- // Remove comma before closing bracket
177- bracketRegex := regexp .MustCompile (`,(\s*])` )
178- content = bracketRegex .ReplaceAllString (content , `$1` )
179-
180- return content
181- }
7+ "github.com/adhocore/jsonc"
8+ )
1829
18310// ParseJSON5 parses JSON5 content and unmarshals it into the target
18411func ParseJSON5 (data []byte , target interface {}) error {
185- preprocessor := NewJSON5Preprocessor ()
186-
187- // Preprocess JSON5 to valid JSON
188- processedData , err := preprocessor .Process (data )
189- if err != nil {
190- return fmt .Errorf ("failed to preprocess JSON5: %w" , err )
191- }
192-
193- // Parse as regular JSON
194- if err := json .Unmarshal (processedData , target ); err != nil {
195- return fmt .Errorf ("failed to parse JSON: %w" , err )
196- }
197-
198- return nil
12+ // Use jsonc library for native JSON5 support (supports comments, trailing commas, unquoted keys, etc.)
13+ j := jsonc .New ()
14+ return j .Unmarshal (data , target )
19915}
20016
20117// FormatAsJSON5 formats a configuration struct as JSON5 with proper formatting
20218func FormatAsJSON5 (cfg * Config ) (string , error ) {
203- // First marshal to JSON to get the structure
19+ // For now, just use regular JSON formatting
20+ // TODO: Add proper JSON5 formatting with comments
20421 jsonData , err := json .MarshalIndent (cfg , "" , " " )
20522 if err != nil {
20623 return "" , fmt .Errorf ("failed to marshal config to JSON: %w" , err )
20724 }
208-
209- // Add header comment
210- result := "{\n "
211- result += " // mvx project configuration\n "
212- result += " // See https://github.com/gnodet/mvx for documentation\n \n "
213-
214- // Parse the JSON structure and format as JSON5
215- var data map [string ]interface {}
216- if err := json .Unmarshal (jsonData , & data ); err != nil {
217- return "" , fmt .Errorf ("failed to unmarshal JSON: %w" , err )
218- }
219-
220- // Format each section
221- sections := []string {"project" , "tools" , "environment" , "commands" }
222- first := true
223-
224- for _ , section := range sections {
225- if value , exists := data [section ]; exists && value != nil {
226- if ! first {
227- result += ",\n \n "
228- }
229- first = false
230-
231- switch section {
232- case "project" :
233- result += " // Project metadata\n "
234- result += formatProjectSection (value )
235- case "tools" :
236- result += " // Tool versions and configurations\n "
237- result += formatToolsSection (value )
238- case "environment" :
239- result += " // Environment variables\n "
240- result += formatEnvironmentSection (value )
241- case "commands" :
242- result += " // Custom commands\n "
243- result += formatCommandsSection (value )
244- }
245- }
246- }
247-
248- result += "\n }\n "
249- return result , nil
250- }
251-
252- // formatProjectSection formats the project section
253- func formatProjectSection (value interface {}) string {
254- project , ok := value .(map [string ]interface {})
255- if ! ok {
256- return ""
257- }
258-
259- result := " project: {\n "
260- if name , exists := project ["name" ]; exists && name != nil {
261- result += fmt .Sprintf (" name: %q,\n " , name )
262- }
263- if desc , exists := project ["description" ]; exists && desc != nil {
264- result += fmt .Sprintf (" description: %q\n " , desc )
265- }
266- result += " }"
267- return result
268- }
269-
270- // formatToolsSection formats the tools section
271- func formatToolsSection (value interface {}) string {
272- tools , ok := value .(map [string ]interface {})
273- if ! ok {
274- return ""
275- }
276-
277- result := " tools: {\n "
278- first := true
279-
280- for toolName , toolData := range tools {
281- if ! first {
282- result += ",\n "
283- }
284- first = false
285-
286- if toolConfig , ok := toolData .(map [string ]interface {}); ok {
287- result += fmt .Sprintf (" %s: {\n " , toolName )
288-
289- if version , exists := toolConfig ["version" ]; exists && version != nil {
290- result += fmt .Sprintf (" version: %q" , version )
291- }
292-
293- if dist , exists := toolConfig ["distribution" ]; exists && dist != nil && dist != "" {
294- result += fmt .Sprintf (",\n distribution: %q" , dist )
295- }
296-
297- result += "\n }"
298- }
299- }
300-
301- result += "\n }"
302- return result
303- }
304-
305- // formatEnvironmentSection formats the environment section
306- func formatEnvironmentSection (value interface {}) string {
307- env , ok := value .(map [string ]interface {})
308- if ! ok {
309- return ""
310- }
311-
312- result := " environment: {\n "
313- first := true
314-
315- for key , val := range env {
316- if ! first {
317- result += ",\n "
318- }
319- first = false
320- result += fmt .Sprintf (" %s: %q" , key , val )
321- }
322-
323- result += "\n }"
324- return result
325- }
326-
327- // formatCommandsSection formats the commands section
328- func formatCommandsSection (value interface {}) string {
329- commands , ok := value .(map [string ]interface {})
330- if ! ok {
331- return ""
332- }
333-
334- result := " commands: {\n "
335- first := true
336-
337- for cmdName , cmdData := range commands {
338- if ! first {
339- result += ",\n \n "
340- }
341- first = false
342-
343- if cmdConfig , ok := cmdData .(map [string ]interface {}); ok {
344- // Quote command names that contain special characters
345- quotedName := cmdName
346- if strings .ContainsAny (cmdName , "-. " ) {
347- quotedName = fmt .Sprintf ("%q" , cmdName )
348- }
349- result += fmt .Sprintf (" %s: {\n " , quotedName )
350-
351- if desc , exists := cmdConfig ["description" ]; exists && desc != nil && desc != "" {
352- result += fmt .Sprintf (" description: %q,\n " , desc )
353- }
354-
355- if script , exists := cmdConfig ["script" ]; exists && script != nil && script != "" {
356- result += fmt .Sprintf (" script: %q" , script )
357- }
358-
359- // Add other fields if they exist
360- if wd , exists := cmdConfig ["working_dir" ]; exists && wd != nil && wd != "" {
361- result += fmt .Sprintf (",\n working_dir: %q" , wd )
362- }
363-
364- if override , exists := cmdConfig ["override" ]; exists && override != nil {
365- if overrideBool , ok := override .(bool ); ok && overrideBool {
366- result += ",\n override: true"
367- }
368- }
369-
370- result += "\n }"
371- }
372- }
373-
374- result += "\n }"
375- return result
25+ return string (jsonData ), nil
37626}
0 commit comments