gosora/common/templates/templates.go

1911 lines
54 KiB
Go

package tmpl
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"reflect"
"runtime/debug"
"strconv"
"strings"
"text/template/parse"
"time"
"unicode"
)
// TODO: Turn this file into a library
var textOverlapList = make(map[string]int)
// TODO: Stop hard-coding this here
var langPkg = "github.com/Azareal/Gosora/common/phrases"
type VarItem struct {
Name string
Destination string
Type string
}
type VarItemReflect struct {
Name string
Destination string
Value reflect.Value
}
type CTemplateConfig struct {
Minify bool
Debug bool
SuperDebug bool
SkipHandles bool
SkipTmplPtrMap bool
SkipInitBlock bool
PackageName string
}
// nolint
type CTemplateSet struct {
templateList map[string]*parse.Tree
fileDir string
funcMap map[string]interface{}
importMap map[string]string
//templateFragmentCount map[string]int
fragOnce map[string]bool
fragmentCursor map[string]int
FragOut []OutFrag
fragBuf []Fragment
varList map[string]VarItem
localVars map[string]map[string]VarItemReflect
hasDispInt bool
localDispStructIndex int
langIndexToName []string
guestOnly bool
memberOnly bool
stats map[string]int
//tempVars map[string]string
config CTemplateConfig
baseImportMap map[string]string
buildTags string
overridenTrack map[string]map[string]bool
overridenRoots map[string]map[string]bool
themeName string
perThemeTmpls map[string]bool
logger *log.Logger
loggerf *os.File
lang string
}
func NewCTemplateSet(in string) *CTemplateSet {
f, err := os.OpenFile("./logs/tmpls-"+in+"-"+strconv.FormatInt(time.Now().Unix(), 10)+".log", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0755)
if err != nil {
panic(err)
}
return &CTemplateSet{
config: CTemplateConfig{
PackageName: "main",
},
baseImportMap: map[string]string{},
overridenRoots: map[string]map[string]bool{},
funcMap: map[string]interface{}{
"and": "&&",
"not": "!",
"or": "||",
"eq": "==",
"ge": ">=",
"gt": ">",
"le": "<=",
"lt": "<",
"ne": "!=",
"add": "+",
"subtract": "-",
"multiply": "*",
"divide": "/",
"dock": true,
"hasWidgets": true,
"elapsed": true,
"lang": true,
"langf": true,
"level": true,
"bunit": true,
"abstime": true,
"reltime": true,
"scope": true,
"dyntmpl": true,
"index": true,
"flush": true,
},
logger: log.New(f, "", log.LstdFlags),
loggerf: f,
lang: in,
}
}
func (c *CTemplateSet) SetConfig(config CTemplateConfig) {
if config.PackageName == "" {
config.PackageName = "main"
}
c.config = config
}
func (c *CTemplateSet) GetConfig() CTemplateConfig {
return c.config
}
func (c *CTemplateSet) SetBaseImportMap(importMap map[string]string) {
c.baseImportMap = importMap
}
func (c *CTemplateSet) SetBuildTags(tags string) {
c.buildTags = tags
}
func (c *CTemplateSet) SetOverrideTrack(overriden map[string]map[string]bool) {
c.overridenTrack = overriden
}
func (c *CTemplateSet) GetOverridenRoots() map[string]map[string]bool {
return c.overridenRoots
}
func (c *CTemplateSet) SetThemeName(name string) {
c.themeName = name
}
func (c *CTemplateSet) SetPerThemeTmpls(perThemeTmpls map[string]bool) {
c.perThemeTmpls = perThemeTmpls
}
func (c *CTemplateSet) ResetLogs(in string) {
f, err := os.OpenFile("./logs/tmpls-"+in+"-"+strconv.FormatInt(time.Now().Unix(), 10)+".log", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0755)
if err != nil {
panic(err)
}
c.logger = log.New(f, "", log.LstdFlags)
c.loggerf = f
}
type SkipBlock struct {
Frags map[int]int
LastCount int
ClosestFragSkip int
}
type Skipper struct {
Count int
Index int
}
type OutFrag struct {
TmplName string
Index int
Body string
}
func (c *CTemplateSet) CompileByLoggedin(name string, fileDir string, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (stub string, gout string, mout string, err error) {
c.importMap = map[string]string{}
for index, item := range c.baseImportMap {
c.importMap[index] = item
}
for _, importItem := range imports {
c.importMap[importItem] = importItem
}
var importList string
for _, item := range c.importMap {
ispl := strings.Split(item, " ")
if len(ispl) > 1 {
importList += "import " + ispl[0] + " \"" + ispl[1] + "\"\n"
} else {
importList += "import \"" + item + "\"\n"
}
}
fname := strings.TrimSuffix(name, filepath.Ext(name))
if c.themeName != "" {
_, ok := c.perThemeTmpls[fname]
if !ok {
return "", "", "", nil
}
fname += "_" + c.themeName
}
c.importMap["github.com/Azareal/Gosora/common"] = "c github.com/Azareal/Gosora/common"
stub = `package ` + c.config.PackageName + `
` + importList + `
import "errors"
`
if !c.config.SkipInitBlock {
stub += "// nolint\nfunc init() {\n"
if !c.config.SkipHandles && c.themeName == "" {
stub += "\tc.Template_" + fname + "_handle = Template_" + fname + "\n"
stub += "\tc.Ctemplates = append(c.Ctemplates,\"" + fname + "\")\n"
}
if !c.config.SkipTmplPtrMap {
stub += "tmpl := Template_" + fname + "\n"
stub += "\tc.TmplPtrMap[\"" + fname + "\"] = &tmpl\n"
stub += "\tc.TmplPtrMap[\"o_" + fname + "\"] = tmpl\n"
}
stub += "}\n\n"
}
// TODO: Try to remove this redundant interface cast
stub += `
// nolint
func Template_` + fname + `(tmpl_` + fname + `_i interface{}, w io.Writer) error {
tmpl_` + fname + `_vars, ok := tmpl_` + fname + `_i.(` + expects + `)
if !ok {
return errors.New("invalid page struct value")
}
if tmpl_` + fname + `_vars.CurrentUser.Loggedin {
return Template_` + fname + `_member(tmpl_` + fname + `_i, w)
}
return Template_` + fname + `_guest(tmpl_` + fname + `_i, w)
}`
c.fileDir = fileDir
content, err := c.loadTemplate(c.fileDir, name)
if err != nil {
c.detail("bailing out: ", err)
return "", "", "", err
}
c.guestOnly = true
gout, err = c.compile(name, content, expects, expectsInt, varList, imports...)
if err != nil {
return "", "", "", err
}
c.guestOnly = false
c.memberOnly = true
mout, err = c.compile(name, content, expects, expectsInt, varList, imports...)
c.memberOnly = false
return stub, gout, mout, err
}
func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (out string, err error) {
if c.config.Debug {
c.logger.Println("Compiling template '" + name + "'")
}
c.fileDir = fileDir
content, err := c.loadTemplate(c.fileDir, name)
if err != nil {
c.detail("bailing out: ", err)
return "", err
}
return c.compile(name, content, expects, expectsInt, varList, imports...)
}
func (c *CTemplateSet) compile(name string, content string, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (out string, err error) {
defer func() {
if r := recover(); r != nil {
fmt.Println(r)
debug.PrintStack()
err := c.loggerf.Sync()
if err != nil {
fmt.Println(err)
}
log.Fatal("")
return
}
}()
//c.dumpCall("compile", name, content, expects, expectsInt, varList, imports)
//c.detailf("c: %+v\n", c)
c.importMap = map[string]string{}
for index, item := range c.baseImportMap {
c.importMap[index] = item
}
c.importMap["errors"] = "errors"
for _, importItem := range imports {
c.importMap[importItem] = importItem
}
c.varList = varList
c.hasDispInt = false
c.localDispStructIndex = 0
c.stats = make(map[string]int)
tree := parse.New(name, c.funcMap)
treeSet := make(map[string]*parse.Tree)
tree, err = tree.Parse(content, "{{", "}}", treeSet, c.funcMap)
if err != nil {
return "", err
}
c.detail(name)
fname := strings.TrimSuffix(name, filepath.Ext(name))
if c.themeName != "" {
_, ok := c.perThemeTmpls[fname]
if !ok {
c.detail("fname not in c.perThemeTmpls")
c.detail("c.perThemeTmpls", c.perThemeTmpls)
return "", nil
}
fname += "_" + c.themeName
}
if c.guestOnly {
fname += "_guest"
} else if c.memberOnly {
fname += "_member"
}
c.detail("root overridenTrack loop")
c.detail("fname:", fname)
for themeName, track := range c.overridenTrack {
c.detail("themeName:", themeName)
c.detailf("track: %+v\n", track)
croot, ok := c.overridenRoots[themeName]
if !ok {
croot = make(map[string]bool)
c.overridenRoots[themeName] = croot
}
c.detailf("croot: %+v\n", croot)
for tmplName, _ := range track {
cname := tmplName
if c.guestOnly {
cname += "_guest"
} else if c.memberOnly {
cname += "_member"
}
c.detail("cname:", cname)
if fname == cname {
c.detail("match")
croot[strings.TrimSuffix(strings.TrimSuffix(fname, "_guest"), "_member")] = true
} else {
c.detail("no match")
}
}
}
c.detailf("c.overridenRoots: %+v\n", c.overridenRoots)
var outBuf []OutBufferFrame
rootHold := "tmpl_" + fname + "_vars"
con := CContext{
RootHolder: rootHold,
VarHolder: rootHold,
HoldReflect: reflect.ValueOf(expectsInt),
RootTemplateName: fname,
TemplateName: fname,
OutBuf: &outBuf,
}
c.templateList = map[string]*parse.Tree{fname: tree}
c.detail(c.templateList)
c.localVars = make(map[string]map[string]VarItemReflect)
c.localVars[fname] = make(map[string]VarItemReflect)
c.localVars[fname]["."] = VarItemReflect{".", con.VarHolder, con.HoldReflect}
if c.fragOnce == nil {
c.fragOnce = make(map[string]bool)
}
c.fragmentCursor = map[string]int{fname: 0}
c.fragBuf = nil
c.langIndexToName = nil
// TODO: Is this the first template loaded in? We really should have some sort of constructor for CTemplateSet
//if c.templateFragmentCount == nil {
// c.templateFragmentCount = make(map[string]int)
//}
//c.detailf("c: %+v\n", c)
startIndex := con.StartTemplate("")
c.rootIterate(c.templateList[fname], con)
con.EndTemplate("")
c.afterTemplate(con, startIndex)
//c.templateFragmentCount[fname] = c.fragmentCursor[fname] + 1
_, ok := c.fragOnce[fname]
if !ok {
c.fragOnce[fname] = true
}
if len(c.langIndexToName) > 0 {
c.importMap[langPkg] = langPkg
}
// TODO: Simplify this logic by doing some reordering?
if c.lang == "normal" {
c.importMap["net/http"] = "net/http"
}
var importList string
for _, item := range c.importMap {
ispl := strings.Split(item, " ")
if len(ispl) > 1 {
importList += "import " + ispl[0] + " \"" + ispl[1] + "\"\n"
} else {
importList += "import \"" + item + "\"\n"
}
}
var varString string
for _, varItem := range c.varList {
varString += "var " + varItem.Name + " " + varItem.Type + " = " + varItem.Destination + "\n"
}
var fout string
if c.buildTags != "" {
fout += "// +build " + c.buildTags + "\n\n"
}
fout += "// Code generated by Gosora. More below:\n/* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */\n"
fout += "package " + c.config.PackageName + "\n" + importList + "\n"
if !c.config.SkipInitBlock {
if len(c.langIndexToName) > 0 {
fout += "var " + fname + "_tmpl_phrase_id int\n\n"
}
fout += "// nolint\nfunc init() {\n"
if !c.config.SkipHandles && c.themeName == "" {
fout += "\tc.Template_" + fname + "_handle = Template_" + fname + "\n"
fout += "\tc.Ctemplates = append(c.Ctemplates,\"" + fname + "\")\n"
}
if !c.config.SkipTmplPtrMap {
fout += "tmpl := Template_" + fname + "\n"
fout += "\tc.TmplPtrMap[\"" + fname + "\"] = &tmpl\n"
fout += "\tc.TmplPtrMap[\"o_" + fname + "\"] = tmpl\n"
}
if len(c.langIndexToName) > 0 {
fout += "\t" + fname + "_tmpl_phrase_id = phrases.RegisterTmplPhraseNames([]string{\n"
for _, name := range c.langIndexToName {
fout += "\t\t" + `"` + name + `"` + ",\n"
}
fout += "\t})\n"
}
fout += "}\n\n"
}
if c.lang == "normal" {
fout += "// nolint\nfunc Template_" + fname + "(tmpl_" + fname + "_i interface{}, w io.Writer) error {\n"
fout += `tmpl_` + fname + `_vars, ok := tmpl_` + fname + `_i.(` + expects + `)
if !ok {
return errors.New("invalid page struct value")
}
`
fout += `var iw http.ResponseWriter
gzw, ok := w.(c.GzipResponseWriter)
if ok {
iw = gzw.ResponseWriter
}
_ = iw
`
} else {
fout += "// nolint\nfunc Template_" + fname + "(tmpl_" + fname + "_vars interface{}, w io.Writer) error {\n"
}
if len(c.langIndexToName) > 0 {
fout += "var plist = phrases.GetTmplPhrasesBytes(" + fname + "_tmpl_phrase_id)\n"
}
fout += varString
skipped := make(map[string]*SkipBlock) // map[templateName]*SkipBlock{map[atIndexAndAfter]skipThisMuch,lastCount}
writeTextFrame := func(tmplName string, index int) {
out := "w.Write(" + tmplName + "_frags[" + strconv.Itoa(index) + "]" + ")\n"
c.detail("writing ", out)
fout += out
}
for fid := 0; len(outBuf) > fid; fid++ {
frame := outBuf[fid]
c.detail(frame.Type + " frame")
if frame.Type == "text" {
c.detail(frame)
oid := fid
c.detail("oid:", oid)
skipBlock, ok := skipped[frame.TemplateName]
if !ok {
skipBlock = &SkipBlock{make(map[int]int), 0, 0}
skipped[frame.TemplateName] = skipBlock
}
skip := skipBlock.LastCount
c.detailf("skipblock %+v\n", skipBlock)
//var count int
for len(outBuf) > fid+1 && outBuf[fid+1].Type == "text" && outBuf[fid+1].TemplateName == frame.TemplateName {
c.detail("pre fid:", fid)
//count++
next := outBuf[fid+1]
c.detail("next frame:", next)
c.detail("frame frag:", c.fragBuf[frame.Extra2.(int)])
c.detail("next frag:", c.fragBuf[next.Extra2.(int)])
c.fragBuf[frame.Extra2.(int)].Body += c.fragBuf[next.Extra2.(int)].Body
c.fragBuf[next.Extra2.(int)].Seen = true
fid++
skipBlock.LastCount++
skipBlock.Frags[frame.Extra.(int)] = skipBlock.LastCount
c.detail("post fid:", fid)
}
writeTextFrame(frame.TemplateName, frame.Extra.(int)-skip)
} else if frame.Type == "varsub" || frame.Type == "cvarsub" {
fout += "w.Write(" + frame.Body + ")\n"
} else if frame.Type == "identifier" {
fout += frame.Body
} else if frame.Type == "lang" {
fout += "w.Write(plist[" + strconv.Itoa(frame.Extra.(int)) + "])\n"
} else {
fout += frame.Body
}
}
fout += "return nil\n}\n"
writeFrag := func(tmplName string, index int, body string) {
//c.detail("writing ", fragmentPrefix)
c.FragOut = append(c.FragOut, OutFrag{tmplName, index, body})
}
for _, frag := range c.fragBuf {
c.detail("frag: ", frag)
if frag.Seen {
c.detail("invisible")
continue
}
// TODO: What if the same template is invoked in multiple spots in a template?
skipBlock := skipped[frag.TemplateName]
skip := skipBlock.Frags[skipBlock.ClosestFragSkip]
_, ok := skipBlock.Frags[frag.Index]
if ok {
skipBlock.ClosestFragSkip = frag.Index
}
c.detailf("skipblock %+v\n", skipBlock)
c.detail("skipping ", skip)
index := frag.Index - skip
if index < 0 {
index = 0
}
writeFrag(frag.TemplateName, index, frag.Body)
}
fout = strings.Replace(fout, `))
w.Write([]byte(`, " + ", -1)
fout = strings.Replace(fout, "` + `", "", -1)
if c.config.Debug {
for index, count := range c.stats {
c.logger.Println(index+": ", strconv.Itoa(count))
}
c.logger.Println(" ")
}
c.detail("Output!")
c.detail(fout)
return fout, nil
}
func (c *CTemplateSet) rootIterate(tree *parse.Tree, con CContext) {
c.dumpCall("rootIterate", tree, con)
c.detail(tree.Root)
for _, node := range tree.Root.Nodes {
c.detail("Node:", node.String())
c.compileSwitch(con, node)
}
c.retCall("rootIterate")
}
func inSlice(haystack []string, expr string) bool {
for _, needle := range haystack {
if needle == expr {
return true
}
}
return false
}
func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) {
c.dumpCall("compileSwitch", con, node)
defer c.retCall("compileSwitch")
switch node := node.(type) {
case *parse.ActionNode:
c.detail("Action Node")
if node.Pipe == nil {
break
}
for _, cmd := range node.Pipe.Cmds {
c.compileSubSwitch(con, cmd)
}
case *parse.IfNode:
c.detail("If Node:")
c.detail("node.Pipe", node.Pipe)
var expr string
for _, cmd := range node.Pipe.Cmds {
c.detail("If Node Bit:", cmd)
c.detail("Bit Type:", reflect.ValueOf(cmd).Type().Name())
expr += c.compileExprSwitch(con, cmd)
c.detail("Expression Step:", c.compileExprSwitch(con, cmd))
}
c.detail("Expression:", expr)
// Simple member / guest optimisation for now
// TODO: Expand upon this
userExprs, negUserExprs := buildUserExprs(con.RootHolder)
if c.guestOnly {
c.detail("optimising away member branch")
if inSlice(userExprs, expr) {
c.detail("positive conditional:", expr)
if node.ElseList != nil {
c.compileSwitch(con, node.ElseList)
}
return
} else if inSlice(negUserExprs, expr) {
c.detail("negative conditional:", expr)
c.compileSwitch(con, node.List)
return
}
} else if c.memberOnly {
c.detail("optimising away guest branch")
if (con.RootHolder + ".CurrentUser.Loggedin") == expr {
c.detail("positive conditional:", expr)
c.compileSwitch(con, node.List)
return
} else if ("!" + con.RootHolder + ".CurrentUser.Loggedin") == expr {
c.detail("negative conditional:", expr)
if node.ElseList != nil {
c.compileSwitch(con, node.ElseList)
}
return
}
}
con.Push("startif", "if "+expr+" {\n")
c.compileSwitch(con, node.List)
if node.ElseList == nil {
c.detail("Selected Branch 1")
con.Push("endif", "}\n")
} else {
c.detail("Selected Branch 2")
con.Push("endif", "}")
con.Push("startelse", " else {\n")
c.compileSwitch(con, node.ElseList)
con.Push("endelse", "}\n")
}
case *parse.ListNode:
c.detailf("List Node: %+v\n", node)
for _, subnode := range node.Nodes {
c.compileSwitch(con, subnode)
}
case *parse.RangeNode:
c.compileRangeNode(con, node)
case *parse.TemplateNode:
c.compileSubTemplate(con, node)
case *parse.TextNode:
c.addText(con, node.Text)
default:
c.unknownNode(node)
}
}
func (c *CTemplateSet) addText(con CContext, text []byte) {
c.dumpCall("addText", con, text)
tmpText := bytes.TrimSpace(text)
if len(tmpText) == 0 {
return
}
nodeText := string(text)
c.detail("con.TemplateName: ", con.TemplateName)
fragIndex := c.fragmentCursor[con.TemplateName]
_, ok := c.fragOnce[con.TemplateName]
c.fragBuf = append(c.fragBuf, Fragment{nodeText, con.TemplateName, fragIndex, ok})
con.PushText(strconv.Itoa(fragIndex), fragIndex, len(c.fragBuf)-1)
c.fragmentCursor[con.TemplateName] = fragIndex + 1
}
func (c *CTemplateSet) compileRangeNode(con CContext, node *parse.RangeNode) {
c.dumpCall("compileRangeNode", con, node)
defer c.retCall("compileRangeNode")
c.detail("node.Pipe: ", node.Pipe)
var expr string
var outVal reflect.Value
for _, cmd := range node.Pipe.Cmds {
c.detail("Range Bit:", cmd)
// ! This bit is slightly suspect, hm.
expr, outVal = c.compileReflectSwitch(con, cmd)
}
c.detail("Expr:", expr)
c.detail("Range Kind Switch!")
startIf := func(item reflect.Value, useCopy bool) {
con.Push("startif", "if len("+expr+") != 0 {\n")
startIndex := con.StartLoop("for _, item := range " + expr + " {\n")
ccon := con
var depth string
if ccon.VarHolder == "item" {
depth = strings.TrimPrefix(ccon.VarHolder, "item")
if depth != "" {
idepth, err := strconv.Atoi(depth)
if err != nil {
panic(err)
}
depth = strconv.Itoa(idepth + 1)
}
}
ccon.VarHolder = "item" + depth
ccon.HoldReflect = item
c.compileSwitch(ccon, node.List)
if con.LastBufIndex() == startIndex {
con.DiscardAndAfter(startIndex - 1)
return
}
con.EndLoop("}\n")
c.afterTemplate(con, startIndex)
if node.ElseList != nil {
con.Push("endif", "}")
con.Push("startelse", " else {\n")
if !useCopy {
ccon = con
}
c.compileSwitch(ccon, node.ElseList)
con.Push("endelse", "}\n")
} else {
con.Push("endif", "}\n")
}
}
switch outVal.Kind() {
case reflect.Map:
var item reflect.Value
for _, key := range outVal.MapKeys() {
item = outVal.MapIndex(key)
}
c.detail("Range item:", item)
if !item.IsValid() {
c.critical("expr:", expr)
c.critical("con.VarHolder", con.VarHolder)
panic("item" + "^\n" + "Invalid map. Maybe, it doesn't have any entries for the template engine to analyse?")
}
startIf(item, true)
case reflect.Slice:
if outVal.Len() == 0 {
c.critical("expr:", expr)
c.critical("con.VarHolder", con.VarHolder)
panic("The sample data needs at-least one or more elements for the slices. We're looking into removing this requirement at some point!")
}
startIf(outVal.Index(0), false)
case reflect.Invalid:
return
}
}
// ! Temporary, we probably want something that is good with non-struct pointers too
// For compileSubSwitch and compileSubTemplate
func (c *CTemplateSet) skipStructPointers(cur reflect.Value, id string) reflect.Value {
if cur.Kind() == reflect.Ptr {
c.detail("Looping over pointer")
for cur.Kind() == reflect.Ptr {
cur = cur.Elem()
}
c.detail("Data Kind:", cur.Kind().String())
c.detail("Field Bit:", id)
}
return cur
}
// For compileSubSwitch and compileSubTemplate
func (c *CTemplateSet) checkIfValid(cur reflect.Value, varHolder string, holdreflect reflect.Value, varBit string, multiline bool) {
if !cur.IsValid() {
c.critical("Debug Data:")
c.critical("Holdreflect:", holdreflect)
c.critical("Holdreflect.Kind():", holdreflect.Kind())
if !c.config.SuperDebug {
c.critical("cur.Kind():", cur.Kind().String())
}
c.critical("")
if !multiline {
panic(varHolder + varBit + "^\n" + "Invalid value. Maybe, it doesn't exist?")
}
panic(varBit + "^\n" + "Invalid value. Maybe, it doesn't exist?")
}
}
func (c *CTemplateSet) compileSubSwitch(con CContext, node *parse.CommandNode) {
c.dumpCall("compileSubSwitch", con, node)
switch n := node.Args[0].(type) {
case *parse.FieldNode:
c.detail("Field Node:", n.Ident)
/* Use reflect to determine if the field is for a method, otherwise assume it's a variable. Variable declarations are coming soon! */
cur := con.HoldReflect
var varBit string
if cur.Kind() == reflect.Interface {
cur = cur.Elem()
varBit += ".(" + cur.Type().Name() + ")"
}
var assLines string
multiline := false
for _, id := range n.Ident {
c.detail("Data Kind:", cur.Kind().String())
c.detail("Field Bit:", id)
cur = c.skipStructPointers(cur, id)
c.checkIfValid(cur, con.VarHolder, con.HoldReflect, varBit, multiline)
c.detail("in-loop varBit: " + varBit)
if cur.Kind() == reflect.Map {
cur = cur.MapIndex(reflect.ValueOf(id))
varBit += "[\"" + id + "\"]"
cur = c.skipStructPointers(cur, id)
if cur.Kind() == reflect.Struct || cur.Kind() == reflect.Interface {
// TODO: Move the newVarByte declaration to the top level or to the if level, if a dispInt is only used in a particular if statement
var dispStr, newVarByte string
if cur.Kind() == reflect.Interface {
dispStr = "Int"
if !c.hasDispInt {
newVarByte = ":"
c.hasDispInt = true
}
}
// TODO: De-dupe identical struct types rather than allocating a variable for each one
if cur.Kind() == reflect.Struct {
dispStr = "Struct" + strconv.Itoa(c.localDispStructIndex)
newVarByte = ":"
c.localDispStructIndex++
}
con.VarHolder = "disp" + dispStr
varBit = con.VarHolder + " " + newVarByte + "= " + con.VarHolder + varBit + "\n"
multiline = true
} else {
continue
}
}
if cur.Kind() != reflect.Interface {
cur = cur.FieldByName(id)
varBit += "." + id
}
// TODO: Handle deeply nested pointers mixed with interfaces mixed with pointers better
if cur.Kind() == reflect.Interface {
cur = cur.Elem()
varBit += ".("
// TODO: Surely, there's a better way of doing this?
if cur.Type().PkgPath() != "main" && cur.Type().PkgPath() != "" {
c.importMap["html/template"] = "html/template"
varBit += strings.TrimPrefix(cur.Type().PkgPath(), "html/") + "."
}
varBit += cur.Type().Name() + ")"
}
c.detail("End Cycle: ", varBit)
}
if multiline {
assSplit := strings.Split(varBit, "\n")
varBit = assSplit[len(assSplit)-1]
assSplit = assSplit[:len(assSplit)-1]
assLines = strings.Join(assSplit, "\n") + "\n"
}
c.compileVarSub(con, con.VarHolder+varBit, cur, assLines, func(in string) string {
for _, varItem := range c.varList {
if strings.HasPrefix(in, varItem.Destination) {
in = strings.Replace(in, varItem.Destination, varItem.Name, 1)
}
}
return in
})
case *parse.DotNode:
c.detail("Dot Node:", node.String())
c.compileVarSub(con, con.VarHolder, con.HoldReflect, "", nil)
case *parse.NilNode:
panic("Nil is not a command x.x")
case *parse.VariableNode:
c.detail("Variable Node:", n.String())
c.detail(n.Ident)
varname, reflectVal := c.compileIfVarSub(con, n.String())
c.compileVarSub(con, varname, reflectVal, "", nil)
case *parse.StringNode:
con.Push("stringnode", n.Quoted)
case *parse.IdentifierNode:
c.detail("Identifier Node:", node)
c.detail("Identifier Node Args:", node.Args)
out, outval, lit, noident := c.compileIdentSwitch(con, node)
if noident {
return
} else if lit {
con.Push("identifier", out)
return
}
c.compileVarSub(con, out, outval, "", nil)
default:
c.unknownNode(node)
}
}
func (c *CTemplateSet) compileExprSwitch(con CContext, node *parse.CommandNode) (out string) {
c.dumpCall("compileExprSwitch", con, node)
firstWord := node.Args[0]
switch n := firstWord.(type) {
case *parse.FieldNode:
if c.config.SuperDebug {
c.logger.Println("Field Node:", n.Ident)
for _, id := range n.Ident {
c.logger.Println("Field Bit:", id)
}
}
/* Use reflect to determine if the field is for a method, otherwise assume it's a variable. Coming Soon. */
out = c.compileBoolSub(con, n.String())
case *parse.ChainNode:
c.detail("Chain Node:", n.Node)
c.detail("Node Args:", node.Args)
case *parse.IdentifierNode:
c.detail("Identifier Node:", node)
c.detail("Node Args:", node.Args)
out = c.compileIdentSwitchN(con, node)
case *parse.DotNode:
out = con.VarHolder
case *parse.VariableNode:
c.detail("Variable Node:", n.String())
c.detail("Node Identifier:", n.Ident)
out, _ = c.compileIfVarSub(con, n.String())
case *parse.NilNode:
panic("Nil is not a command x.x")
case *parse.PipeNode:
c.detail("Pipe Node!")
c.detail(n)
c.detail("Node Args:", node.Args)
out += c.compileIdentSwitchN(con, node)
default:
c.unknownNode(firstWord)
}
c.retCall("compileExprSwitch", out)
return out
}
func (c *CTemplateSet) unknownNode(node parse.Node) {
c.logger.Println("Unknown Kind:", reflect.ValueOf(node).Elem().Kind())
c.logger.Println("Unknown Type:", reflect.ValueOf(node).Elem().Type().Name())
panic("I don't know what node this is! Grr...")
}
func (c *CTemplateSet) compileIdentSwitchN(con CContext, node *parse.CommandNode) (out string) {
c.detail("in compileIdentSwitchN")
out, _, _, _ = c.compileIdentSwitch(con, node)
return out
}
func (c *CTemplateSet) dumpSymbol(pos int, node *parse.CommandNode, symbol string) {
c.detail("symbol: ", symbol)
c.detail("node.Args[pos + 1]", node.Args[pos+1])
c.detail("node.Args[pos + 2]", node.Args[pos+2])
}
func (c *CTemplateSet) compareFunc(con CContext, pos int, node *parse.CommandNode, compare string) (out string) {
c.dumpSymbol(pos, node, compare)
return c.compileIfVarSubN(con, node.Args[pos+1].String()) + " " + compare + " " + c.compileIfVarSubN(con, node.Args[pos+2].String())
}
func (c *CTemplateSet) simpleMath(con CContext, pos int, node *parse.CommandNode, symbol string) (out string, val reflect.Value) {
leftParam, val2 := c.compileIfVarSub(con, node.Args[pos+1].String())
rightParam, val3 := c.compileIfVarSub(con, node.Args[pos+2].String())
if val2.IsValid() {
val = val2
} else if val3.IsValid() {
val = val3
} else {
// TODO: What does this do?
numSample := 1
val = reflect.ValueOf(numSample)
}
c.dumpSymbol(pos, node, symbol)
return leftParam + " " + symbol + " " + rightParam, val
}
func (c *CTemplateSet) compareJoin(con CContext, pos int, node *parse.CommandNode, symbol string) (pos2 int, out string) {
c.detailf("Building %s function", symbol)
if pos == 0 {
c.logger.Println("pos:", pos)
panic(symbol + " is missing a left operand")
}
if len(node.Args) <= pos {
c.logger.Println("post pos:", pos)
c.logger.Println("len(node.Args):", len(node.Args))
panic(symbol + " is missing a right operand")
}
left := c.compileBoolSub(con, node.Args[pos-1].String())
_, funcExists := c.funcMap[node.Args[pos+1].String()]
var right string
if !funcExists {
right = c.compileBoolSub(con, node.Args[pos+1].String())
}
out = left + " " + symbol + " " + right
c.detail("Left operand:", node.Args[pos-1])
c.detail("Right operand:", node.Args[pos+1])
if !funcExists {
pos++
}
c.detail("pos:", pos)
c.detail("len(node.Args):", len(node.Args))
return pos, out
}
func (c *CTemplateSet) compileIdentSwitch(con CContext, node *parse.CommandNode) (out string, val reflect.Value, literal bool, notident bool) {
c.dumpCall("compileIdentSwitch", con, node)
litString := func(inner string, bytes bool) {
if !bytes {
inner = "StringToBytes(" + inner + ")"
}
out = "w.Write(" + inner + ")\n"
literal = true
}
ArgLoop:
for pos := 0; pos < len(node.Args); pos++ {
id := node.Args[pos]
c.detail("pos:", pos)
c.detail("ID:", id)
switch id.String() {
case "not":
out += "!"
case "or", "and":
var rout string
pos, rout = c.compareJoin(con, pos, node, c.funcMap[id.String()].(string)) // TODO: Test this
out += rout
case "le", "lt", "gt", "ge", "eq", "ne":
out += c.compareFunc(con, pos, node, c.funcMap[id.String()].(string))
break ArgLoop
case "add", "subtract", "divide", "multiply":
rout, rval := c.simpleMath(con, pos, node, c.funcMap[id.String()].(string))
out += rout
val = rval
break ArgLoop
case "elapsed":
leftOperand := node.Args[pos+1].String()
leftParam, _ := c.compileIfVarSub(con, leftOperand)
// TODO: Refactor this
// TODO: Validate that this is actually a time.Time
litString("time.Since("+leftParam+").String()", false)
c.importMap["time"] = "time"
break ArgLoop
case "dock":
// TODO: Implement string literals properly
leftOperand := node.Args[pos+1].String()
rightOperand := node.Args[pos+2].String()
if len(leftOperand) == 0 || len(rightOperand) == 0 {
panic("The left or right operand for function dock cannot be left blank")
}
leftParam := leftOperand
if leftOperand[0] != '"' {
leftParam, _ = c.compileIfVarSub(con, leftParam)
}
if rightOperand[0] == '"' {
panic("The right operand for function dock cannot be a string")
}
rightParam, val3 := c.compileIfVarSub(con, rightOperand)
if !val3.IsValid() {
panic("val3 is invalid")
}
val = val3
// TODO: Refactor this
litString("c.BuildWidget("+leftParam+","+rightParam+")", false)
break ArgLoop
case "hasWidgets":
// TODO: Implement string literals properly
leftOperand := node.Args[pos+1].String()
rightOperand := node.Args[pos+2].String()
if len(leftOperand) == 0 || len(rightOperand) == 0 {
panic("The left or right operand for function dock cannot be left blank")
}
leftParam := leftOperand
if leftOperand[0] != '"' {
leftParam, _ = c.compileIfVarSub(con, leftParam)
}
if rightOperand[0] == '"' {
panic("The right operand for function dock cannot be a string")
}
rightParam, val3 := c.compileIfVarSub(con, rightOperand)
if !val3.IsValid() {
panic("val3 is invalid")
}
val = val3
// TODO: Refactor this
out = "c.HasWidgets(" + leftParam + "," + rightParam + ")"
literal = true
break ArgLoop
case "lang":
// TODO: Implement string literals properly
leftOperand := node.Args[pos+1].String()
if len(leftOperand) == 0 {
panic("The left operand for the language string cannot be left blank")
}
if leftOperand[0] == '"' {
// ! Slightly crude but it does the job
leftParam := strings.Replace(leftOperand, "\"", "", -1)
c.langIndexToName = append(c.langIndexToName, leftParam)
notident = true
con.PushPhrase(len(c.langIndexToName) - 1)
} else {
leftParam := leftOperand
if leftOperand[0] != '"' {
leftParam, _ = c.compileIfVarSub(con, leftParam)
}
// TODO: Add an optimisation if it's a string literal passsed in from a parent template rather than a true dynamic
litString("phrases.GetTmplPhrasef("+leftParam+")", false)
c.importMap[langPkg] = langPkg
}
break ArgLoop
case "langf":
// TODO: Implement string literals properly
leftOperand := node.Args[pos+1].String()
if len(leftOperand) == 0 {
panic("The left operand for the language string cannot be left blank")
}
if leftOperand[0] != '"' {
panic("Phrase names cannot be dynamic")
}
var olist []string
for i := pos + 2; i < len(node.Args); i++ {
op := node.Args[i].String()
if op != "" {
if op[0] == '.' || op[0] == '$' {
panic("langf args cannot be dynamic")
}
if op[0] != '"' && !unicode.IsDigit(rune(op[0])) {
break
}
olist = append(olist, op)
}
}
if len(olist) == 0 {
panic("You must provide parameters for langf")
}
ob := ","
for _, op := range olist {
allNum := true
for _, o := range op {
if !unicode.IsDigit(o) {
allNum = false
}
}
if allNum {
ob += strings.Replace(op, "\"", "\\\"", -1) + ","
} else {
ob += ob + ","
}
}
if ob != "" {
ob = ob[:len(ob)-1]
}
// TODO: Implement string literals properly
// ! Slightly crude but it does the job
litString("phrases.GetTmplPhrasef("+leftOperand+ob+")", false)
c.importMap[langPkg] = langPkg
break ArgLoop
case "level":
// TODO: Implement level literals
leftOperand := node.Args[pos+1].String()
if len(leftOperand) == 0 {
panic("The leftoperand for function level cannot be left blank")
}
leftParam, _ := c.compileIfVarSub(con, leftOperand)
// TODO: Refactor this
litString("phrases.GetLevelPhrase("+leftParam+")", false)
c.importMap[langPkg] = langPkg
break ArgLoop
case "bunit":
// TODO: Implement bunit literals
leftOperand := node.Args[pos+1].String()
if len(leftOperand) == 0 {
panic("The leftoperand for function buint cannot be left blank")
}
leftParam, _ := c.compileIfVarSub(con, leftOperand)
out = "{\nbyteFloat, unit := c.ConvertByteUnit(float64(" + leftParam + "))\n"
out += "w.Write(fmt.Sprintf(\"%.1f\", byteFloat) + unit)\n"
literal = true
break ArgLoop
case "abstime":
// TODO: Implement level literals
leftOperand := node.Args[pos+1].String()
if len(leftOperand) == 0 {
panic("The leftoperand for function abstime cannot be left blank")
}
leftParam, _ := c.compileIfVarSub(con, leftOperand)
// TODO: Refactor this
litString(leftParam+".Format(\"2006-01-02 15:04:05\")", false)
break ArgLoop
case "reltime":
// TODO: Implement level literals
leftOperand := node.Args[pos+1].String()
if len(leftOperand) == 0 {
panic("The leftoperand for function reltime cannot be left blank")
}
leftParam, _ := c.compileIfVarSub(con, leftOperand)
// TODO: Refactor this
litString("c.RelativeTime("+leftParam+")", false)
break ArgLoop
case "scope":
literal = true
break ArgLoop
case "dyntmpl":
var pageParam, headParam string
// TODO: Implement string literals properly
// TODO: Should we check to see if pos+3 is within the bounds of the slice?
nameOperand := node.Args[pos+1].String()
pageOperand := node.Args[pos+2].String()
headOperand := node.Args[pos+3].String()
if len(nameOperand) == 0 || len(pageOperand) == 0 || len(headOperand) == 0 {
panic("None of the three operands for function dyntmpl can be left blank")
}
nameParam := nameOperand
if nameOperand[0] != '"' {
nameParam, _ = c.compileIfVarSub(con, nameParam)
}
if pageOperand[0] == '"' {
panic("The page operand for function dyntmpl cannot be a string")
}
if headOperand[0] == '"' {
panic("The head operand for function dyntmpl cannot be a string")
}
pageParam, val3 := c.compileIfVarSub(con, pageOperand)
if !val3.IsValid() {
panic("val3 is invalid")
}
headParam, val4 := c.compileIfVarSub(con, headOperand)
if !val4.IsValid() {
panic("val4 is invalid")
}
val = val4
// TODO: Refactor this
// TODO: Call the template function directly rather than going through RunThemeTemplate to eliminate a round of indirection?
out = "{\nerr := " + headParam + ".Theme.RunTmpl(" + nameParam + "," + pageParam + ",w)\n"
out += "if err != nil {\nreturn err\n}\n}\n"
literal = true
break ArgLoop
case "flush":
if c.lang == "js" {
continue
}
out = "if fl, ok := iw.(http.Flusher); ok {\n"
out += "fl.Flush()\n"
out += "}\n"
literal = true
c.importMap["net/http"] = "net/http"
break ArgLoop
default:
c.detail("Variable!")
if len(node.Args) > (pos + 1) {
nextNode := node.Args[pos+1].String()
if nextNode == "or" || nextNode == "and" {
continue
}
}
out += c.compileIfVarSubN(con, id.String())
}
}
c.retCall("compileIdentSwitch", out, val, literal)
return out, val, literal, notident
}
func (c *CTemplateSet) compileReflectSwitch(con CContext, node *parse.CommandNode) (out string, outVal reflect.Value) {
c.dumpCall("compileReflectSwitch", con, node)
firstWord := node.Args[0]
switch n := firstWord.(type) {
case *parse.FieldNode:
if c.config.SuperDebug {
c.logger.Println("Field Node:", n.Ident)
for _, id := range n.Ident {
c.logger.Println("Field Bit:", id)
}
}
/* Use reflect to determine if the field is for a method, otherwise assume it's a variable. Coming Soon. */
return c.compileIfVarSub(con, n.String())
case *parse.ChainNode:
c.detail("Chain Node:", n.Node)
c.detail("node.Args:", node.Args)
case *parse.DotNode:
return con.VarHolder, con.HoldReflect
case *parse.NilNode:
panic("Nil is not a command x.x")
default:
//panic("I don't know what node this is")
}
return out, outVal
}
func (c *CTemplateSet) compileIfVarSubN(con CContext, varname string) (out string) {
c.dumpCall("compileIfVarSubN", con, varname)
out, _ = c.compileIfVarSub(con, varname)
return out
}
func (c *CTemplateSet) compileIfVarSub(con CContext, varname string) (out string, val reflect.Value) {
c.dumpCall("compileIfVarSub", con, varname)
cur := con.HoldReflect
if varname[0] != '.' && varname[0] != '$' {
return varname, cur
}
stepInterface := func() {
nobreak := (cur.Type().Name() == "nobreak")
c.detailf("cur.Type().Name(): %+v\n", cur.Type().Name())
if cur.Kind() == reflect.Interface && !nobreak {
cur = cur.Elem()
out += ".(" + cur.Type().Name() + ")"
}
}
bits := strings.Split(varname, ".")
if varname[0] == '$' {
var res VarItemReflect
if varname[1] == '.' {
res = c.localVars[con.TemplateName]["."]
} else {
res = c.localVars[con.TemplateName][strings.TrimPrefix(bits[0], "$")]
}
out += res.Destination
cur = res.Value
if cur.Kind() == reflect.Interface {
cur = cur.Elem()
}
} else {
out += con.VarHolder
stepInterface()
}
bits[0] = strings.TrimPrefix(bits[0], "$")
dumpKind := func(pre string) {
c.detail(pre+" Kind:", cur.Kind())
c.detail(pre+" Type:", cur.Type().Name())
}
dumpKind("Cur")
for _, bit := range bits {
c.detail("Variable Field:", bit)
if bit == "" {
continue
}
// TODO: Fix this up so that it works for regular pointers and not just struct pointers. Ditto for the other cur.Kind() == reflect.Ptr we have in this file
if cur.Kind() == reflect.Ptr {
c.detail("Looping over pointer")
for cur.Kind() == reflect.Ptr {
cur = cur.Elem()
}
c.detail("Data Kind:", cur.Kind().String())
c.detail("Field Bit:", bit)
}
cur = cur.FieldByName(bit)
out += "." + bit
if !cur.IsValid() {
c.logger.Println("cur: ", cur)
panic(out + "^\n" + "Invalid value. Maybe, it doesn't exist?")
}
stepInterface()
if !cur.IsValid() {
c.logger.Println("cur: ", cur)
panic(out + "^\n" + "Invalid value. Maybe, it doesn't exist?")
}
dumpKind("Data")
}
c.detail("Out Value:", out)
dumpKind("Out")
for _, varItem := range c.varList {
if strings.HasPrefix(out, varItem.Destination) {
out = strings.Replace(out, varItem.Destination, varItem.Name, 1)
}
}
_, ok := c.stats[out]
if ok {
c.stats[out]++
} else {
c.stats[out] = 1
}
c.retCall("compileIfVarSub", out, cur)
return out, cur
}
func (c *CTemplateSet) compileBoolSub(con CContext, varname string) string {
c.dumpCall("compileBoolSub", con, varname)
out, val := c.compileIfVarSub(con, varname)
// TODO: What if it's a pointer or an interface? I *think* we've got pointers handled somewhere, but not interfaces which we don't know the types of at compile time
switch val.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
out += " > 0"
case reflect.Bool: // Do nothing
case reflect.String:
out += " != \"\""
case reflect.Slice, reflect.Map:
out = "len(" + out + ") != 0"
default:
c.logger.Println("Variable Name:", varname)
c.logger.Println("Variable Holder:", con.VarHolder)
c.logger.Println("Variable Kind:", con.HoldReflect.Kind())
panic("I don't know what this variable's type is o.o\n")
}
c.retCall("compileBoolSub", out)
return out
}
// For debugging the template generator
func (c *CTemplateSet) debugParam(param interface{}, depth int) (pstr string) {
switch p := param.(type) {
case CContext:
return "con,"
case reflect.Value:
if p.Kind() == reflect.Ptr || p.Kind() == reflect.Interface {
for p.Kind() == reflect.Ptr || p.Kind() == reflect.Interface {
if p.Kind() == reflect.Ptr {
pstr += "*"
} else {
pstr += "£"
}
p = p.Elem()
}
}
kind := p.Kind().String()
if kind != "struct" {
pstr += kind
} else {
pstr += p.Type().Name()
}
return pstr + ","
case string:
return "\"" + p + "\","
case int:
return strconv.Itoa(p) + ","
case bool:
if p {
return "true,"
}
return "false,"
case func(string) string:
if p == nil {
return "nil,"
}
return "func(string) string),"
default:
return "?,"
}
}
func (c *CTemplateSet) dumpCall(name string, params ...interface{}) {
var pstr string
for _, param := range params {
pstr += c.debugParam(param, 0)
}
if len(pstr) > 0 {
pstr = pstr[:len(pstr)-1]
}
c.detail("called " + name + "(" + pstr + ")")
}
func (c *CTemplateSet) retCall(name string, params ...interface{}) {
var pstr string
for _, param := range params {
pstr += c.debugParam(param, 0)
}
if len(pstr) > 0 {
pstr = pstr[:len(pstr)-1]
}
c.detail("returned from " + name + " => (" + pstr + ")")
}
func buildUserExprs(holder string) ([]string, []string) {
userExprs := []string{
holder + ".CurrentUser.Loggedin",
holder + ".CurrentUser.IsSuperMod",
holder + ".CurrentUser.IsAdmin",
}
negUserExprs := []string{
"!" + holder + ".CurrentUser.Loggedin",
"!" + holder + ".CurrentUser.IsSuperMod",
"!" + holder + ".CurrentUser.IsAdmin",
}
return userExprs, negUserExprs
}
func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.Value, assLines string, onEnd func(string) string) {
c.dumpCall("compileVarSub", con, varname, val, assLines, onEnd)
defer c.retCall("compileVarSub")
if onEnd == nil {
onEnd = func(in string) string {
return in
}
}
// Is this a literal string?
if len(varname) != 0 && varname[0] == '"' {
con.Push("lvarsub", onEnd(assLines+"w.Write(StringToBytes("+varname+"))\n"))
return
}
for _, varItem := range c.varList {
if strings.HasPrefix(varname, varItem.Destination) {
varname = strings.Replace(varname, varItem.Destination, varItem.Name, 1)
}
}
_, ok := c.stats[varname]
if ok {
c.stats[varname]++
} else {
c.stats[varname] = 1
}
if val.Kind() == reflect.Interface {
val = val.Elem()
}
if val.Kind() == reflect.Ptr {
for val.Kind() == reflect.Ptr {
val = val.Elem()
varname = "*" + varname
}
}
c.detail("varname: ", varname)
c.detail("assLines: ", assLines)
var base string
switch val.Kind() {
case reflect.Int:
c.importMap["strconv"] = "strconv"
base = "StringToBytes(strconv.Itoa(" + varname + "))"
case reflect.Bool:
// TODO: Take c.memberOnly into account
// TODO: Make this a template fragment so more optimisations can be applied to this
// TODO: De-duplicate this logic
userExprs, negUserExprs := buildUserExprs(con.RootHolder)
if c.guestOnly {
c.detail("optimising away member branch")
if inSlice(userExprs, varname) {
c.detail("positive conditional:", varname)
c.addText(con, []byte("false"))
return
} else if inSlice(negUserExprs, varname) {
c.detail("negative conditional:", varname)
c.addText(con, []byte("true"))
return
}
} else if c.memberOnly {
c.detail("optimising away guest branch")
if (con.RootHolder + ".CurrentUser.Loggedin") == varname {
c.detail("positive conditional:", varname)
c.addText(con, []byte("true"))
return
} else if ("!" + con.RootHolder + ".CurrentUser.Loggedin") == varname {
c.detail("negative conditional:", varname)
c.addText(con, []byte("false"))
return
}
}
con.Push("startif", "if "+varname+" {\n")
c.addText(con, []byte("true"))
con.Push("endif", "} ")
con.Push("startelse", "else {\n")
c.addText(con, []byte("false"))
con.Push("endelse", "}\n")
return
case reflect.Slice:
if val.Len() == 0 {
c.critical("varname:", varname)
panic("The sample data needs at-least one or more elements for the slices. We're looking into removing this requirement at some point!")
}
item := val.Index(0)
if item.Type().Name() != "uint8" { // uint8 == byte, complicated because it's a type alias
panic("unable to format " + item.Type().Name() + " as text")
}
base = varname
case reflect.String:
if val.Type().Name() != "string" && !strings.HasPrefix(varname, "string(") {
varname = "string(" + varname + ")"
}
base = "StringToBytes(" + varname + ")"
// We don't to waste time on this conversion / w.Write call when guests don't have sessions
// TODO: Implement this properly
if c.guestOnly && base == "StringToBytes("+con.RootHolder+".CurrentUser.Session))" {
return
}
case reflect.Int64:
c.importMap["strconv"] = "strconv"
base = "StringToBytes(strconv.FormatInt(" + varname + ", 10))"
case reflect.Struct:
// TODO: Avoid clashing with other packages which have structs named Time
if val.Type().Name() == "Time" {
base = "StringToBytes(" + varname + ".String())"
} else {
if !val.IsValid() {
panic(assLines + varname + "^\n" + "Invalid value. Maybe, it doesn't exist?")
}
c.logger.Println("Unknown Struct Name:", varname)
c.logger.Println("Unknown Struct:", val.Type().Name())
panic("-- I don't know what this variable's type is o.o\n")
}
default:
if !val.IsValid() {
panic(assLines + varname + "^\n" + "Invalid value. Maybe, it doesn't exist?")
}
c.logger.Println("Unknown Variable Name:", varname)
c.logger.Println("Unknown Kind:", val.Kind())
c.logger.Println("Unknown Type:", val.Type().Name())
panic("-- I don't know what this variable's type is o.o\n")
}
c.detail("base: ", base)
if assLines == "" {
con.Push("varsub", base)
} else {
con.Push("lvarsub", onEnd(assLines+base))
}
}
func (c *CTemplateSet) compileSubTemplate(pcon CContext, node *parse.TemplateNode) {
c.dumpCall("compileSubTemplate", pcon, node)
defer c.retCall("compileSubTemplate")
c.detail("Template Node: ", node.Name)
// TODO: Cascade errors back up the tree to the caller?
content, err := c.loadTemplate(c.fileDir, node.Name)
if err != nil {
c.logger.Fatal(err)
}
tree := parse.New(node.Name, c.funcMap)
var treeSet = make(map[string]*parse.Tree)
tree, err = tree.Parse(content, "{{", "}}", treeSet, c.funcMap)
if err != nil {
c.logger.Fatal(err)
}
fname := strings.TrimSuffix(node.Name, filepath.Ext(node.Name))
if c.themeName != "" {
_, ok := c.perThemeTmpls[fname]
if !ok {
c.detail("fname not in c.perThemeTmpls")
c.detail("c.perThemeTmpls", c.perThemeTmpls)
}
fname += "_" + c.themeName
}
if c.guestOnly {
fname += "_guest"
} else if c.memberOnly {
fname += "_member"
}
con := pcon
con.VarHolder = "tmpl_" + fname + "_vars"
con.TemplateName = fname
if node.Pipe != nil {
for _, cmd := range node.Pipe.Cmds {
switch p := cmd.Args[0].(type) {
case *parse.FieldNode:
// TODO: Incomplete but it should cover the basics
cur := pcon.HoldReflect
var varBit string
if cur.Kind() == reflect.Interface {
cur = cur.Elem()
varBit += ".(" + cur.Type().Name() + ")"
}
for _, id := range p.Ident {
c.detail("Data Kind:", cur.Kind().String())
c.detail("Field Bit:", id)
cur = c.skipStructPointers(cur, id)
c.checkIfValid(cur, pcon.VarHolder, pcon.HoldReflect, varBit, false)
if cur.Kind() != reflect.Interface {
cur = cur.FieldByName(id)
varBit += "." + id
}
// TODO: Handle deeply nested pointers mixed with interfaces mixed with pointers better
if cur.Kind() == reflect.Interface {
cur = cur.Elem()
varBit += ".("
// TODO: Surely, there's a better way of doing this?
if cur.Type().PkgPath() != "main" && cur.Type().PkgPath() != "" {
c.importMap["html/template"] = "html/template"
varBit += strings.TrimPrefix(cur.Type().PkgPath(), "html/") + "."
}
varBit += cur.Type().Name() + ")"
}
}
con.VarHolder = pcon.VarHolder + varBit
con.HoldReflect = cur
case *parse.DotNode:
con.VarHolder = pcon.VarHolder
con.HoldReflect = pcon.HoldReflect
case *parse.NilNode:
panic("Nil is not a command x.x")
default:
c.critical("Unknown Param Type:", p)
pvar := reflect.ValueOf(p)
c.critical("param kind:", pvar.Kind().String())
c.critical("param type:", pvar.Type().Name())
if pvar.Kind() == reflect.Ptr {
c.critical("Looping over pointer")
for pvar.Kind() == reflect.Ptr {
pvar = pvar.Elem()
}
c.critical("concrete kind:", pvar.Kind().String())
c.critical("concrete type:", pvar.Type().Name())
}
panic("")
}
}
}
c.templateList[fname] = tree
subtree := c.templateList[fname]
c.detail("subtree.Root", subtree.Root)
c.localVars[fname] = make(map[string]VarItemReflect)
c.localVars[fname]["."] = VarItemReflect{".", con.VarHolder, con.HoldReflect}
c.fragmentCursor[fname] = 0
var startBit, endBit string
if con.LoopDepth != 0 {
startBit = "{\n"
endBit = "}\n"
}
con.StartTemplate(startBit)
c.rootIterate(subtree, con)
con.EndTemplate(endBit)
//c.templateFragmentCount[fname] = c.fragmentCursor[fname] + 1
_, ok := c.fragOnce[fname]
if !ok {
c.fragOnce[fname] = true
}
// map[string]map[string]bool
c.detail("overridenTrack loop")
c.detail("fname:", fname)
for themeName, track := range c.overridenTrack {
c.detail("themeName:", themeName)
c.detailf("track: %+v\n", track)
croot, ok := c.overridenRoots[themeName]
if !ok {
croot = make(map[string]bool)
c.overridenRoots[themeName] = croot
}
c.detailf("croot: %+v\n", croot)
for tmplName, _ := range track {
cname := tmplName
if c.guestOnly {
cname += "_guest"
} else if c.memberOnly {
cname += "_member"
}
c.detail("cname:", cname)
if fname == cname {
c.detail("match")
croot[strings.TrimSuffix(strings.TrimSuffix(con.RootTemplateName, "_guest"), "_member")] = true
} else {
c.detail("no match")
}
}
}
c.detailf("c.overridenRoots: %+v\n", c.overridenRoots)
}
func (c *CTemplateSet) loadTemplate(fileDir string, name string) (content string, err error) {
c.dumpCall("loadTemplate", fileDir, name)
c.detail("c.themeName: ", c.themeName)
if c.themeName != "" {
c.detail("per-theme override: ", "./themes/"+c.themeName+"/overrides/"+name)
res, err := ioutil.ReadFile("./themes/" + c.themeName + "/overrides/" + name)
if err == nil {
content = string(res)
if c.config.Minify {
content = Minify(content)
}
return content, nil
}
c.detail("override err: ", err)
}
res, err := ioutil.ReadFile(c.fileDir + "overrides/" + name)
if err != nil {
c.detail("override path: ", c.fileDir+"overrides/"+name)
c.detail("override err: ", err)
res, err = ioutil.ReadFile(c.fileDir + name)
if err != nil {
return "", err
}
}
content = string(res)
if c.config.Minify {
content = Minify(content)
}
return content, nil
}
func (c *CTemplateSet) afterTemplate(con CContext, startIndex int) {
c.dumpCall("afterTemplate", con, startIndex)
defer c.retCall("afterTemplate")
var loopDepth = 0
var outBuf = *con.OutBuf
var varcounts = make(map[string]int)
var loopStart = startIndex
if outBuf[startIndex].Type == "startloop" && (len(outBuf) > startIndex+1) {
loopStart++
}
// Exclude varsubs within loops for now
for i := loopStart; i < len(outBuf); i++ {
item := outBuf[i]
c.detail("item:", item)
if item.Type == "startloop" {
loopDepth++
c.detail("loopDepth:", loopDepth)
} else if item.Type == "endloop" {
loopDepth--
c.detail("loopDepth:", loopDepth)
if loopDepth == -1 {
break
}
} else if item.Type == "varsub" && loopDepth == 0 {
count := varcounts[item.Body]
varcounts[item.Body] = count + 1
c.detail("count " + strconv.Itoa(count) + " for " + item.Body)
c.detail("loopDepth:", loopDepth)
}
}
var varstr string
var i int
var varmap = make(map[string]int)
for name, count := range varcounts {
if count > 1 {
varstr += "var cached_var_" + strconv.Itoa(i) + " = " + name + "\n"
varmap[name] = i
i++
}
}
// Exclude varsubs within loops for now
loopDepth = 0
for i := loopStart; i < len(outBuf); i++ {
item := outBuf[i]
if item.Type == "startloop" {
loopDepth++
} else if item.Type == "endloop" {
loopDepth--
if loopDepth == -1 {
break
}
} else if item.Type == "varsub" && loopDepth == 0 {
index, ok := varmap[item.Body]
if ok {
item.Body = "cached_var_" + strconv.Itoa(index)
item.Type = "cvarsub"
outBuf[i] = item
}
}
}
con.AttachVars(varstr, startIndex)
}
// TODO: Should we rethink the way the log methods work or their names?
func (c *CTemplateSet) detail(args ...interface{}) {
if c.config.SuperDebug {
c.logger.Println(args...)
}
}
func (c *CTemplateSet) detailf(left string, args ...interface{}) {
if c.config.SuperDebug {
c.logger.Printf(left, args...)
}
}
func (c *CTemplateSet) error(args ...interface{}) {
if c.config.Debug {
c.logger.Println(args...)
}
}
func (c *CTemplateSet) critical(args ...interface{}) {
c.logger.Println(args...)
}