mirror of
https://github.com/mikefarah/yq.git
synced 2026-07-04 19:35:38 +00:00
wip
This commit is contained in:
parent
1338b521ff
commit
4e9d5e8e48
@ -15,11 +15,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type tomlDecoder struct {
|
type tomlDecoder struct {
|
||||||
parser toml.Parser
|
parser toml.Parser
|
||||||
finished bool
|
finished bool
|
||||||
d DataTreeNavigator
|
d DataTreeNavigator
|
||||||
rootMap *CandidateNode
|
rootMap *CandidateNode
|
||||||
fileBytes []byte
|
fileBytes []byte
|
||||||
|
firstKeyValue bool // Track if this is the first key-value for root comment
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTomlDecoder() Decoder {
|
func NewTomlDecoder() Decoder {
|
||||||
@ -42,6 +43,7 @@ func (dec *tomlDecoder) Init(reader io.Reader) error {
|
|||||||
Kind: MappingNode,
|
Kind: MappingNode,
|
||||||
Tag: "!!map",
|
Tag: "!!map",
|
||||||
}
|
}
|
||||||
|
dec.firstKeyValue = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,47 +70,54 @@ func (dec *tomlDecoder) extractLineComment(endPos int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// extractHeadComment extracts comments before a given start position
|
// extractHeadComment extracts comments before a given start position
|
||||||
// Only extracts comments from immediately preceding lines (no blank lines in between)
|
// Skips whitespace (including blank lines) first, then collects comments
|
||||||
func (dec *tomlDecoder) extractHeadComment(startPos int) string {
|
func (dec *tomlDecoder) extractHeadComment(startPos int) string {
|
||||||
src := dec.fileBytes
|
src := dec.fileBytes
|
||||||
var comments []string
|
var comments []string
|
||||||
|
|
||||||
// Start just before the token and go back to previous newline
|
// Start just before the token and skip trailing whitespace (including newlines)
|
||||||
i := startPos - 1
|
i := startPos - 1
|
||||||
for i >= 0 && src[i] != '\n' {
|
for i >= 0 && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
|
||||||
i--
|
i--
|
||||||
}
|
}
|
||||||
// Now i is at the newline before the current line, or -1 if at start
|
|
||||||
|
|
||||||
// Keep collecting comment lines going backwards
|
// Keep collecting comment lines going backwards
|
||||||
for i >= 0 {
|
for i >= 0 {
|
||||||
// Move to end of previous line
|
// Find line boundaries: go back to find start, then forward to find end
|
||||||
i-- // skip the newline
|
|
||||||
if i < 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the start of this line
|
|
||||||
lineEnd := i
|
lineEnd := i
|
||||||
|
// Find the end of this line
|
||||||
|
for lineEnd < len(src) && src[lineEnd] != '\n' {
|
||||||
|
lineEnd++
|
||||||
|
}
|
||||||
|
lineEnd-- // Back up from the newline
|
||||||
|
|
||||||
|
// Now find the start of this line
|
||||||
for i >= 0 && src[i] != '\n' {
|
for i >= 0 && src[i] != '\n' {
|
||||||
i--
|
i--
|
||||||
}
|
}
|
||||||
lineStart := i + 1
|
lineStart := i + 1
|
||||||
|
|
||||||
line := strings.TrimSpace(string(src[lineStart : lineEnd+1]))
|
line := strings.TrimRight(string(src[lineStart:lineEnd+1]), " \t\r")
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
|
||||||
// Empty line stops the comment block
|
// Empty line stops the comment block
|
||||||
if line == "" {
|
if trimmed == "" {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-comment line stops the comment block
|
// Non-comment line stops the comment block
|
||||||
if !strings.HasPrefix(line, "#") {
|
if !strings.HasPrefix(trimmed, "#") {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepend this comment line
|
// Prepend this comment line
|
||||||
comments = append([]string{line}, comments...)
|
comments = append([]string{trimmed}, comments...)
|
||||||
|
|
||||||
|
// Move to previous line (skip any whitespace/newlines)
|
||||||
|
i = lineStart - 1
|
||||||
|
for i >= 0 && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
|
||||||
|
i--
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(comments) > 0 {
|
if len(comments) > 0 {
|
||||||
@ -131,28 +140,37 @@ func (dec *tomlDecoder) getFullPath(tomlNode *toml.Node) []interface{} {
|
|||||||
func (dec *tomlDecoder) processKeyValueIntoMap(rootMap *CandidateNode, tomlNode *toml.Node) error {
|
func (dec *tomlDecoder) processKeyValueIntoMap(rootMap *CandidateNode, tomlNode *toml.Node) error {
|
||||||
value := tomlNode.Value()
|
value := tomlNode.Value()
|
||||||
path := dec.getFullPath(value.Next())
|
path := dec.getFullPath(value.Next())
|
||||||
log.Debug("processKeyValueIntoMap: %v", path)
|
|
||||||
|
|
||||||
valueNode, err := dec.decodeNode(value)
|
valueNode, err := dec.decodeNode(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract comments using the value's Raw range (more reliable than KeyValue node)
|
// Extract comments using the KeyValue node's start and value's end
|
||||||
startPos := int(value.Raw.Offset)
|
kvStartPos := int(tomlNode.Raw.Offset)
|
||||||
endPos := int(value.Raw.Offset + value.Raw.Length)
|
valueEndPos := int(value.Raw.Offset + value.Raw.Length)
|
||||||
|
|
||||||
|
log.Debug("processKeyValueIntoMap: kvStartPos=%d, valueEndPos=%d, firstKeyValue=%v", kvStartPos, valueEndPos, dec.firstKeyValue)
|
||||||
|
|
||||||
// HeadComment appears before the key-value line
|
// HeadComment appears before the key-value line
|
||||||
if startPos > 0 {
|
// Use kvStartPos + 1 to ensure we look before the key, not at position 0
|
||||||
if headComment := dec.extractHeadComment(startPos); headComment != "" {
|
headComment := dec.extractHeadComment(kvStartPos + 1)
|
||||||
|
log.Debug("processKeyValueIntoMap: extracted headComment: %q", headComment)
|
||||||
|
if headComment != "" {
|
||||||
|
// For the first key-value, attach head comment to root
|
||||||
|
if dec.firstKeyValue {
|
||||||
|
log.Debug("processKeyValueIntoMap: attaching head comment to root")
|
||||||
|
dec.rootMap.HeadComment = headComment
|
||||||
|
dec.firstKeyValue = false
|
||||||
|
} else {
|
||||||
valueNode.HeadComment = headComment
|
valueNode.HeadComment = headComment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// LineComment appears after the value on the same line
|
// LineComment appears after the value on the same line
|
||||||
if lineComment := dec.extractLineComment(endPos); lineComment != "" {
|
if lineComment := dec.extractLineComment(valueEndPos); lineComment != "" {
|
||||||
valueNode.LineComment = lineComment
|
valueNode.LineComment = lineComment
|
||||||
}
|
}
|
||||||
|
|
||||||
context := Context{}
|
context := Context{}
|
||||||
context = context.SingleChildContext(rootMap)
|
context = context.SingleChildContext(rootMap)
|
||||||
|
|
||||||
|
|||||||
@ -320,12 +320,71 @@ yq '.' sample.toml
|
|||||||
```
|
```
|
||||||
will output
|
will output
|
||||||
```yaml
|
```yaml
|
||||||
|
|
||||||
# This is a comment
|
# This is a comment
|
||||||
|
|
||||||
A = "hello" # inline comment
|
A = "hello" # inline comment
|
||||||
|
# This is a comment
|
||||||
B = 12
|
B = 12
|
||||||
|
|
||||||
# Table comment
|
|
||||||
[person]
|
[person]
|
||||||
|
# This is a comment
|
||||||
name = "Tom" # name comment
|
name = "Tom" # name comment
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Roundtrip: sample from web
|
||||||
|
Given a sample.toml file of:
|
||||||
|
```toml
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq '.' sample.toml
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
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.alpha]
|
||||||
|
ip = "10.0.0.1"
|
||||||
|
role = "frontend"
|
||||||
|
|
||||||
|
[servers.beta]
|
||||||
|
ip = "10.0.0.2"
|
||||||
|
role = "backend"
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
@ -104,6 +104,19 @@ func (te *tomlEncoder) formatScalar(node *CandidateNode) string {
|
|||||||
func (te *tomlEncoder) encodeRootMapping(w io.Writer, node *CandidateNode) error {
|
func (te *tomlEncoder) encodeRootMapping(w io.Writer, node *CandidateNode) error {
|
||||||
te.wroteRootAttr = false // Reset state
|
te.wroteRootAttr = false // Reset state
|
||||||
|
|
||||||
|
// Write root head comment if present
|
||||||
|
if node.HeadComment != "" {
|
||||||
|
if _, err := w.Write([]byte("\n")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := te.writeComment(w, node.HeadComment); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := w.Write([]byte("\n")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Preserve existing order by iterating Content
|
// Preserve existing order by iterating Content
|
||||||
for i := 0; i < len(node.Content); i += 2 {
|
for i := 0; i < len(node.Content); i += 2 {
|
||||||
keyNode := node.Content[i]
|
keyNode := node.Content[i]
|
||||||
|
|||||||
@ -225,6 +225,32 @@ B = 12
|
|||||||
name = "Tom" # name comment
|
name = "Tom" # name comment
|
||||||
`
|
`
|
||||||
|
|
||||||
|
var sampleFromWeb = `
|
||||||
|
# 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"
|
||||||
|
`
|
||||||
|
|
||||||
var tomlScenarios = []formatScenario{
|
var tomlScenarios = []formatScenario{
|
||||||
{
|
{
|
||||||
skipDoc: true,
|
skipDoc: true,
|
||||||
@ -503,6 +529,13 @@ var tomlScenarios = []formatScenario{
|
|||||||
expected: rtComments,
|
expected: rtComments,
|
||||||
scenarioType: "roundtrip",
|
scenarioType: "roundtrip",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "Roundtrip: sample from web",
|
||||||
|
input: sampleFromWeb,
|
||||||
|
expression: ".",
|
||||||
|
expected: sampleFromWeb,
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTomlScenario(t *testing.T, s formatScenario) {
|
func testTomlScenario(t *testing.T, s formatScenario) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user