Skip to content

Commit

Permalink
tpl/transform: Add template func for TOML/JSON/YAML docs examples con…
Browse files Browse the repository at this point in the history
…version

Usage:

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

Fixes gohugoio#4389
  • Loading branch information
bep committed Feb 10, 2018
1 parent f554503 commit f37d1ba
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 0 deletions.
6 changes: 6 additions & 0 deletions helpers/general.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
7 changes: 7 additions & 0 deletions tpl/transform/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

}
Expand Down
98 changes: 98 additions & 0 deletions tpl/transform/remarshal.go
Original file line number Diff line number Diff line change
@@ -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")

}
154 changes: 154 additions & 0 deletions tpl/transform/remarshal_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit f37d1ba

Please sign in to comment.