diff --git a/examples/data1.yaml b/examples/data1.yaml index 3ef8ec68..bfea295a 100644 --- a/examples/data1.yaml +++ b/examples/data1.yaml @@ -1,15 +1,8 @@ -strings: - - a: banana - - a: cat - - a: apple - -numbers: - - a: 12 - - a: 13 - - a: 120 - - -obj: - - a: {cat: "adog"} - - a: {cat: "doga"} - - a: apple +# above_cat +cat: # inline_cat + # above_array + array: # inline_array + - 3 # inline_3 + # above_4 + - 4 # inline_4 +# below_cat \ No newline at end of file diff --git a/pkg/yqlib/doc/usage/xml.md b/pkg/yqlib/doc/usage/xml.md index 17c19871..3edc6758 100644 --- a/pkg/yqlib/doc/usage/xml.md +++ b/pkg/yqlib/doc/usage/xml.md @@ -108,7 +108,8 @@ yq e -o=xml '.' sample.yml ``` will output ```xml -purrs``` +purrs +``` ## Encode xml: array Given a sample.yml file of: @@ -127,7 +128,8 @@ will output purrs meows -``` + +``` ## Encode xml: attributes Fields with the matching xml-attribute-prefix are assumed to be attributes. @@ -147,7 +149,8 @@ will output ```xml true -``` + +``` ## Encode xml: attributes with content Fields with the matching xml-content-name is assumed to be content. @@ -165,5 +168,33 @@ yq e -o=xml '.' sample.yml ``` will output ```xml -cool``` +cool +``` + +## Encode xml: comments +A best attempt is made to copy comments to xml. + +Given a sample.yml file of: +```yaml +# above_cat +cat: # inline_cat + # above_array + array: # inline_array + - val1 # inline_val1 + # above_val2 + - val2 # inline_val2 +# below_cat + +``` +then +```bash +yq e -o=xml '.' sample.yml +``` +will output +```xml + + val1 + val2 + +``` diff --git a/pkg/yqlib/encoder_properties.go b/pkg/yqlib/encoder_properties.go index 94de0cb5..8cae7ee1 100644 --- a/pkg/yqlib/encoder_properties.go +++ b/pkg/yqlib/encoder_properties.go @@ -3,7 +3,6 @@ package yqlib import ( "fmt" "io" - "strings" "github.com/magiconair/properties" yaml "gopkg.in/yaml.v3" @@ -30,9 +29,7 @@ func (pe *propertiesEncoder) Encode(node *yaml.Node) error { } func (pe *propertiesEncoder) doEncode(p *properties.Properties, node *yaml.Node, path string) error { - p.SetComment(path, - strings.Replace(node.HeadComment, "#", "", 1)+ - strings.Replace(node.LineComment, "#", "", 1)) + p.SetComment(path, headAndLineComment(node)) switch node.Kind { case yaml.ScalarNode: _, _, err := p.Set(path, node.Value) diff --git a/pkg/yqlib/encoder_xml.go b/pkg/yqlib/encoder_xml.go index f1cac841..255a1279 100644 --- a/pkg/yqlib/encoder_xml.go +++ b/pkg/yqlib/encoder_xml.go @@ -32,14 +32,20 @@ func (e *xmlEncoder) Encode(node *yaml.Node) error { if err != nil { return err } - var charData xml.CharData = []byte("\n") - err = e.xmlEncoder.EncodeToken(charData) + case yaml.DocumentNode: + err := e.encodeComment(headAndLineComment(node)) + if err != nil { + return err + } + + err = e.Encode(unwrapDoc(node)) + if err != nil { + return err + } + err = e.encodeComment(footComment(node)) if err != nil { return err } - return e.xmlEncoder.Flush() - case yaml.DocumentNode: - return e.Encode(unwrapDoc(node)) case yaml.ScalarNode: var charData xml.CharData = []byte(node.Value) err := e.xmlEncoder.EncodeToken(charData) @@ -47,8 +53,12 @@ func (e *xmlEncoder) Encode(node *yaml.Node) error { return err } return e.xmlEncoder.Flush() + default: + return fmt.Errorf("unsupported type %v", node.Tag) } - return fmt.Errorf("unsupported type %v", node.Tag) + var charData xml.CharData = []byte("\n") + return e.xmlEncoder.EncodeToken(charData) + } func (e *xmlEncoder) encodeTopLevelMap(node *yaml.Node) error { @@ -57,7 +67,16 @@ func (e *xmlEncoder) encodeTopLevelMap(node *yaml.Node) error { value := node.Content[i+1] start := xml.StartElement{Name: xml.Name{Local: key.Value}} - err := e.doEncode(value, start) + err := e.encodeComment(headAndLineComment(key)) + if err != nil { + return err + } + + err = e.doEncode(value, start) + if err != nil { + return err + } + err = e.encodeComment(footComment(key)) if err != nil { return err } @@ -65,6 +84,22 @@ func (e *xmlEncoder) encodeTopLevelMap(node *yaml.Node) error { return nil } +func (e *xmlEncoder) encodeStart(node *yaml.Node, start xml.StartElement) error { + err := e.xmlEncoder.EncodeToken(start) + if err != nil { + return err + } + return e.encodeComment(headAndLineComment(node)) +} + +func (e *xmlEncoder) encodeEnd(node *yaml.Node, start xml.StartElement) error { + err := e.xmlEncoder.EncodeToken(start.End()) + if err != nil { + return err + } + return e.encodeComment(footComment(node)) +} + func (e *xmlEncoder) doEncode(node *yaml.Node, start xml.StartElement) error { switch node.Kind { case yaml.MappingNode: @@ -72,22 +107,33 @@ func (e *xmlEncoder) doEncode(node *yaml.Node, start xml.StartElement) error { case yaml.SequenceNode: return e.encodeArray(node, start) case yaml.ScalarNode: - err := e.xmlEncoder.EncodeToken(start) + err := e.encodeStart(node, start) if err != nil { return err } var charData xml.CharData = []byte(node.Value) err = e.xmlEncoder.EncodeToken(charData) - if err != nil { return err } - return e.xmlEncoder.EncodeToken(start.End()) + + return e.encodeEnd(node, start) } return fmt.Errorf("unsupported type %v", node.Tag) } +func (e *xmlEncoder) encodeComment(commentStr string) error { + if commentStr != "" { + var comment xml.Comment = []byte(commentStr) + err := e.xmlEncoder.EncodeToken(comment) + if err != nil { + return err + } + } + return nil +} + func (e *xmlEncoder) encodeArray(node *yaml.Node, start xml.StartElement) error { for i := 0; i < len(node.Content); i++ { value := node.Content[i] @@ -116,7 +162,7 @@ func (e *xmlEncoder) encodeMap(node *yaml.Node, start xml.StartElement) error { } } - err := e.xmlEncoder.EncodeToken(start) + err := e.encodeStart(node, start) if err != nil { return err } @@ -126,6 +172,11 @@ func (e *xmlEncoder) encodeMap(node *yaml.Node, start xml.StartElement) error { key := node.Content[i] value := node.Content[i+1] + err := e.encodeComment(headAndLineComment(key)) + if err != nil { + return err + } + if !strings.HasPrefix(key.Value, e.attributePrefix) && key.Value != e.contentName { start := xml.StartElement{Name: xml.Name{Local: key.Value}} err := e.doEncode(value, start) @@ -140,7 +191,11 @@ func (e *xmlEncoder) encodeMap(node *yaml.Node, start xml.StartElement) error { return err } } + err = e.encodeComment(footComment(key)) + if err != nil { + return err + } } - return e.xmlEncoder.EncodeToken(start.End()) + return e.encodeEnd(node, start) } diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index 9524dddc..e1fb18b6 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -236,6 +236,15 @@ func createScalarNode(value interface{}, stringValue string) *yaml.Node { return node } +func headAndLineComment(node *yaml.Node) string { + return strings.Replace(node.HeadComment, "#", "", 1) + + strings.Replace(node.LineComment, "#", "", 1) +} + +func footComment(node *yaml.Node) string { + return strings.Replace(node.FootComment, "#", "", 1) +} + func createValueOperation(value interface{}, stringValue string) *Operation { var node *yaml.Node = createScalarNode(value, stringValue) diff --git a/pkg/yqlib/xml_test.go b/pkg/yqlib/xml_test.go index eb0c2c72..2eeecf06 100644 --- a/pkg/yqlib/xml_test.go +++ b/pkg/yqlib/xml_test.go @@ -52,6 +52,22 @@ type xmlScenario struct { encodeScenario bool } +var yamlWithComments = `# above_cat +cat: # inline_cat + # above_array + array: # inline_array + - val1 # inline_val1 + # above_val2 + - val2 # inline_val2 +# below_cat +` + +var expectedXmlWithComments = ` + val1 + val2 + +` + var xmlScenarios = []xmlScenario{ { description: "Parse xml: simple", @@ -79,33 +95,40 @@ var xmlScenarios = []xmlScenario{ { description: "Encode xml: simple", input: "cat: purrs", - expected: "purrs", + expected: "purrs\n", encodeScenario: true, }, { description: "Encode xml: array", input: "pets:\n cat:\n - purrs\n - meows", - expected: "\n purrs\n meows\n", + expected: "\n purrs\n meows\n\n", encodeScenario: true, }, { description: "Encode xml: attributes", subdescription: "Fields with the matching xml-attribute-prefix are assumed to be attributes.", input: "cat:\n +name: tiger\n meows: true\n", - expected: "\n true\n", + expected: "\n true\n\n", encodeScenario: true, }, { skipDoc: true, input: "cat:\n ++name: tiger\n meows: true\n", - expected: "\n true\n", + expected: "\n true\n\n", encodeScenario: true, }, { description: "Encode xml: attributes with content", subdescription: "Fields with the matching xml-content-name is assumed to be content.", input: "cat:\n +name: tiger\n +content: cool\n", - expected: "cool", + expected: "cool\n", + encodeScenario: true, + }, + { + description: "Encode xml: comments", + subdescription: "A best attempt is made to copy comments to xml.", + input: yamlWithComments, + expected: expectedXmlWithComments, encodeScenario: true, }, }