mirror of
https://github.com/mikefarah/yq.git
synced 2026-07-06 04:45:39 +00:00
wip - comments
This commit is contained in:
parent
4f72c37de7
commit
48707369a0
@ -43,6 +43,110 @@ type attributeWithName struct {
|
|||||||
Attr *hclsyntax.Attribute
|
Attr *hclsyntax.Attribute
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractLineComment extracts any inline comment after the given position
|
||||||
|
func extractLineComment(src []byte, endPos int) string {
|
||||||
|
// Look for # comment after the token
|
||||||
|
for i := endPos; i < len(src); i++ {
|
||||||
|
if src[i] == '#' {
|
||||||
|
// Found comment, extract until end of line
|
||||||
|
start := i
|
||||||
|
for i < len(src) && src[i] != '\n' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(src[start:i]))
|
||||||
|
}
|
||||||
|
if src[i] == '\n' {
|
||||||
|
// Hit newline before comment
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Skip whitespace and other characters
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractLeadingComments extracts comments from the very beginning of the file
|
||||||
|
func extractLeadingComments(src []byte) string {
|
||||||
|
var comments []string
|
||||||
|
i := 0
|
||||||
|
|
||||||
|
// Skip leading whitespace
|
||||||
|
for i < len(src) && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract comment lines from the start
|
||||||
|
for i < len(src) && src[i] == '#' {
|
||||||
|
lineStart := i
|
||||||
|
// Find end of line
|
||||||
|
for i < len(src) && src[i] != '\n' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
comments = append(comments, strings.TrimSpace(string(src[lineStart:i])))
|
||||||
|
// Skip newline
|
||||||
|
if i < len(src) && src[i] == '\n' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
// Skip whitespace between comment lines
|
||||||
|
for i < len(src) && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(comments) > 0 {
|
||||||
|
return strings.Join(comments, "\n")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractHeadComment extracts comments before a given start position
|
||||||
|
func extractHeadComment(src []byte, startPos int) string {
|
||||||
|
var comments []string
|
||||||
|
|
||||||
|
// Look backwards from startPos for comment lines
|
||||||
|
i := startPos - 1
|
||||||
|
|
||||||
|
// Skip whitespace backwards to find comment
|
||||||
|
for i >= 0 && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found a #, extract the comment
|
||||||
|
if i >= 0 && src[i] == '#' {
|
||||||
|
// Find the start of this line
|
||||||
|
lineStart := i
|
||||||
|
for lineStart > 0 && src[lineStart-1] != '\n' {
|
||||||
|
lineStart--
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract from line start to comment end
|
||||||
|
comments = append(comments, strings.TrimSpace(string(src[lineStart:i+1])))
|
||||||
|
|
||||||
|
// Look for more comment lines above this one
|
||||||
|
i = lineStart - 1
|
||||||
|
for i >= 0 && (src[i] == '\n' || src[i] == '\r') {
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
|
||||||
|
for i >= 0 && src[i] == '#' {
|
||||||
|
lineStart = i
|
||||||
|
for lineStart > 0 && src[lineStart-1] != '\n' {
|
||||||
|
lineStart--
|
||||||
|
}
|
||||||
|
comments = append([]string{strings.TrimSpace(string(src[lineStart : i+1]))}, comments...)
|
||||||
|
|
||||||
|
i = lineStart - 1
|
||||||
|
for i >= 0 && (src[i] == '\n' || src[i] == '\r') {
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(comments) > 0 {
|
||||||
|
return strings.Join(comments, "\n")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (dec *hclDecoder) Init(reader io.Reader) error {
|
func (dec *hclDecoder) Init(reader io.Reader) error {
|
||||||
data, err := io.ReadAll(reader)
|
data, err := io.ReadAll(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -71,11 +175,26 @@ func (dec *hclDecoder) Decode() (*CandidateNode, error) {
|
|||||||
|
|
||||||
root := &CandidateNode{Kind: MappingNode}
|
root := &CandidateNode{Kind: MappingNode}
|
||||||
|
|
||||||
|
// Extract file-level head comments (comments at the very beginning of the file)
|
||||||
|
if leadingComment := extractLeadingComments(dec.fileBytes); leadingComment != "" {
|
||||||
|
root.HeadComment = leadingComment
|
||||||
|
}
|
||||||
|
|
||||||
// process attributes in declaration order
|
// process attributes in declaration order
|
||||||
body := dec.file.Body.(*hclsyntax.Body)
|
body := dec.file.Body.(*hclsyntax.Body)
|
||||||
for _, attrWithName := range sortedAttributes(body.Attributes) {
|
for _, attrWithName := range sortedAttributes(body.Attributes) {
|
||||||
keyNode := createStringScalarNode(attrWithName.Name)
|
keyNode := createStringScalarNode(attrWithName.Name)
|
||||||
valNode := convertHclExprToNode(attrWithName.Attr.Expr, dec.fileBytes)
|
valNode := convertHclExprToNode(attrWithName.Attr.Expr, dec.fileBytes)
|
||||||
|
|
||||||
|
// Attach comments if any
|
||||||
|
attrRange := attrWithName.Attr.Range()
|
||||||
|
if headComment := extractHeadComment(dec.fileBytes, attrRange.Start.Byte); headComment != "" {
|
||||||
|
valNode.HeadComment = headComment
|
||||||
|
}
|
||||||
|
if lineComment := extractLineComment(dec.fileBytes, attrRange.End.Byte); lineComment != "" {
|
||||||
|
valNode.LineComment = lineComment
|
||||||
|
}
|
||||||
|
|
||||||
root.AddKeyValueChild(keyNode, valNode)
|
root.AddKeyValueChild(keyNode, valNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +213,16 @@ func hclBodyToNode(body *hclsyntax.Body, src []byte) *CandidateNode {
|
|||||||
for _, attrWithName := range sortedAttributes(body.Attributes) {
|
for _, attrWithName := range sortedAttributes(body.Attributes) {
|
||||||
key := createStringScalarNode(attrWithName.Name)
|
key := createStringScalarNode(attrWithName.Name)
|
||||||
val := convertHclExprToNode(attrWithName.Attr.Expr, src)
|
val := convertHclExprToNode(attrWithName.Attr.Expr, src)
|
||||||
|
|
||||||
|
// Attach comments if any
|
||||||
|
attrRange := attrWithName.Attr.Range()
|
||||||
|
if headComment := extractHeadComment(src, attrRange.Start.Byte); headComment != "" {
|
||||||
|
val.HeadComment = headComment
|
||||||
|
}
|
||||||
|
if lineComment := extractLineComment(src, attrRange.End.Byte); lineComment != "" {
|
||||||
|
val.LineComment = lineComment
|
||||||
|
}
|
||||||
|
|
||||||
node.AddKeyValueChild(key, val)
|
node.AddKeyValueChild(key, val)
|
||||||
}
|
}
|
||||||
for _, block := range body.Blocks {
|
for _, block := range body.Blocks {
|
||||||
|
|||||||
@ -39,6 +39,11 @@ func (he *hclEncoder) Encode(writer io.Writer, node *CandidateNode) error {
|
|||||||
|
|
||||||
f := hclwrite.NewEmptyFile()
|
f := hclwrite.NewEmptyFile()
|
||||||
body := f.Body()
|
body := f.Body()
|
||||||
|
|
||||||
|
// Collect comments as we encode
|
||||||
|
commentMap := make(map[string]string)
|
||||||
|
he.collectComments(node, "", commentMap)
|
||||||
|
|
||||||
if err := he.encodeNode(body, node); err != nil {
|
if err := he.encodeNode(body, node); err != nil {
|
||||||
return fmt.Errorf("failed to encode HCL: %w", err)
|
return fmt.Errorf("failed to encode HCL: %w", err)
|
||||||
}
|
}
|
||||||
@ -47,7 +52,10 @@ func (he *hclEncoder) Encode(writer io.Writer, node *CandidateNode) error {
|
|||||||
output := f.Bytes()
|
output := f.Bytes()
|
||||||
compactOutput := he.compactSpacing(output)
|
compactOutput := he.compactSpacing(output)
|
||||||
|
|
||||||
_, err := writer.Write(compactOutput)
|
// Inject comments back into the output
|
||||||
|
finalOutput := he.injectComments(compactOutput, commentMap)
|
||||||
|
|
||||||
|
_, err := writer.Write(finalOutput)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +66,76 @@ func (he *hclEncoder) compactSpacing(input []byte) []byte {
|
|||||||
return re.ReplaceAll(input, []byte("$1 ="))
|
return re.ReplaceAll(input, []byte("$1 ="))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// collectComments recursively collects comments from nodes for later injection
|
||||||
|
func (he *hclEncoder) collectComments(node *CandidateNode, prefix string, commentMap map[string]string) {
|
||||||
|
if node == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// For mapping nodes, collect comments from values
|
||||||
|
if node.Kind == MappingNode {
|
||||||
|
// Collect root-level head comment if at root (prefix is empty)
|
||||||
|
if prefix == "" && node.HeadComment != "" {
|
||||||
|
commentMap[".head"] = node.HeadComment
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(node.Content); i += 2 {
|
||||||
|
keyNode := node.Content[i]
|
||||||
|
valueNode := node.Content[i+1]
|
||||||
|
key := keyNode.Value
|
||||||
|
|
||||||
|
// Create a path for this key
|
||||||
|
path := key
|
||||||
|
if prefix != "" {
|
||||||
|
path = prefix + "." + key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store comments for this value
|
||||||
|
if valueNode.HeadComment != "" {
|
||||||
|
commentMap[path+".head"] = valueNode.HeadComment
|
||||||
|
}
|
||||||
|
if valueNode.LineComment != "" {
|
||||||
|
commentMap[path+".line"] = valueNode.LineComment
|
||||||
|
}
|
||||||
|
if valueNode.FootComment != "" {
|
||||||
|
commentMap[path+".foot"] = valueNode.FootComment
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse into nested mappings
|
||||||
|
if valueNode.Kind == MappingNode {
|
||||||
|
he.collectComments(valueNode, path, commentMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// injectComments adds collected comments back into the HCL output
|
||||||
|
func (he *hclEncoder) injectComments(output []byte, commentMap map[string]string) []byte {
|
||||||
|
// Convert output to string for easier manipulation
|
||||||
|
result := string(output)
|
||||||
|
|
||||||
|
// Look for head comments at the root level
|
||||||
|
// These are typically comments before the first attribute
|
||||||
|
for path, comment := range commentMap {
|
||||||
|
parts := strings.Split(path, ".")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
commentType := parts[len(parts)-1] // "head", "line", or "foot"
|
||||||
|
|
||||||
|
if commentType == "head" && len(parts) == 2 {
|
||||||
|
// Root-level head comment - inject at the beginning
|
||||||
|
// Check if comment not already there
|
||||||
|
if !strings.HasPrefix(result, strings.TrimSpace(comment)) {
|
||||||
|
result = comment + "\n" + result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(result)
|
||||||
|
}
|
||||||
|
|
||||||
// Helper runes for unquoted identifiers
|
// Helper runes for unquoted identifiers
|
||||||
func isHCLIdentifierStart(r rune) bool {
|
func isHCLIdentifierStart(r rune) bool {
|
||||||
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_'
|
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_'
|
||||||
|
|||||||
@ -238,6 +238,18 @@ var hclFormatScenarios = []formatScenario{
|
|||||||
expected: "name = \"app\"\nversion = 1\nenabled = true\n",
|
expected: "name = \"app\"\nversion = 1\nenabled = true\n",
|
||||||
scenarioType: "roundtrip",
|
scenarioType: "roundtrip",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "decode with comments",
|
||||||
|
input: "# Configuration\nport = 8080 # server port",
|
||||||
|
expected: "# Configuration\nport: 8080 # server port\n",
|
||||||
|
scenarioType: "decode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "roundtrip with comments",
|
||||||
|
input: "# Configuration\nport = 8080",
|
||||||
|
expected: "# Configuration\nport = 8080\n",
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func testHclScenario(t *testing.T, s formatScenario) {
|
func testHclScenario(t *testing.T, s formatScenario) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user