Skip to content

Commit 06a425c

Browse files
committed
Merge branch 'pr-31'
2 parents fb786bf + 96bf986 commit 06a425c

File tree

4 files changed

+54
-625
lines changed

4 files changed

+54
-625
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
)
99

1010
require (
11+
github.com/adhocore/jsonc v0.10.0 // indirect
1112
github.com/inconshreveable/mousetrap v1.1.0 // indirect
1213
github.com/spf13/pflag v1.0.6 // indirect
1314
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/adhocore/jsonc v0.10.0 h1:YjNX9TojBfxQJ4kuoiNqVR5SFqu1YBEMsm+HxWnxbOI=
2+
github.com/adhocore/jsonc v0.10.0/go.mod h1:Ar4gd3i83+1Z+5M5SG6Vrfw9q3TO544OwLXH4+ZhWTE=
13
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
24
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
35
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=

pkg/config/json5.go

Lines changed: 8 additions & 358 deletions
Original file line numberDiff line numberDiff line change
@@ -3,374 +3,24 @@ package config
33
import (
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
18411
func 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
20218
func 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

Comments
 (0)