diff --git a/pkg/yqlib/decoder_xml.go b/pkg/yqlib/decoder_xml.go index ada214ca..39e2b3ac 100644 --- a/pkg/yqlib/decoder_xml.go +++ b/pkg/yqlib/decoder_xml.go @@ -4,6 +4,7 @@ import ( "encoding/xml" "fmt" "io" + "strings" "unicode" "golang.org/x/net/html/charset" @@ -61,17 +62,19 @@ func (dec *xmlDecoder) createSequence(nodes []*xmlNode) (*yaml.Node, error) { } func (dec *xmlDecoder) createMap(n *xmlNode) (*yaml.Node, error) { - yamlNode := &yaml.Node{Kind: yaml.MappingNode, HeadComment: n.Comment} + log.Debug("createMap: headC: %v, footC: %v", n.HeadComment, n.FootComment) + yamlNode := &yaml.Node{Kind: yaml.MappingNode, HeadComment: n.HeadComment} if len(n.Data) > 0 { label := dec.contentPrefix yamlNode.Content = append(yamlNode.Content, createScalarNode(label, label), createScalarNode(n.Data, n.Data)) } - for _, keyValuePair := range n.Children { + for i, keyValuePair := range n.Children { label := keyValuePair.K children := keyValuePair.V labelNode := createScalarNode(label, label) + // labelNode.HeadComment = n.HeadComment var valueNode *yaml.Node var err error log.Debug("len of children in %v is %v", label, len(children)) @@ -81,10 +84,15 @@ func (dec *xmlDecoder) createMap(n *xmlNode) (*yaml.Node, error) { return nil, err } } else { + valueNode, err = dec.convertToYamlNode(children[0]) if err != nil { return nil, err } + + if i == len(n.Children)-1 { + valueNode.FootComment = n.FootComment + } } yamlNode.Content = append(yamlNode.Content, labelNode, valueNode) } @@ -97,7 +105,9 @@ func (dec *xmlDecoder) convertToYamlNode(n *xmlNode) (*yaml.Node, error) { return dec.createMap(n) } scalar := createScalarNode(n.Data, n.Data) - scalar.HeadComment = n.Comment + log.Debug("scalar headC: %v, footC: %v", n.HeadComment, n.FootComment) + scalar.LineComment = n.HeadComment + return scalar, nil } @@ -124,9 +134,10 @@ func (dec *xmlDecoder) Decode(rootYamlNode *yaml.Node) error { } type xmlNode struct { - Children []*xmlChildrenKv - Comment string - Data string + Children []*xmlChildrenKv + HeadComment string + FootComment string + Data string } type xmlChildrenKv struct { @@ -158,6 +169,7 @@ type element struct { parent *element n *xmlNode label string + state string } // this code is heavily based on https://github.com/basgys/goxml2json @@ -183,6 +195,8 @@ func (dec *xmlDecoder) decodeXml(root *xmlNode) error { switch se := t.(type) { case xml.StartElement: + log.Debug("start element %v", se.Name.Local) + elem.state = "started" // Build new a new current element and link it to its parent elem = &element{ parent: elem, @@ -198,6 +212,8 @@ func (dec *xmlDecoder) decodeXml(root *xmlNode) error { // Extract XML data (if any) elem.n.Data = trimNonGraphic(string(se)) case xml.EndElement: + log.Debug("end element %v", elem.label) + elem.state = "finished" // And add it to its parent list if elem.parent != nil { elem.parent.n.AddChild(elem.label, elem.n) @@ -206,13 +222,32 @@ func (dec *xmlDecoder) decodeXml(root *xmlNode) error { // Then change the current element to its parent elem = elem.parent case xml.Comment: - elem.n.Comment = trimNonGraphic(string(xml.CharData(se))) + + commentStr := trimNonGraphic(string(xml.CharData(se))) + if elem.state == "started" { + log.Debug("got a foot comment for %v: %v", elem.label, commentStr) + elem.n.FootComment = commentStr + } else { + log.Debug("got a head comment for %v: %v", elem.label, commentStr) + elem.n.HeadComment = joinFilter([]string{elem.n.HeadComment, commentStr}) + } + } } return nil } +func joinFilter(rawStrings []string) string { + stringsToJoin := make([]string, 0) + for _, str := range rawStrings { + if str != "" { + stringsToJoin = append(stringsToJoin, str) + } + } + return strings.Join(stringsToJoin, " ") +} + // trimNonGraphic returns a slice of the string s, with all leading and trailing // non graphic characters and spaces removed. // diff --git a/pkg/yqlib/doc/usage/xml.md b/pkg/yqlib/doc/usage/xml.md index 3edc6758..eee366c4 100644 --- a/pkg/yqlib/doc/usage/xml.md +++ b/pkg/yqlib/doc/usage/xml.md @@ -22,179 +22,3 @@ XML nodes that have attributes then plain content, e.g: The content of the node will be set as a field in the map with the key "+content". Use the `--xml-content-name` flag to change this. -## Parse xml: simple -Given a sample.xml file of: -```xml - -meow -``` -then -```bash -yq e -p=xml '.' sample.xml -``` -will output -```yaml -cat: meow -``` - -## Parse xml: array -Consecutive nodes with identical xml names are assumed to be arrays. - -Given a sample.xml file of: -```xml - -1 -2 -``` -then -```bash -yq e -p=xml '.' sample.xml -``` -will output -```yaml -animal: - - "1" - - "2" -``` - -## Parse xml: attributes -Attributes are converted to fields, with the attribute prefix. - -Given a sample.xml file of: -```xml - - - 7 - -``` -then -```bash -yq e -p=xml '.' sample.xml -``` -will output -```yaml -cat: - +legs: "4" - legs: "7" -``` - -## Parse xml: attributes with content -Content is added as a field, using the content name - -Given a sample.xml file of: -```xml - -meow -``` -then -```bash -yq e -p=xml '.' sample.xml -``` -will output -```yaml -cat: - +content: meow - +legs: "4" -``` - -## Encode xml: simple -Given a sample.yml file of: -```yaml -cat: purrs -``` -then -```bash -yq e -o=xml '.' sample.yml -``` -will output -```xml -purrs -``` - -## Encode xml: array -Given a sample.yml file of: -```yaml -pets: - cat: - - purrs - - meows -``` -then -```bash -yq e -o=xml '.' sample.yml -``` -will output -```xml - - purrs - meows - -``` - -## Encode xml: attributes -Fields with the matching xml-attribute-prefix are assumed to be attributes. - -Given a sample.yml file of: -```yaml -cat: - +name: tiger - meows: true - -``` -then -```bash -yq e -o=xml '.' sample.yml -``` -will output -```xml - - true - -``` - -## Encode xml: attributes with content -Fields with the matching xml-content-name is assumed to be content. - -Given a sample.yml file of: -```yaml -cat: - +name: tiger - +content: cool - -``` -then -```bash -yq e -o=xml '.' sample.yml -``` -will output -```xml -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/xml_test.go b/pkg/yqlib/xml_test.go index d9c0aaa3..e888a506 100644 --- a/pkg/yqlib/xml_test.go +++ b/pkg/yqlib/xml_test.go @@ -24,12 +24,18 @@ func decodeXml(t *testing.T, xml string) *CandidateNode { return &CandidateNode{Node: node} } -func yamlToXml(sampleYaml string, indent int) string { +func processScenario(s xmlScenario) string { var output bytes.Buffer writer := bufio.NewWriter(&output) - var encoder = NewXmlEncoder(writer, indent, "+", "+content") - inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0, NewYamlDecoder()) + var encoder = NewXmlEncoder(writer, 2, "+", "+content") + + var decoder = NewYamlDecoder() + if s.scenarioType == "roundtrip" { + decoder = NewXmlDecoder("+", "+content") + } + + inputs, err := readDocuments(strings.NewReader(s.input), "sample.yml", 0, decoder) if err != nil { panic(err) } @@ -49,10 +55,24 @@ type xmlScenario struct { description string subdescription string skipDoc bool - encodeScenario bool + scenarioType string } -var yamlWithComments = `need to fix leadingContent thing. This should fail.# above_cat +var expectedDecodeYamlWithComments = `D0, P[], (doc)::# before cat +cat: + # in cat + x: "3" # xca + # cool + # smart + y: + # befored + d: "4" # ind ind2 + # afterd + +# after cat +` + +var yamlWithComments = `# above_cat cat: # inline_cat # above_array array: # inline_array @@ -69,73 +89,85 @@ var expectedXmlWithComments = `34", + expected: expectedDecodeYamlWithComments, + scenarioType: "decode", }, + // { + // description: "Encode xml: simple", + // input: "cat: purrs", + // expected: "purrs\n", + // scenarioType: "encode", + // }, + // { + // description: "Encode xml: array", + // input: "pets:\n cat:\n - purrs\n - meows", + // expected: "\n purrs\n meows\n\n", + // scenarioType: "encode", + // }, + // { + // 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\n", + // scenarioType: "encode", + // }, + // { + // skipDoc: true, + // input: "cat:\n ++name: tiger\n meows: true\n", + // expected: "\n true\n\n", + // scenarioType: "encode", + // }, + // { + // 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\n", + // scenarioType: "encode", + // }, + // { + // description: "Encode xml: comments", + // subdescription: "A best attempt is made to copy comments to xml.", + // input: yamlWithComments, + // expected: expectedXmlWithComments, + // scenarioType: "encode", + // }, + // { + // skipDoc: true, + // input: "value", + // expected: "value", + // scenarioType: "roundtrip", + // }, } -func testXmlScenario(t *testing.T, s *xmlScenario) { - if s.encodeScenario { - test.AssertResultWithContext(t, s.expected, yamlToXml(s.input, 2), s.description) +func testXmlScenario(t *testing.T, s xmlScenario) { + if s.scenarioType == "encode" || s.scenarioType == "roundtrip" { + test.AssertResultWithContext(t, s.expected, processScenario(s), s.description) } else { var actual = resultToString(t, decodeXml(t, s.input)) test.AssertResultWithContext(t, s.expected, actual, s.description) @@ -148,7 +180,7 @@ func documentXmlScenario(t *testing.T, w *bufio.Writer, i interface{}) { if s.skipDoc { return } - if s.encodeScenario { + if s.scenarioType == "encode" { documentXmlEncodeScenario(w, s) } else { documentXmlDecodeScenario(t, w, s) @@ -200,12 +232,12 @@ func documentXmlEncodeScenario(w *bufio.Writer, s xmlScenario) { writeOrPanic(w, "```bash\nyq e -o=xml '.' sample.yml\n```\n") writeOrPanic(w, "will output\n") - writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", yamlToXml(s.input, 2))) + writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", processScenario(s))) } func TestXmlScenarios(t *testing.T) { for _, tt := range xmlScenarios { - testXmlScenario(t, &tt) + testXmlScenario(t, tt) } genericScenarios := make([]interface{}, len(xmlScenarios)) for i, s := range xmlScenarios {