Skip to content

Commit

Permalink
Copy traverse code from pkg/util/template/validation.go
Browse files Browse the repository at this point in the history
  • Loading branch information
pkong-ds committed Aug 26, 2024
1 parent 2291ef2 commit e8a557c
Show file tree
Hide file tree
Showing 2 changed files with 298 additions and 1 deletion.
159 changes: 159 additions & 0 deletions devtools/gotemplatetranslationlinter/func.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package main

import (
"bytes"
"fmt"
"html/template"
"io"
"reflect"
"strconv"
"strings"
"time"

"github.com/Masterminds/sprig"
)

const (
templateTranslationMessageTemplateName = "__translation_message.html"
)

type tpl interface {
ExecuteTemplate(wr io.Writer, name string, data any) error
}

func MakeTemplateFuncMap(t tpl) map[string]interface{} {
var templateFuncMap = sprig.HermeticHtmlFuncMap()
// templateFuncMap[messageformat.TemplateRuntimeFuncName] = messageformat.TemplateRuntimeFunc
templateFuncMap["rfc3339"] = RFC3339
templateFuncMap["ensureTime"] = EnsureTime
templateFuncMap["isNil"] = IsNil
templateFuncMap["showAttributeValue"] = ShowAttributeValue
templateFuncMap["htmlattr"] = HTMLAttr
templateFuncMap["include"] = makeInclude(t)
templateFuncMap["translate"] = makeTranslate(t)
templateFuncMap["trimHTML"] = trimHTML
return templateFuncMap
}

func RFC3339(date interface{}) interface{} {
switch date := date.(type) {
case *time.Time:
return date.UTC().Format(time.RFC3339)
case time.Time:
return date.UTC().Format(time.RFC3339)
default:
return "INVALID_DATE"
}
}

func EnsureTime(anyValue interface{}) interface{} {
switch anyValue := anyValue.(type) {
case *time.Time:
return anyValue
case time.Time:
return anyValue
case string:
t, err := time.Parse(time.RFC3339, anyValue)
if err != nil {
panic(err)
}
return t
case *string:
t, err := time.Parse(time.RFC3339, *anyValue)
if err != nil {
panic(err)
}
return t
default:
return anyValue
}
}

func IsNil(v interface{}) bool {
return v == nil ||
(reflect.ValueOf(v).Kind() == reflect.Ptr && reflect.ValueOf(v).IsNil())
}

func ShowAttributeValue(v interface{}) string {
value := reflect.ValueOf(v)
if value.Kind() == reflect.Ptr {
if !value.IsNil() {
return ShowAttributeValue(reflect.ValueOf(v).Elem().Interface())
}
return ""
}

switch v := v.(type) {
case string:
return v
case float64:
return strconv.FormatFloat(v, 'f', -1, 64)
case float32:
return strconv.FormatFloat(float64(v), 'f', -1, 32)
case nil:
return ""
default:
return fmt.Sprintf("%v", v)

}
}

func HTMLAttr(v string) template.HTMLAttr {
// Ignore gosec error because the app developer can actually write any template
// But we should be careful that do not pass any user input to this function
return template.HTMLAttr(v) // nolint:gosec
}

func makeInclude(t tpl) func(tplName string, data any) (template.HTML, error) {
return func(
tplName string,
data any,
) (template.HTML, error) {
buf := &bytes.Buffer{}
err := t.ExecuteTemplate(buf, tplName, data)
// Ignore gosec error because the app developer can actually write any template
// But we should be careful that do not pass any user input to this function
html := template.HTML(buf.String()) // nolint:gosec
return html, err
}
}

// `translate` is intended for `include` a translation message but wrapped it
// in a span and set its translation key with data attribute
// In theory it can be used with resources other than translation, but take your
// own risks
func makeTranslate(t tpl) func(tranlsationKey string, data any) (template.HTML, error) {
include := makeInclude(t)
return func(
tranlsationKey string,
data any,
) (template.HTML, error) {
included, err := include(tranlsationKey, data)
if err != nil {
return template.HTML(""), err
}
buf := &bytes.Buffer{}
d := make(map[string]interface{})
d["Key"] = tranlsationKey
d["Value"] = included
err = t.ExecuteTemplate(buf, templateTranslationMessageTemplateName, d)
// Ignore gosec error because the app developer can actually write any template
// But we should be careful that do not pass any user input to this function
html := template.HTML(buf.String()) // nolint:gosec
return html, err
}
}

func trimHTML(input interface{}) interface{} {
switch input := input.(type) {
case string:
return strings.TrimSpace(input)
case template.HTML:
// `Masterminds/sprig`'s `trimAll` cannot handle html type, so we need to convert it to string first
// Ignore gosec error because this function is intended to be used with trusted input (__alert_message.html)
// and the output is intended to be used as HTML
return template.HTML(strings.TrimSpace(string(input))) // nolint:gosec
default:
return ""
}
}
140 changes: 139 additions & 1 deletion devtools/gotemplatetranslationlinter/translation_key_rule.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,147 @@
package main

import (
"fmt"
htmltemplate "html/template"
"sort"
"text/template/parse"
)

type TranslationKeyRule struct{}

func (r TranslationKeyRule) Check(content string, path string) LintViolations {
return r.check(content, path)
}

func (r TranslationKeyRule) check(content string, path string) LintViolations {
var violations LintViolations
// TODO: implement linting logic here
t := r.makeTemplate(content)

r.validateHTMLTemplate(t)

// TODO: add violations
return violations
}

func (r TranslationKeyRule) makeTemplate(content string) *htmltemplate.Template {
t := htmltemplate.New("")
funcMap := MakeTemplateFuncMap(t)
t.Funcs(funcMap)
parsed := htmltemplate.Must(t.Parse(content))

return parsed
}

func (r TranslationKeyRule) validateHTMLTemplate(template *htmltemplate.Template) error {
tpls := template.Templates()

sort.Slice(tpls, func(i, j int) bool {
return tpls[i].Name() < tpls[j].Name()
})

for _, tpl := range tpls {
if tpl.Tree == nil {
fmt.Printf("tpl.Tree == nil\n")
continue
}
// fmt.Printf("tpl.Tree != nil\n")
if err := r.validateTree(tpl.Tree); err != nil {
return err
}
}
return nil
}

func (r TranslationKeyRule) validateTree(tree *parse.Tree) (err error) {
validateFn := func(n parse.Node, depth int) (cont bool) {
// fmt.Printf("inside validateFn\n")
switch n := n.(type) {
case *parse.CommandNode:
fmt.Printf("case *parse.CommandNode:\n")
for _, arg := range n.Args {
if ident, ok := arg.(*parse.IdentifierNode); ok && ident.String() == "include" {
// TODO: handle include fn
fmt.Printf("pkong# xdd %s\n", ident)
fmt.Printf("pkong# include n: %s\n", n)
}
}
case *parse.TemplateNode:
fmt.Printf("case *parse.TemplateNode:\n")
// TODO: handle template node
fmt.Printf("pkong# template n: %s\n", n)
// default:
// fmt.Printf("pkong# node n: %s\n", n)
// fmt.Printf("pkong# node n.(type): %t\n", n)
}

return err == nil
}

r.traverseTree(tree, validateFn)
return
}

func (r TranslationKeyRule) traverseTree(tree *parse.Tree, fn func(n parse.Node, depth int) (cont bool)) {
r.traverseTreeVisit(tree.Root, 0, fn)
}

func (r TranslationKeyRule) traverseTreeVisitBranch(n *parse.BranchNode, depth int, fn func(n parse.Node, depth int) (cont bool)) (cont bool) {
if cont = r.traverseTreeVisit(n.Pipe, depth, fn); !cont {
return
}
if cont = r.traverseTreeVisit(n.List, depth, fn); !cont {
return
}
if n.ElseList != nil {
if cont = r.traverseTreeVisit(n.ElseList, depth, fn); !cont {
return false
}
}
return
}

func (r TranslationKeyRule) traverseTreeVisit(n parse.Node, depth int, fn func(n parse.Node, depth int) (cont bool)) (cont bool) {
// fmt.Printf("traverseTreeVisit\n")
cont = fn(n, depth)
if !cont {
return
}

// fmt.Printf("n: %s\n", n)
switch n := n.(type) {
case *parse.PipeNode:
fmt.Printf("case *parse.PipeNode:\n")
for _, cmd := range n.Cmds {
if cont = r.traverseTreeVisit(cmd, depth, fn); !cont {
break
}
}
case *parse.CommandNode:
fmt.Printf("case *parse.CommandNode:\n")
for _, arg := range n.Args {
if pipe, ok := arg.(*parse.PipeNode); ok {
if cont = r.traverseTreeVisit(pipe, depth+1, fn); !cont {
break
}
}
}
case *parse.ActionNode:
cont = r.traverseTreeVisit(n.Pipe, depth, fn)
case *parse.TemplateNode, *parse.TextNode:
break
case *parse.IfNode:
cont = r.traverseTreeVisitBranch(&n.BranchNode, depth, fn)
case *parse.RangeNode:
cont = r.traverseTreeVisitBranch(&n.BranchNode, depth, fn)
case *parse.WithNode:
cont = r.traverseTreeVisitBranch(&n.BranchNode, depth, fn)
case *parse.ListNode:
for _, n := range n.Nodes {
if cont = r.traverseTreeVisit(n, depth+1, fn); !cont {
break
}
}
}

return
}

0 comments on commit e8a557c

Please sign in to comment.