Implement XML data support

Example:

```
{{ with resources.Get "https://example.com/rss.xml" | transform.Unmarshal }}
    {{ range .channel.item }}
        <strong>{{ .title | plainify | htmlUnescape }}</strong><br />
        <p>{{ .description | plainify | htmlUnescape }}</p>
        {{ $link := .link | plainify | htmlUnescape }}
        <a href="{{ $link }}">{{ $link }}</a><br />
        <hr>
    {{ end }}
{{ end }}
```

Closes #4470
This commit is contained in:
Paul van Brouwershaven
2021-12-02 17:30:36 +01:00
committed by GitHub
parent 58adbeef88
commit 0eaaa8fee3
12 changed files with 167 additions and 12 deletions

View File

@@ -24,6 +24,7 @@ import (
"github.com/gohugoio/hugo/common/herrors"
"github.com/niklasfasching/go-org/org"
xml "github.com/clbanning/mxj/v2"
toml "github.com/pelletier/go-toml/v2"
"github.com/pkg/errors"
"github.com/spf13/afero"
@@ -135,6 +136,25 @@ func (d Decoder) UnmarshalTo(data []byte, f Format, v interface{}) error {
err = d.unmarshalORG(data, v)
case JSON:
err = json.Unmarshal(data, v)
case XML:
var xmlRoot xml.Map
xmlRoot, err = xml.NewMapXml(data)
var xmlValue map[string]interface{}
if err == nil {
xmlRootName, err := xmlRoot.Root()
if err != nil {
return toFileError(f, errors.Wrap(err, "failed to unmarshal XML"))
}
xmlValue = xmlRoot[xmlRootName].(map[string]interface{})
}
switch v := v.(type) {
case *map[string]interface{}:
*v = xmlValue
case *interface{}:
*v = xmlValue
}
case TOML:
err = toml.Unmarshal(data, v)
case YAML:

View File

@@ -20,6 +20,59 @@ import (
qt "github.com/frankban/quicktest"
)
func TestUnmarshalXML(t *testing.T) {
c := qt.New(t)
xmlDoc := `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0"
xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Example feed</title>
<link>https://example.com/</link>
<description>Example feed</description>
<generator>Hugo -- gohugo.io</generator>
<language>en-us</language>
<copyright>Example</copyright>
<lastBuildDate>Fri, 08 Jan 2021 14:44:10 +0000</lastBuildDate>
<atom:link href="https://example.com/feed.xml" rel="self" type="application/rss+xml"/>
<item>
<title>Example title</title>
<link>https://example.com/2021/11/30/example-title/</link>
<pubDate>Tue, 30 Nov 2021 15:00:00 +0000</pubDate>
<guid>https://example.com/2021/11/30/example-title/</guid>
<description>Example description</description>
</item>
</channel>
</rss>`
expect := map[string]interface{}{
"-atom": "http://www.w3.org/2005/Atom", "-version": "2.0",
"channel": map[string]interface{}{
"copyright": "Example",
"description": "Example feed",
"generator": "Hugo -- gohugo.io",
"item": map[string]interface{}{
"description": "Example description",
"guid": "https://example.com/2021/11/30/example-title/",
"link": "https://example.com/2021/11/30/example-title/",
"pubDate": "Tue, 30 Nov 2021 15:00:00 +0000",
"title": "Example title"},
"language": "en-us",
"lastBuildDate": "Fri, 08 Jan 2021 14:44:10 +0000",
"link": []interface{}{"https://example.com/", map[string]interface{}{
"-href": "https://example.com/feed.xml",
"-rel": "self",
"-type": "application/rss+xml"}},
"title": "Example feed",
}}
d := Default
m, err := d.Unmarshal([]byte(xmlDoc), XML)
c.Assert(err, qt.IsNil)
c.Assert(m, qt.DeepEquals, expect)
}
func TestUnmarshalToMap(t *testing.T) {
c := qt.New(t)
@@ -38,6 +91,7 @@ func TestUnmarshalToMap(t *testing.T) {
{"a: Easy!\nb:\n c: 2\n d: [3, 4]", YAML, map[string]interface{}{"a": "Easy!", "b": map[string]interface{}{"c": 2, "d": []interface{}{3, 4}}}},
{"a:\n true: 1\n false: 2", YAML, map[string]interface{}{"a": map[string]interface{}{"true": 1, "false": 2}}},
{`{ "a": "b" }`, JSON, expect},
{`<root><a>b</a></root>`, XML, expect},
{`#+a: b`, ORG, expect},
// errors
{`a = b`, TOML, false},
@@ -72,6 +126,7 @@ func TestUnmarshalToInterface(t *testing.T) {
{`#+DATE: <2020-06-26 Fri>`, ORG, map[string]interface{}{"date": "2020-06-26"}},
{`a = "b"`, TOML, expect},
{`a: "b"`, YAML, expect},
{`<root><a>b</a></root>`, XML, expect},
{`a,b,c`, CSV, [][]string{{"a", "b", "c"}}},
{"a: Easy!\nb:\n c: 2\n d: [3, 4]", YAML, map[string]interface{}{"a": "Easy!", "b": map[string]interface{}{"c": 2, "d": []interface{}{3, 4}}}},
// errors

View File

@@ -30,6 +30,7 @@ const (
TOML Format = "toml"
YAML Format = "yaml"
CSV Format = "csv"
XML Format = "xml"
)
// FormatFromString turns formatStr, typically a file extension without any ".",
@@ -51,6 +52,8 @@ func FormatFromString(formatStr string) Format {
return ORG
case "csv":
return CSV
case "xml":
return XML
}
return ""
@@ -68,27 +71,32 @@ func FormatFromMediaType(m media.Type) Format {
return ""
}
// FormatFromContentString tries to detect the format (JSON, YAML or TOML)
// FormatFromContentString tries to detect the format (JSON, YAML, TOML or XML)
// in the given string.
// It return an empty string if no format could be detected.
func (d Decoder) FormatFromContentString(data string) Format {
csvIdx := strings.IndexRune(data, d.Delimiter)
jsonIdx := strings.Index(data, "{")
yamlIdx := strings.Index(data, ":")
xmlIdx := strings.Index(data, "<")
tomlIdx := strings.Index(data, "=")
if isLowerIndexThan(csvIdx, jsonIdx, yamlIdx, tomlIdx) {
if isLowerIndexThan(csvIdx, jsonIdx, yamlIdx, xmlIdx, tomlIdx) {
return CSV
}
if isLowerIndexThan(jsonIdx, yamlIdx, tomlIdx) {
if isLowerIndexThan(jsonIdx, yamlIdx, xmlIdx, tomlIdx) {
return JSON
}
if isLowerIndexThan(yamlIdx, tomlIdx) {
if isLowerIndexThan(yamlIdx, xmlIdx, tomlIdx) {
return YAML
}
if isLowerIndexThan(xmlIdx, tomlIdx) {
return XML
}
if tomlIdx != -1 {
return TOML
}

View File

@@ -30,6 +30,7 @@ func TestFormatFromString(t *testing.T) {
{"json", JSON},
{"yaml", YAML},
{"yml", YAML},
{"xml", XML},
{"toml", TOML},
{"config.toml", TOML},
{"tOMl", TOML},
@@ -48,6 +49,7 @@ func TestFormatFromMediaType(t *testing.T) {
}{
{media.JSONType, JSON},
{media.YAMLType, YAML},
{media.XMLType, XML},
{media.TOMLType, TOML},
{media.CalendarType, ""},
} {
@@ -70,6 +72,7 @@ func TestFormatFromContentString(t *testing.T) {
{`foo:"bar"`, YAML},
{`{ "foo": "bar"`, JSON},
{`a,b,c"`, CSV},
{`<foo>bar</foo>"`, XML},
{`asdfasdf`, Format("")},
{``, Format("")},
} {