tpl/transform: Add template func for TOML/JSON/YAML docs examples conversion
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 9 Feb 2018 08:21:46 +0000 (09:21 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 10 Feb 2018 11:36:31 +0000 (12:36 +0100)
Usage:

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

Fixes #4389

helpers/general.go
tpl/transform/init.go
tpl/transform/remarshal.go [new file with mode: 0644]
tpl/transform/remarshal_test.go [new file with mode: 0644]

index 3d43964406d7036cbbd8ba1aedf19ef1b460e1d8..da05548f49a0fbae1e18cb2be6cc9e38a30fa839 100644 (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))
+}
index c0e9b2d5db419847303213123f8a97f5051cef64..86951c2530981b2a080f9f0a3869b93e0dc42c7a 100644 (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
 
        }
diff --git a/tpl/transform/remarshal.go b/tpl/transform/remarshal.go
new file mode 100644 (file)
index 0000000..490def5
--- /dev/null
@@ -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")
+
+}
diff --git a/tpl/transform/remarshal_test.go b/tpl/transform/remarshal_test.go
new file mode 100644 (file)
index 0000000..78980e6
--- /dev/null
@@ -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)
+       }
+}