Split the transpiled topic alt route into a guest part and a member part for extra speed.

Fixed a bug in the template fragment coalescer.
Added the loadTemplate function to help reduce the amount of duplicated code in the template generator.
Added a couple of helper methods to CContext to reduce the amount of possibly error prone boilerplate.
This commit is contained in:
Azareal 2018-11-26 15:08:10 +10:00
parent 826330035f
commit 50fef78078
7 changed files with 267 additions and 80 deletions

View File

@ -43,6 +43,8 @@ func interpretedTopicTemplate(pi TopicPage, w io.Writer) error {
// nolint
var Template_topic_handle = interpretedTopicTemplate
var Template_topic_alt_handle = interpretedTopicTemplate
var Template_topic_alt_guest_handle = interpretedTopicTemplate
var Template_topic_alt_member_handle = interpretedTopicTemplate
// nolint
var Template_topics_handle = func(pi TopicListPage, w io.Writer) error {
@ -172,6 +174,12 @@ func tmplInitHeaders(user User, user2 User, user3 User) (*Header, *Header, *Head
return header, buildHeader(user2), buildHeader(user3)
}
type TmplLoggedin struct {
Stub string
Guest string
Member string
}
// ? - Add template hooks?
func CompileTemplates() error {
var config tmpl.CTemplateConfig
@ -210,15 +218,24 @@ func CompileTemplates() error {
var compile = func(name string, expects string, expectsInt interface{}) (tmpl string, err error) {
return c.Compile(name+".html", "templates/", expects, expectsInt, varList)
}
var compileByLoggedin = func(name string, expects string, expectsInt interface{}) (tmpl TmplLoggedin, err error) {
stub, guest, member, err := c.CompileByLoggedin(name+".html", "templates/", expects, expectsInt, varList)
return TmplLoggedin{stub, guest, member}, err
}
header.Title = "Topic Name"
tpage := TopicPage{header, replyList, topic, &Forum{ID: 1, Name: "Hahaha"}, poll, 1, 1}
tpage.Forum.Link = BuildForumURL(NameToSlug(tpage.Forum.Name), tpage.Forum.ID)
topicIDTmpl, err := compile("topic", "common.TopicPage", tpage)
topicTmpl, err := compile("topic", "common.TopicPage", tpage)
if err != nil {
return err
}
topicIDAltTmpl, err := compile("topic_alt", "common.TopicPage", tpage)
/*topicAltTmpl, err := compile("topic_alt", "common.TopicPage", tpage)
if err != nil {
return err
}*/
topicAltTmpl, err := compileByLoggedin("topic_alt", "common.TopicPage", tpage)
if err != nil {
return err
}
@ -309,17 +326,28 @@ func CompileTemplates() error {
}
var wg sync.WaitGroup
var writeTemplate = func(name string, content string) {
var writeTemplate = func(name string, content interface{}) {
log.Print("Writing template '" + name + "'")
if content == "" {
log.Fatal("No content body")
var writeTmpl = func(name string, content string) {
if content == "" {
log.Fatal("No content body for " + name)
}
err := writeFile("./template_"+name+".go", content)
if err != nil {
log.Fatal(err)
}
}
wg.Add(1)
go func() {
err := writeFile("./template_"+name+".go", content)
if err != nil {
log.Fatal(err)
switch content := content.(type) {
case string:
writeTmpl(name, content)
case TmplLoggedin:
writeTmpl(name, content.Stub)
writeTmpl(name+"_guest", content.Guest)
writeTmpl(name+"_member", content.Member)
}
wg.Done()
}()
@ -341,8 +369,8 @@ func CompileTemplates() error {
}
log.Print("Writing the templates")
writeTemplate("topic", topicIDTmpl)
writeTemplate("topic_alt", topicIDAltTmpl)
writeTemplate("topic", topicTmpl)
writeTemplate("topic_alt", topicAltTmpl)
writeTemplate("profile", profileTmpl)
writeTemplate("forums", forumsTmpl)
writeTemplate("topics", topicListTmpl)
@ -415,11 +443,11 @@ func CompileJSTemplates() error {
header.Title = "Topic Name"
tpage := TopicPage{header, replyList, topic, &Forum{ID: 1, Name: "Hahaha"}, poll, 1, 1}
tpage.Forum.Link = BuildForumURL(NameToSlug(tpage.Forum.Name), tpage.Forum.ID)
topicIDTmpl, err := c.Compile("topic_posts.html", "templates/", "common.TopicPage", tpage, varList)
topicPostsTmpl, err := c.Compile("topic_posts.html", "templates/", "common.TopicPage", tpage, varList)
if err != nil {
return err
}
topicIDAltTmpl, err := c.Compile("topic_alt_posts.html", "templates/", "common.TopicPage", tpage, varList)
topicAltPostsTmpl, err := c.Compile("topic_alt_posts.html", "templates/", "common.TopicPage", tpage, varList)
if err != nil {
return err
}
@ -443,8 +471,8 @@ func CompileJSTemplates() error {
}
writeTemplate("alert", alertTmpl)
writeTemplate("topics_topic", topicListItemTmpl)
writeTemplate("topic_posts", topicIDTmpl)
writeTemplate("topic_alt_posts", topicIDAltTmpl)
writeTemplate("topic_posts", topicPostsTmpl)
writeTemplate("topic_alt_posts", topicAltPostsTmpl)
writeTemplateList(c, &wg, dirPrefix)
return nil
}

View File

@ -25,6 +25,7 @@ type OutBufferFrame struct {
}
type CContext struct {
RootHolder string
VarHolder string
HoldReflect reflect.Value
TemplateName string
@ -34,17 +35,17 @@ type CContext struct {
func (con *CContext) Push(nType string, body string) (index int) {
*con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, nType, con.TemplateName, nil, nil})
return len(*con.OutBuf) - 1
return con.LastBufIndex()
}
func (con *CContext) PushText(body string, fragIndex int, fragOutIndex int) (index int) {
*con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, "text", con.TemplateName, fragIndex, fragOutIndex})
return len(*con.OutBuf) - 1
return con.LastBufIndex()
}
func (con *CContext) PushPhrase(body string, langIndex int) (index int) {
*con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, "lang", con.TemplateName, langIndex, nil})
return len(*con.OutBuf) - 1
return con.LastBufIndex()
}
func (con *CContext) StartLoop(body string) (index int) {
@ -57,8 +58,7 @@ func (con *CContext) EndLoop(body string) (index int) {
}
func (con *CContext) StartTemplate(body string) (index int) {
*con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, "starttemplate", con.TemplateName, nil, nil})
return len(*con.OutBuf) - 1
return con.addFrame(body, "starttemplate", nil, nil)
}
func (con *CContext) EndTemplate(body string) (index int) {
@ -74,3 +74,25 @@ func (con *CContext) AttachVars(vars string, index int) {
node.Body += vars
outBuf[index] = node
}
func (con *CContext) addFrame(body string, ftype string, extra1 interface{}, extra2 interface{}) (index int) {
*con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, ftype, con.TemplateName, extra1, extra2})
return con.LastBufIndex()
}
func (con *CContext) LastBufIndex() int {
return len(*con.OutBuf) - 1
}
func (con *CContext) DiscardAndAfter(index int) {
outBuf := *con.OutBuf
if len(outBuf) <= index {
return
}
if index == 0 {
outBuf = nil
} else {
outBuf = outBuf[:index]
}
*con.OutBuf = outBuf
}

View File

@ -56,15 +56,13 @@ type CTemplateSet struct {
hasDispInt bool
localDispStructIndex int
langIndexToName []string
guestOnly bool
memberOnly bool
stats map[string]int
previousNode parse.NodeType
currentNode parse.NodeType
nextNode parse.NodeType
//tempVars map[string]string
config CTemplateConfig
baseImportMap map[string]string
buildTags string
expectsInt interface{}
}
func NewCTemplateSet() *CTemplateSet {
@ -126,10 +124,86 @@ type Skipper struct {
Index int
}
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
}
if len(imports) > 0 {
for _, importItem := range imports {
c.importMap[importItem] = importItem
}
}
var importList string
for _, item := range c.importMap {
importList += "import \"" + item + "\"\n"
}
fname := strings.TrimSuffix(name, filepath.Ext(name))
c.importMap["github.com/Azareal/Gosora/common"] = "github.com/Azareal/Gosora/common"
stub = `package ` + c.config.PackageName + `
` + importList + `
`
if !c.config.SkipInitBlock {
stub += "// nolint\nfunc init() {\n"
if !c.config.SkipHandles {
stub += "\tcommon.Template_" + fname + "_handle = Template_" + fname + "\n"
stub += "\tcommon.Ctemplates = append(common.Ctemplates,\"" + fname + "\")\n\tcommon.TmplPtrMap[\"" + fname + "\"] = &common.Template_" + fname + "_handle\n"
}
if !c.config.SkipTmplPtrMap {
stub += "\tcommon.TmplPtrMap[\"o_" + fname + "\"] = Template_" + fname + "\n"
}
stub += "}\n\n"
}
stub += `
// nolint
func Template_` + fname + `(tmpl_` + fname + `_vars ` + expects + `, w io.Writer) error {
if tmpl_` + fname + `_vars.CurrentUser.Loggedin {
return Template_` + fname + `_member(tmpl_` + fname + `_vars, w)
}
return Template_` + fname + `_guest(tmpl_` + fname + `_vars, w)
}`
c.fileDir = fileDir
content, err := c.loadTemplate(c.fileDir, name)
if err != nil {
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 {
fmt.Println("Compiling template '" + name + "'")
}
c.fileDir = fileDir
content, err := c.loadTemplate(c.fileDir, name)
if err != nil {
return "", err
}
return c.compile(name, content, expects, expectsInt, varList, imports...)
}
func (c *CTemplateSet) compile(name string, content, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (out string, err error) {
c.importMap = map[string]string{}
for index, item := range c.baseImportMap {
c.importMap[index] = item
@ -140,26 +214,10 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe
}
}
c.fileDir = fileDir
c.varList = varList
c.hasDispInt = false
c.localDispStructIndex = 0
c.stats = make(map[string]int)
c.expectsInt = expectsInt
res, err := ioutil.ReadFile(fileDir + "overrides/" + name)
if err != nil {
c.detail("override path: ", fileDir+"overrides/"+name)
c.detail("override err: ", err)
res, err = ioutil.ReadFile(fileDir + name)
if err != nil {
return "", err
}
}
content := string(res)
if c.config.Minify {
content = minify(content)
}
tree := parse.New(name, c.funcMap)
var treeSet = make(map[string]*parse.Tree)
@ -170,8 +228,15 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe
c.detail(name)
fname := strings.TrimSuffix(name, filepath.Ext(name))
if c.guestOnly {
fname += "_guest"
} else if c.memberOnly {
fname += "_member"
}
var outBuf []OutBufferFrame
con := CContext{VarHolder: "tmpl_" + fname + "_vars", HoldReflect: reflect.ValueOf(expectsInt), TemplateName: fname, OutBuf: &outBuf}
var rootHold = "tmpl_" + fname + "_vars"
con := CContext{RootHolder: rootHold, VarHolder: rootHold, HoldReflect: reflect.ValueOf(expectsInt), TemplateName: fname, OutBuf: &outBuf}
c.templateList = map[string]*parse.Tree{fname: tree}
c.detail(c.templateList)
c.localVars = make(map[string]map[string]VarItemReflect)
@ -264,6 +329,7 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe
c.detail("text frame:")
c.detail(frame)
oid := fid
c.detail("oid:", oid)
skipBlock, ok := skipped[frame.TemplateName]
if !ok {
skipBlock = &SkipBlock{make(map[int]int), 0, 0}
@ -271,7 +337,10 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe
}
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)])
@ -279,13 +348,17 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe
c.fragBuf[frame.Extra2.(int)].Body += c.fragBuf[next.Extra2.(int)].Body
c.fragBuf[next.Extra2.(int)].Seen = true
fid++
skipBlock.LastCount += (fid - oid)
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" {
c.detail(frame.Type + " frame")
fout += "w.Write(" + frame.Body + ")\n"
} else if frame.Type == "identifier" {
c.detailf(frame.Type+" frame:%+v\n", frame)
fout += frame.Body
} else {
c.detail(frame.Type + " frame")
fout += frame.Body
@ -334,14 +407,8 @@ w.Write([]byte(`, " + ", -1)
func (c *CTemplateSet) rootIterate(tree *parse.Tree, con CContext) {
c.dumpCall("rootIterate", tree, con)
c.detail(tree.Root)
treeLength := len(tree.Root.Nodes)
for index, node := range tree.Root.Nodes {
for _, node := range tree.Root.Nodes {
c.detail("Node:", node.String())
c.previousNode = c.currentNode
c.currentNode = node.Type()
if treeLength != (index + 1) {
c.nextNode = tree.Root.Nodes[index+1].Type()
}
c.compileSwitch(con, node)
}
c.retCall("rootIterate")
@ -371,9 +438,54 @@ func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) {
}
c.detail("Expression:", expr)
c.previousNode = c.currentNode
c.currentNode = parse.NodeList
c.nextNode = -1
// Simple member / guest optimisation for now
// TODO: Expand upon this
var inSlice = func(haystack []string, expr string) bool {
for _, needle := range haystack {
if needle == expr {
return true
}
}
return false
}
var userExprs = []string{
con.RootHolder + ".CurrentUser.Loggedin",
con.RootHolder + ".CurrentUser.IsSuperMod",
con.RootHolder + ".CurrentUser.IsAdmin",
}
var negUserExprs = []string{
"!" + con.RootHolder + ".CurrentUser.Loggedin",
"!" + con.RootHolder + ".CurrentUser.IsSuperMod",
"!" + con.RootHolder + ".CurrentUser.IsAdmin",
}
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 {
@ -387,7 +499,7 @@ func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) {
con.Push("endelse", "}\n")
}
case *parse.ListNode:
c.detail("List Node")
c.detailf("List Node: %+v\n", node)
for _, subnode := range node.Nodes {
c.compileSwitch(con, subnode)
}
@ -396,14 +508,10 @@ func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) {
case *parse.TemplateNode:
c.compileSubTemplate(con, node)
case *parse.TextNode:
c.previousNode = c.currentNode
c.currentNode = node.Type()
c.nextNode = 0
tmpText := bytes.TrimSpace(node.Text)
if len(tmpText) == 0 {
return
}
nodeText := string(node.Text)
fragIndex := c.fragmentCursor[con.TemplateName]
_, ok := c.FragOnce[con.TemplateName]
@ -447,6 +555,10 @@ func (c *CTemplateSet) compileRangeNode(con CContext, node *parse.RangeNode) {
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 {
@ -1115,6 +1227,7 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V
c.importMap["strconv"] = "strconv"
base = "[]byte(strconv.Itoa(" + varname + "))"
case reflect.Bool:
// TODO: Take c.guestOnly / c.memberOnly into account
con.Push("startif", "if "+varname+" {\n")
con.Push("varsub", "[]byte(\"true\")")
con.Push("endif", "} ")
@ -1127,6 +1240,11 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V
varname = "string(" + varname + ")"
}
base = "[]byte(" + 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 == "[]byte("+con.RootHolder+".CurrentUser.Session))" {
return
}
case reflect.Int64:
c.importMap["strconv"] = "strconv"
base = "[]byte(strconv.FormatInt(" + varname + ", 10))"
@ -1151,7 +1269,26 @@ func (c *CTemplateSet) compileSubTemplate(pcon CContext, node *parse.TemplateNod
c.dumpCall("compileSubTemplate", pcon, node)
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 {
log.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 {
log.Fatal(err)
}
fname := strings.TrimSuffix(node.Name, filepath.Ext(node.Name))
if c.guestOnly {
fname += "_guest"
} else if c.memberOnly {
fname += "_member"
}
con := pcon
con.VarHolder = "tmpl_" + fname + "_vars"
con.TemplateName = fname
@ -1171,28 +1308,6 @@ func (c *CTemplateSet) compileSubTemplate(pcon CContext, node *parse.TemplateNod
}
}
// TODO: Cascade errors back up the tree to the caller?
res, err := ioutil.ReadFile(c.fileDir + "overrides/" + node.Name)
if err != nil {
c.detail("override path: ", c.fileDir+"overrides/"+node.Name)
c.detail("override err: ", err)
res, err = ioutil.ReadFile(c.fileDir + node.Name)
if err != nil {
log.Fatal(err)
}
}
content := string(res)
if c.config.Minify {
content = minify(content)
}
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 {
log.Fatal(err)
}
c.templateList[fname] = tree
subtree := c.templateList[fname]
c.detail("subtree.Root", subtree.Root)
@ -1216,6 +1331,23 @@ func (c *CTemplateSet) compileSubTemplate(pcon CContext, node *parse.TemplateNod
}
}
func (c *CTemplateSet) loadTemplate(fileDir string, name string) (content string, err error) {
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")

View File

@ -62,6 +62,7 @@ func userRoutes() *RouteGroup {
Action("routes.AccountEditEmailTokenSubmit", "/user/edit/token/", "extraData"),
MemberView("routes.LevelList", "/user/levels/"),
//MemberView("routes.LevelRankings", "/user/rankings/"),
)
}

View File

@ -38,7 +38,7 @@
</div>
<div style="clear: both;"></div>
</nav>
<div class="right_of_nav"><!--{{dock "rightOfNav" .Header }}-->
<div class="right_of_nav">{{/**<!--{{dock "rightOfNav" .Header }}-->**/}}
{{/** TODO: Make this a separate template and load it via the theme docks, here for now so we can rapidly prototype the Nox theme **/}}
{{if eq .Header.Theme.Name "nox"}}
<div class="user_box">

View File

@ -15,12 +15,14 @@
<span class="topic_name_forum_sep hide_on_edit"> / </span>
<a href="{{.Forum.Link}}" class="topic_forum hide_on_edit">{{.Forum.Name}}</a>
{{/** TODO: Does this need to be guarded by a permission? It's only visible in edit mode anyway, which can't be triggered, if they don't have the permission **/}}
{{if .CurrentUser.Loggedin}}
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
{{if .CurrentUser.Perms.EditTopic}}
<input class='show_on_edit topic_name_input' name="topic_name" value='{{.Topic.Title}}' type="text" aria-label="{{lang "topic.title_input_aria"}}" />
<button name="topic-button" class="formbutton show_on_edit submit_edit">{{lang "topic.update_button"}}</button>
{{end}}
{{end}}
{{end}}
<span class="topic_view_count hide_on_edit">{{.Topic.ViewCount}}</span>
{{/** TODO: Inline this CSS **/}}
{{if .Topic.IsClosed}}<span class='username hide_on_micro topic_status_e topic_status_closed hide_on_edit' title='{{lang "status.closed_tooltip"}}' aria-label='{{lang "topic.status_closed_aria"}}' style="font-weight:normal;float: right;position:relative;top:-5px;">&#x1F512;&#xFE0E</span>{{end}}
@ -102,6 +104,7 @@
{{template "topic_alt_posts.html" . }}
</div>
{{if .CurrentUser.Loggedin}}
{{if .CurrentUser.Perms.CreateReply}}
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
<div class="rowblock topic_reply_container">
@ -144,6 +147,7 @@
</div>
{{end}}
{{end}}
{{end}}
</main>

View File

@ -29,7 +29,7 @@
<div class="action_button_right">
<a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.post_like_count_tooltip"}}">{{.LikeCount}}</a>
<a class="action_button created_at hide_on_mobile">{{.RelativeCreatedAt}}</a>
{{if $.CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.IPAddress}}" title="IP Address" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.IPAddress}}</a>{{end}}
{{if $.CurrentUser.Loggedin}}{{if $.CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.IPAddress}}" title="IP Address" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.IPAddress}}</a>{{end}}{{end}}
</div>
</div>
{{end}}