tpl/transform: Add template func for TOML/JSON/YAML docs examples conversion

Usage:

```html
{{ "title = \"Hello World\"" | transform.Remarshal "json" | safeHTML }}
```

Fixes #4389
This commit is contained in:
Bjørn Erik Pedersen 2018-02-09 09:21:46 +01:00
parent 2e95ec6844
commit d382502d6d
4 changed files with 265 additions and 0 deletions

View file

@ -465,3 +465,9 @@ func DiffStringSlices(slice1 []string, slice2 []string) []string {
return diffStr
}
// DiffString splits the strings into fields and runs it into DiffStringSlices.
// Useful for tests.
func DiffStrings(s1, s2 string) []string {
return DiffStringSlices(strings.Fields(s1), strings.Fields(s2))
}

View file

@ -88,6 +88,13 @@ func init() {
},
)
ns.AddMethodMapping(ctx.Remarshal,
nil,
[][2]string{
{`{{ "title = \"Hello World\"" | transform.Remarshal "json" | safeHTML }}`, "{\n \"title\": \"Hello World\"\n}\n"},
},
)
return ns
}

View file

@ -0,0 +1,98 @@
package transform
import (
"bytes"
"errors"
"strings"
"github.com/gohugoio/hugo/parser"
"github.com/spf13/cast"
)
// Remarshal is used in the Hugo documentation to convert configuration
// examples from YAML to JSON, TOML (and possibly the other way around).
// The is primarily a helper for the Hugo docs site.
// It is not a general purpose YAML to TOML converter etc., and may
// change without notice if it serves a purpose in the docs.
// Format is one of json, yaml or toml.
func (ns *Namespace) Remarshal(format string, data interface{}) (string, error) {
from, err := cast.ToStringE(data)
if err != nil {
return "", err
}
from = strings.TrimSpace(from)
format = strings.TrimSpace(strings.ToLower(format))
if from == "" {
return "", nil
}
mark, err := toFormatMark(format)
if err != nil {
return "", err
}
fromFormat, err := detectFormat(from)
if err != nil {
return "", err
}
var metaHandler func(d []byte) (map[string]interface{}, error)
switch fromFormat {
case "yaml":
metaHandler = parser.HandleYAMLMetaData
case "toml":
metaHandler = parser.HandleTOMLMetaData
case "json":
metaHandler = parser.HandleJSONMetaData
}
meta, err := metaHandler([]byte(from))
if err != nil {
return "", err
}
var result bytes.Buffer
if err := parser.InterfaceToConfig(meta, mark, &result); err != nil {
return "", err
}
return result.String(), nil
}
func toFormatMark(format string) (rune, error) {
// TODO(bep) the parser package needs a cleaning.
switch format {
case "yaml":
return rune(parser.YAMLLead[0]), nil
case "toml":
return rune(parser.TOMLLead[0]), nil
case "json":
return rune(parser.JSONLead[0]), nil
}
return 0, errors.New("failed to detect target data serialization format")
}
func detectFormat(data string) (string, error) {
jsonIdx := strings.Index(data, "{")
yamlIdx := strings.Index(data, ":")
tomlIdx := strings.Index(data, "=")
if jsonIdx != -1 && (yamlIdx == -1 || jsonIdx < yamlIdx) && (tomlIdx == -1 || jsonIdx < tomlIdx) {
return "json", nil
}
if yamlIdx != -1 && (tomlIdx == -1 || yamlIdx < tomlIdx) {
return "yaml", nil
}
if tomlIdx != -1 {
return "toml", nil
}
return "", errors.New("failed to detect data serialization format")
}

View file

@ -0,0 +1,154 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package transform
import (
"fmt"
"testing"
"github.com/gohugoio/hugo/helpers"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestRemarshal(t *testing.T) {
t.Parallel()
ns := New(newDeps(viper.New()))
assert := require.New(t)
tomlExample := `title = "Test Metadata"
[[resources]]
src = "**image-4.png"
title = "The Fourth Image!"
[resources.params]
byline = "picasso"
[[resources]]
name = "my-cool-image-:counter"
src = "**.png"
title = "TOML: The Image #:counter"
[resources.params]
byline = "bep"
`
yamlExample := `resources:
- params:
byline: picasso
src: '**image-4.png'
title: The Fourth Image!
- name: my-cool-image-:counter
params:
byline: bep
src: '**.png'
title: 'TOML: The Image #:counter'
title: Test Metadata
`
jsonExample := `{
"resources": [
{
"params": {
"byline": "picasso"
},
"src": "**image-4.png",
"title": "The Fourth Image!"
},
{
"name": "my-cool-image-:counter",
"params": {
"byline": "bep"
},
"src": "**.png",
"title": "TOML: The Image #:counter"
}
],
"title": "Test Metadata"
}
`
variants := []struct {
format string
data string
}{
{"yaml", yamlExample},
{"json", jsonExample},
{"toml", tomlExample},
{"TOML", tomlExample},
{"Toml", tomlExample},
{" TOML ", tomlExample},
}
for _, v1 := range variants {
for _, v2 := range variants {
// Both from and to may be the same here, but that is fine.
fromTo := fmt.Sprintf("%s => %s", v2.format, v1.format)
converted, err := ns.Remarshal(v1.format, v2.data)
assert.NoError(err, fromTo)
diff := helpers.DiffStrings(v1.data, converted)
if len(diff) > 0 {
t.Errorf("[%s] Expected \n%v\ngot\n%v\ndiff:\n%v", fromTo, v1.data, converted, diff)
}
}
}
}
func TestTestRemarshalError(t *testing.T) {
t.Parallel()
ns := New(newDeps(viper.New()))
assert := require.New(t)
_, err := ns.Remarshal("asdf", "asdf")
assert.Error(err)
_, err = ns.Remarshal("json", "asdf")
assert.Error(err)
}
func TestRemarshalDetectFormat(t *testing.T) {
t.Parallel()
assert := require.New(t)
for i, test := range []struct {
data string
expect interface{}
}{
{`foo = "bar"`, "toml"},
{` foo = "bar"`, "toml"},
{`foo="bar"`, "toml"},
{`foo: "bar"`, "yaml"},
{`foo:"bar"`, "yaml"},
{`{ "foo": "bar"`, "json"},
{`asdfasdf`, false},
{``, false},
} {
errMsg := fmt.Sprintf("[%d] %s", i, test.data)
result, err := detectFormat(test.data)
if b, ok := test.expect.(bool); ok && !b {
assert.Error(err, errMsg)
continue
}
assert.NoError(err, errMsg)
assert.Equal(test.expect, result, errMsg)
}
}