Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rendering template.HTML directly into templates

Tags:

go

I've recently swapped out datastores and as a side-effect have had to change a struct field from template.HTML to string to be compatible with the marshaller/DB driver. This field, RenderedDesc, contains rendered HTML as passed through russross/blackfriday.

Previously I could just pass the whole struct into the template "as is" and call {{ .RenderedDesc }} in the template.

Because it's now a string, I've added a filter to convert it back on template render:

templates.go

func RenderUnsafe(s string) template.HTML {
    return template.HTML(s)
}

template.FuncMap{
        ...
        "unsafe": RenderUnsafe,
    }

_content.tmpl

...
<div class="detail">

    {{ .RenderedDesc | unsafe }}

</div>
...

Is there a better way to achieve this without having to use a filter at the template level? Short of re-writing marshalling logic from my DB driver (not on the cards) it looks like this is the simplest way to "store" strings but render raw HTML.

like image 572
elithrar Avatar asked Oct 20 '22 15:10

elithrar


1 Answers

IMHO, the right way to do this is using a filter, like you are already doing. There are more ways to achieve the same, one of them is using tags and converting the struct in to a map[string]Interface{}. Because map fields can be reached in the same way that structs, your templates will remain unmodified.

Show me the code (playground):

package main

import (
    "html/template"
    "os"
    "reflect"
)

var templates = template.Must(template.New("tmp").Parse(`
    <html>
        <head>
        </head>
        <body>
            <h1>Hello</h1>
            <div class="content">
                Usafe Content = {{.Content}}
                Safe Content  = {{.Safe}}
                Bool          = {{.Bool}}
                Num           = {{.Num}}
                Nested.Num    = {{.Nested.Num}}
                Nested.Bool   = {{.Nested.Bool}}
            </div>
        </body>
    </html>
`))

func asUnsafeMap(any interface{}) map[string]interface{} {
    v := reflect.ValueOf(any)
    if v.Kind() != reflect.Struct {
        panic("asUnsafeMap invoked with a non struct parameter")
    }
    m := map[string]interface{}{}
    for i := 0; i < v.NumField(); i++ {
        value := v.Field(i)
        if !value.CanInterface() {
            continue
        }
        ftype := v.Type().Field(i)
        if ftype.Tag.Get("unsafe") == "html" {
            m[ftype.Name] = template.HTML(value.String())
        } else {
            m[ftype.Name] = value.Interface()
        }
    }
    return m
}

func main() {
    templates.ExecuteTemplate(os.Stdout, "tmp", asUnsafeMap(struct {
        Content string `unsafe:"html"`
        Safe    string
        Bool    bool
        Num     int
        Nested  struct {
            Num  int
            Bool bool
        }
    }{
        Content: "<h2>Lol</h2>",
        Safe:    "<h2>Lol</h2>",
        Bool:    true,
        Num:     10,
        Nested: struct {
            Num  int
            Bool bool
        }{
            Num:  9,
            Bool: true,
        },
    }))
}

Output:

<html>
    <head>
    </head>
    <body>
        <h1>Hello</h1>
        <div class="content">
            Usafe Content = <h2>Lol</h2>
            Safe Content  = &lt;h2&gt;Lol&lt;/h2&gt;
            Bool          = true
            Num           = 10
            Nested.Num    = 9
            Nested.Bool   = true
        </div>
    </body>
</html>

Note: the previous code doesn't work with nested structures, but it will be easy to add support for them. Also, every field tagged as unsafe will be treated as string.

like image 75
Alvivi Avatar answered Oct 24 '22 00:10

Alvivi