diff --git a/cmd/utils.go b/cmd/utils.go index 4cecccfc..5082b6e3 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -206,6 +206,7 @@ func configureEncoder() (yqlib.Encoder, error) { yqlib.ConfiguredYamlPreferences.ColorsEnabled = colorsEnabled yqlib.ConfiguredJSONPreferences.ColorsEnabled = colorsEnabled yqlib.ConfiguredHclPreferences.ColorsEnabled = colorsEnabled + yqlib.ConfiguredTomlPreferences.ColorsEnabled = colorsEnabled yqlib.ConfiguredYamlPreferences.PrintDocSeparators = !noDocSeparators diff --git a/examples/sample.toml b/examples/sample.toml index ea5c4f93..1376c81a 100644 --- a/examples/sample.toml +++ b/examples/sample.toml @@ -1,6 +1,26 @@ -[[fruits]] -[animals] -[[fruits.varieties]] # nested array of tables -name = "red delicious" \ No newline at end of file +# This is a TOML document + +title = "TOML Example" + +[owner] +name = "Tom Preston-Werner" +dob = 1979-05-27T07:32:00-08:00 + +[database] +enabled = true +ports = [ 8000, 8001, 8002 ] +data = [ ["delta", "phi"], [3.14] ] +temp_targets = { cpu = 79.5, case = 72.0 } + +[servers] + +[servers.alpha] +ip = "10.0.0.1" +role = "frontend" + +[servers.beta] +ip = "10.0.0.2" +role = "backend" + diff --git a/pkg/yqlib/encoder_toml.go b/pkg/yqlib/encoder_toml.go index 40605843..e7fc6a30 100644 --- a/pkg/yqlib/encoder_toml.go +++ b/pkg/yqlib/encoder_toml.go @@ -1,17 +1,25 @@ package yqlib import ( + "bytes" "fmt" "io" "strings" + + "github.com/fatih/color" ) type tomlEncoder struct { wroteRootAttr bool // Track if we wrote root-level attributes before tables + prefs TomlPreferences } func NewTomlEncoder() Encoder { - return &tomlEncoder{} + return NewTomlEncoderWithPrefs(ConfiguredTomlPreferences) +} + +func NewTomlEncoderWithPrefs(prefs TomlPreferences) Encoder { + return &tomlEncoder{prefs: prefs} } func (te *tomlEncoder) Encode(writer io.Writer, node *CandidateNode) error { @@ -23,8 +31,28 @@ func (te *tomlEncoder) Encode(writer io.Writer, node *CandidateNode) error { return fmt.Errorf("TOML encoder expects a mapping at the root level") } + // Encode to a buffer first if colors are enabled + var buf bytes.Buffer + var targetWriter io.Writer + if te.prefs.ColorsEnabled { + targetWriter = &buf + } else { + targetWriter = writer + } + // Encode a root mapping as a sequence of attributes, tables, and arrays of tables - return te.encodeRootMapping(writer, node) + if err := te.encodeRootMapping(targetWriter, node); err != nil { + return err + } + + // Apply colorization if enabled + if te.prefs.ColorsEnabled { + colorized := te.colorizeToml(buf.Bytes()) + _, err := writer.Write(colorized) + return err + } + + return nil } func (te *tomlEncoder) PrintDocumentSeparator(_ io.Writer) error { @@ -497,3 +525,130 @@ func (te *tomlEncoder) encodeMappingBodyWithPath(w io.Writer, path []string, m * } return nil } + +// colorizeToml applies syntax highlighting to TOML output using fatih/color +func (te *tomlEncoder) colorizeToml(input []byte) []byte { + toml := string(input) + result := strings.Builder{} + + // Force color output (don't check for TTY) + color.NoColor = false + + // Create color functions for different token types + commentColor := color.New(color.FgHiBlack).SprintFunc() + stringColor := color.New(color.FgGreen).SprintFunc() + numberColor := color.New(color.FgHiMagenta).SprintFunc() + keyColor := color.New(color.FgCyan).SprintFunc() + boolColor := color.New(color.FgHiMagenta).SprintFunc() + sectionColor := color.New(color.FgYellow, color.Bold).SprintFunc() + + // Simple tokenization for TOML coloring + i := 0 + for i < len(toml) { + ch := toml[i] + + // Comments - from # to end of line + if ch == '#' { + end := i + for end < len(toml) && toml[end] != '\n' { + end++ + } + result.WriteString(commentColor(toml[i:end])) + i = end + continue + } + + // Table sections - [section] or [[array]] + if ch == '[' { + end := i + 1 + // Check for [[ + if end < len(toml) && toml[end] == '[' { + end++ + } + // Find closing ] + for end < len(toml) && toml[end] != ']' { + end++ + } + // Include closing ] + if end < len(toml) { + end++ + // Check for ]] + if end < len(toml) && toml[end] == ']' { + end++ + } + } + result.WriteString(sectionColor(toml[i:end])) + i = end + continue + } + + // Strings - quoted text (double or single quotes) + if ch == '"' || ch == '\'' { + quote := ch + end := i + 1 + for end < len(toml) && toml[end] != quote { + if toml[end] == '\\' { + end++ // skip escaped char + } + end++ + } + if end < len(toml) { + end++ // include closing quote + } + result.WriteString(stringColor(toml[i:end])) + i = end + continue + } + + // Numbers - sequences of digits, possibly with decimal point or minus + if (ch >= '0' && ch <= '9') || (ch == '-' && i+1 < len(toml) && toml[i+1] >= '0' && toml[i+1] <= '9') { + end := i + if ch == '-' { + end++ + } + for end < len(toml) && ((toml[end] >= '0' && toml[end] <= '9') || toml[end] == '.' || toml[end] == 'e' || toml[end] == 'E' || toml[end] == '+' || toml[end] == '-') { + end++ + } + result.WriteString(numberColor(toml[i:end])) + i = end + continue + } + + // Identifiers/keys - alphanumeric + underscore + dash + if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' { + end := i + for end < len(toml) && ((toml[end] >= 'a' && toml[end] <= 'z') || + (toml[end] >= 'A' && toml[end] <= 'Z') || + (toml[end] >= '0' && toml[end] <= '9') || + toml[end] == '_' || toml[end] == '-') { + end++ + } + ident := toml[i:end] + + // Check if this is a boolean/null keyword + switch ident { + case "true", "false": + result.WriteString(boolColor(ident)) + default: + // Check if followed by = or whitespace then = (it's a key) + j := end + for j < len(toml) && (toml[j] == ' ' || toml[j] == '\t') { + j++ + } + if j < len(toml) && toml[j] == '=' { + result.WriteString(keyColor(ident)) + } else { + result.WriteString(ident) // plain text for other identifiers + } + } + i = end + continue + } + + // Everything else (whitespace, operators, brackets) - no color + result.WriteByte(ch) + i++ + } + + return []byte(result.String()) +} diff --git a/pkg/yqlib/format.go b/pkg/yqlib/format.go index 811047d5..d91f75e1 100644 --- a/pkg/yqlib/format.go +++ b/pkg/yqlib/format.go @@ -63,7 +63,7 @@ var ShFormat = &Format{"", nil, } var TomlFormat = &Format{"toml", []string{}, - func() Encoder { return NewTomlEncoder() }, + func() Encoder { return NewTomlEncoderWithPrefs(ConfiguredTomlPreferences) }, func() Decoder { return NewTomlDecoder() }, } diff --git a/pkg/yqlib/toml.go b/pkg/yqlib/toml.go new file mode 100644 index 00000000..4147954c --- /dev/null +++ b/pkg/yqlib/toml.go @@ -0,0 +1,15 @@ +package yqlib + +type TomlPreferences struct { + ColorsEnabled bool +} + +func NewDefaultTomlPreferences() TomlPreferences { + return TomlPreferences{ColorsEnabled: false} +} + +func (p *TomlPreferences) Copy() TomlPreferences { + return TomlPreferences{ColorsEnabled: p.ColorsEnabled} +} + +var ConfiguredTomlPreferences = NewDefaultTomlPreferences()