mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-09 19:47:02 +02:00
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:
committed by
GitHub
parent
58adbeef88
commit
0eaaa8fee3
@@ -23,6 +23,8 @@ import (
|
||||
toml "github.com/pelletier/go-toml/v2"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
xml "github.com/clbanning/mxj/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -62,7 +64,14 @@ func InterfaceToConfig(in interface{}, format metadecoders.Format, w io.Writer)
|
||||
|
||||
_, err = w.Write([]byte{'\n'})
|
||||
return err
|
||||
case metadecoders.XML:
|
||||
b, err := xml.AnyXmlIndent(in, "", "\t", "root")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = w.Write(b)
|
||||
return err
|
||||
default:
|
||||
return errors.New("unsupported Format provided")
|
||||
}
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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("")},
|
||||
} {
|
||||
|
Reference in New Issue
Block a user