diff --git a/format/xml/testdata/test.svg.fqtest b/format/xml/testdata/test.svg.fqtest index b4b443c4..aecfc2d7 100644 --- a/format/xml/testdata/test.svg.fqtest +++ b/format/xml/testdata/test.svg.fqtest @@ -1,4 +1,4 @@ -$ fq . test.svg +$ fq -r '. as $a | ., (toxml({indent: 2}) | ., (fromxml | ., (diff($a; .) // "no diff")))' test.svg { "svg": { "-height": "2500", @@ -22,7 +22,35 @@ $ fq . test.svg } } } -$ fq -o array=true . test.svg + + + + +{ + "svg": { + "-height": "2500", + "-inkscape:version": "1.0 (4035a4f, 2020-05-01)", + "-sodipodi:docname": "ffclippy.svg", + "-version": "1.1", + "-viewBox": "0 0 192.756 192.756", + "-width": "2500", + "-xmlns": "http://www.w3.org/2000/svg", + "-xmlns:inkscape": "http://www.inkscape.org/namespaces/inkscape", + "-xmlns:sodipodi": "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd", + "-xmlns:svg": "http://www.w3.org/2000/svg", + "g": { + "-clip-rule": "evenodd", + "-fill-rule": "evenodd", + "-id": "g863" + }, + "sodipodi:namedview": { + "-pagecolor": "#ffffff", + "-showgrid": "false" + } + } +} +no diff +$ fq -r -o array=true '. as $a | ., (toxml({indent: 2}) | ., (fromxml | ., (diff($a; .) // "no diff")))' test.svg [ "svg", { @@ -57,3 +85,42 @@ $ fq -o array=true . test.svg ] ] ] + + + + +[ + "svg", + { + "height": "2500", + "inkscape:version": "1.0 (4035a4f, 2020-05-01)", + "sodipodi:docname": "ffclippy.svg", + "version": "1.1", + "viewBox": "0 0 192.756 192.756", + "width": "2500", + "xmlns": "http://www.w3.org/2000/svg", + "xmlns:inkscape": "http://www.inkscape.org/namespaces/inkscape", + "xmlns:sodipodi": "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd", + "xmlns:svg": "http://www.w3.org/2000/svg" + }, + [ + [ + "sodipodi:namedview", + { + "pagecolor": "#ffffff", + "showgrid": "false" + }, + [] + ], + [ + "g", + { + "clip-rule": "evenodd", + "fill-rule": "evenodd", + "id": "g863" + }, + [] + ] + ] +] +no diff diff --git a/format/xml/xml.go b/format/xml/xml.go index 85d2c4b2..05e47ad2 100644 --- a/format/xml/xml.go +++ b/format/xml/xml.go @@ -3,7 +3,8 @@ package xml // object mode inspired by https://www.xml.com/pub/a/2006/05/31/converting-between-xml-and-json.html // TODO: keep ? root #desc? -// TODO: xml default indent? +// TODO: refactor to share more code +// TODO: rewrite ns stack import ( "bytes" @@ -74,24 +75,26 @@ type xmlNS struct { url string } -func elmName(space, local string) string { - if space == "" { - return local - } - return space + ":" + local -} +// TODO: nss pop? attr not stack? // xmlNNStack is used to undo namespace url resolving, space is url not the "alias" name type xmlNNStack []xmlNS func (nss xmlNNStack) lookup(name xml.Name) string { + var s string for i := len(nss) - 1; i >= 0; i-- { ns := nss[i] if name.Space == ns.url { - return ns.name + // first found or is default namespace + if s == "" || ns.name == "" { + s = ns.name + } + if s == "" { + break + } } } - return "" + return s } func (nss xmlNNStack) push(name string, url string) xmlNNStack { @@ -100,57 +103,11 @@ func (nss xmlNNStack) push(name string, url string) xmlNNStack { return xmlNNStack(n) } -func fromXMLToArray(n xmlNode) any { - var f func(n xmlNode, nss xmlNNStack) []any - f = func(n xmlNode, nss xmlNNStack) []any { - attrs := map[string]any{} - for _, a := range n.Attrs { - local, space := a.Name.Local, a.Name.Space - name := local - if space != "" { - if space == "xmlns" { - nss = nss.push(local, a.Value) - } else { - space = nss.lookup(a.Name) - } - name = space + ":" + local - } else if local == "xmlns" { - // track default namespace - nss = nss.push("", a.Value) - } - attrs[name] = a.Value - } - if attrs["#text"] == nil && !whitespaceRE.Match(n.Chardata) { - attrs["#text"] = strings.TrimSpace(string(n.Chardata)) - } - if attrs["#comment"] == nil && !whitespaceRE.Match(n.Comment) { - attrs["#comment"] = strings.TrimSpace(string(n.Comment)) - } - - nodes := []any{} - for _, c := range n.Nodes { - nodes = append(nodes, f(c, nss)) - } - - local, space := n.XMLName.Local, n.XMLName.Space - if space != "" { - space = nss.lookup(n.XMLName) - } - name := elmName(space, local) - - elm := []any{name} - if len(attrs) > 0 { - elm = append(elm, attrs) - } else { - // make attrs null if there were none, jq allows index into null - elm = append(elm, nil) - } - elm = append(elm, nodes) - - return elm +func elmName(space, local string) string { + if space == "" { + return local } - - return f(n, nil) + return space + ":" + local } func fromXMLToObject(n xmlNode, xi format.XMLIn) any { @@ -160,19 +117,25 @@ func fromXMLToObject(n xmlNode, xi format.XMLIn) any { for _, a := range n.Attrs { local, space := a.Name.Local, a.Name.Space - name := local - if space != "" { - if space == "xmlns" { - nss = nss.push(local, a.Value) - } else { - space = nss.lookup(a.Name) - } - name = space + ":" + local + if space == "xmlns" { + nss = nss.push(local, a.Value) } else if local == "xmlns" { // track default namespace nss = nss.push("", a.Value) } + } + for _, a := range n.Attrs { + local, space := a.Name.Local, a.Name.Space + name := local + if space != "" { + if space == "xmlns" { + // nop + } else { + space = nss.lookup(a.Name) + } + name = elmName(space, local) + } attrs["-"+name] = a.Value } @@ -224,6 +187,64 @@ func fromXMLToObject(n xmlNode, xi format.XMLIn) any { return map[string]any{name: attrs} } +func fromXMLToArray(n xmlNode) any { + var f func(n xmlNode, nss xmlNNStack) []any + f = func(n xmlNode, nss xmlNNStack) []any { + attrs := map[string]any{} + + for _, a := range n.Attrs { + local, space := a.Name.Local, a.Name.Space + if space == "xmlns" { + nss = nss.push(local, a.Value) + } else if local == "xmlns" { + // track default namespace + nss = nss.push("", a.Value) + } + } + + for _, a := range n.Attrs { + local, space := a.Name.Local, a.Name.Space + name := local + if space != "" { + if space == "xmlns" { + // nop + } else { + space = nss.lookup(a.Name) + } + name = elmName(space, local) + } + attrs[name] = a.Value + } + + if attrs["#text"] == nil && !whitespaceRE.Match(n.Chardata) { + attrs["#text"] = strings.TrimSpace(string(n.Chardata)) + } + if attrs["#comment"] == nil && !whitespaceRE.Match(n.Comment) { + attrs["#comment"] = strings.TrimSpace(string(n.Comment)) + } + + nodes := []any{} + for _, c := range n.Nodes { + nodes = append(nodes, f(c, nss)) + } + + name := elmName(nss.lookup(n.XMLName), n.XMLName.Local) + + elm := []any{name} + if len(attrs) > 0 { + elm = append(elm, attrs) + } else { + // make attrs null if there were none, jq allows index into null + elm = append(elm, nil) + } + elm = append(elm, nodes) + + return elm + } + + return f(n, nil) +} + func decodeXML(d *decode.D, in any) any { xi, _ := in.(format.XMLIn) @@ -289,6 +310,20 @@ type ToXMLOpts struct { Indent int } +func xmlNameFromStr(s string) xml.Name { + return xml.Name{Local: s} +} + +func xmlNameSort(a, b xml.Name) bool { + if a.Space != b.Space { + if a.Space == "" { + return true + } + return a.Space < b.Space + } + return a.Local < b.Local +} + func toXMLFromObject(c any, opts ToXMLOpts) any { var f func(name string, content any) (xmlNode, int, bool) f = func(name string, content any) (xmlNode, int, bool) { @@ -319,10 +354,11 @@ func toXMLFromObject(c any, opts ToXMLOpts) any { n.Comment = []byte(s) case strings.HasPrefix(k, "-"): s, _ := v.(string) - n.Attrs = append(n.Attrs, xml.Attr{ - Name: xml.Name{Local: k[1:]}, + a := xml.Attr{ + Name: xmlNameFromStr(k[1:]), Value: s, - }) + } + n.Attrs = append(n.Attrs, a) default: switch v := v.(type) { case []any: @@ -360,8 +396,7 @@ func toXMLFromObject(c any, opts ToXMLOpts) any { } sort.Slice(n.Attrs, func(i, j int) bool { - a, b := n.Attrs[i].Name, n.Attrs[j].Name - return a.Space < b.Space || a.Local < b.Local + return xmlNameSort(n.Attrs[i].Name, n.Attrs[j].Name) }) return n, seq, hasSeq @@ -415,7 +450,7 @@ func toXMLFromArray(c any, opts ToXMLOpts) any { } n := xmlNode{ - XMLName: xml.Name{Local: name}, + XMLName: xmlNameFromStr(name), } for k, v := range attrs { @@ -429,15 +464,14 @@ func toXMLFromArray(c any, opts ToXMLOpts) any { default: s, _ := v.(string) n.Attrs = append(n.Attrs, xml.Attr{ - Name: xml.Name{Local: k}, + Name: xmlNameFromStr(k), Value: s, }) } } sort.Slice(n.Attrs, func(i, j int) bool { - a, b := n.Attrs[i].Name, n.Attrs[j].Name - return a.Space < b.Space || a.Local < b.Local + return xmlNameSort(n.Attrs[i].Name, n.Attrs[j].Name) }) for _, c := range children {