Added the Go and Database versions to the Control Panel Debug Page. Renamed common.TopicsPage to common.TopicListPage. Renamed *HeaderVars to *Header. Added the Paginator struct and refactored the code to use it. io.Writers are now used instead of http.ResponseWriters in transpiled templates for greater flexibility. Added the alert, menu_alerts, and menu_item templates. Added support for more integer types in the arithmetic functions for the transpiled templates. Exported AccSelectBuilder. Added an Each method to AccSelectBuilder. Added column quoting to the order by portions of queries for the MySQL Adapter. Began work on the client side rendering of alerts. Began work on the Menu Manager and associated functionality.
409 lines
11 KiB
Go
409 lines
11 KiB
Go
package common
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"strconv"
|
|
"sync/atomic"
|
|
|
|
"../query_gen/lib"
|
|
)
|
|
|
|
var Menus *DefaultMenuStore
|
|
|
|
type MenuItemList []MenuItem
|
|
|
|
type DefaultMenuStore struct {
|
|
menus map[int]*atomic.Value
|
|
}
|
|
|
|
func NewDefaultMenuStore() *DefaultMenuStore {
|
|
return &DefaultMenuStore{make(map[int]*atomic.Value)}
|
|
}
|
|
|
|
func (store *DefaultMenuStore) Get(mid int) *MenuListHolder {
|
|
aStore, ok := store.menus[mid]
|
|
if ok {
|
|
return aStore.Load().(*MenuListHolder)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type MenuListHolder struct {
|
|
List MenuItemList
|
|
Variations map[int]menuTmpl // 0 = Guest Menu, 1 = Member Menu, 2 = Super Mod Menu, 3 = Admin Menu
|
|
}
|
|
|
|
type menuTmpl struct {
|
|
RenderBuffer [][]byte
|
|
VariableIndices []int
|
|
}
|
|
|
|
type MenuItem struct {
|
|
ID int
|
|
HTMLID string
|
|
CSSClass string
|
|
Position string
|
|
Path string
|
|
Aria string
|
|
Tooltip string
|
|
Order int
|
|
TmplName string
|
|
|
|
GuestOnly bool
|
|
MemberOnly bool
|
|
SuperModOnly bool
|
|
AdminOnly bool
|
|
}
|
|
|
|
func (store *DefaultMenuStore) Load(mid int) error {
|
|
var mlist MenuItemList
|
|
acc := qgen.Builder.Accumulator()
|
|
err := acc.Select("menu_items").Columns("htmlID, cssClass, position, path, aria, tooltip, order, tmplName, guestOnly, memberOnly, staffOnly, adminOnly").Where("mid = " + strconv.Itoa(mid)).Orderby("order ASC").Each(func(rows *sql.Rows) error {
|
|
var mitem = MenuItem{ID: 1}
|
|
err := rows.Scan(&mitem.HTMLID, &mitem.CSSClass, &mitem.Position, &mitem.Path, &mitem.Aria, &mitem.Tooltip, &mitem.Order, &mitem.TmplName, &mitem.GuestOnly, &mitem.MemberOnly, &mitem.SuperModOnly, &mitem.AdminOnly)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mlist = append(mlist, mitem)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hold := &MenuListHolder{mlist, make(map[int]menuTmpl)}
|
|
err = hold.Preparse()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var aStore = &atomic.Value{}
|
|
aStore.Store(hold)
|
|
store.menus[mid] = aStore
|
|
return nil
|
|
}
|
|
|
|
// TODO: Run this in main, sync ticks, when the phrase file changes (need to implement the sync for that first), and when the settings are changed
|
|
func (hold *MenuListHolder) Preparse() error {
|
|
var tmpls = make(map[string]MenuTmpl)
|
|
var loadTmpl = func(name string) error {
|
|
data, err := ioutil.ReadFile("./templates/" + name + ".html")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmpls[name] = hold.Parse(name, data)
|
|
return nil
|
|
}
|
|
err := loadTmpl("menu_item")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = loadTmpl("menu_alerts")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var addVariation = func(index int, callback func(mitem MenuItem) bool) {
|
|
renderBuffer, variableIndices := hold.Scan(tmpls, callback)
|
|
hold.Variations[index] = menuTmpl{renderBuffer, variableIndices}
|
|
fmt.Print("renderBuffer: ")
|
|
menuDumpSlice(renderBuffer)
|
|
fmt.Printf("\nvariableIndices: %+v\n", variableIndices)
|
|
}
|
|
|
|
// Guest Menu
|
|
addVariation(0, func(mitem MenuItem) bool {
|
|
return !mitem.MemberOnly
|
|
})
|
|
// Member Menu
|
|
addVariation(1, func(mitem MenuItem) bool {
|
|
return !mitem.SuperModOnly && !mitem.GuestOnly
|
|
})
|
|
// Super Mod Menu
|
|
addVariation(2, func(mitem MenuItem) bool {
|
|
return !mitem.AdminOnly && !mitem.GuestOnly
|
|
})
|
|
// Admin Menu
|
|
addVariation(3, func(mitem MenuItem) bool {
|
|
return !mitem.GuestOnly
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func nextCharIs(tmplData []byte, i int, expects byte) bool {
|
|
if len(tmplData) <= (i + 1) {
|
|
return false
|
|
}
|
|
return tmplData[i+1] == expects
|
|
}
|
|
|
|
func skipUntilIfExists(tmplData []byte, i int, expects byte) (newI int, hasIt bool) {
|
|
j := i
|
|
for ; j < len(tmplData); j++ {
|
|
if tmplData[j] == expects {
|
|
return j, true
|
|
}
|
|
}
|
|
return j, false
|
|
}
|
|
|
|
func skipUntilCharsExist(tmplData []byte, i int, expects []byte) (newI int, hasIt bool) {
|
|
j := i
|
|
expectIndex := 0
|
|
for ; j < len(tmplData) && expectIndex < len(expects); j++ {
|
|
if tmplData[j] != expects[expectIndex] {
|
|
return j, false
|
|
}
|
|
expectIndex++
|
|
}
|
|
return j, true
|
|
}
|
|
|
|
type menuRenderItem struct {
|
|
Type int // 0: text, 1: variable
|
|
Index int
|
|
}
|
|
|
|
type MenuTmpl struct {
|
|
Name string
|
|
TextBuffer [][]byte
|
|
VariableBuffer [][]byte
|
|
RenderList []menuRenderItem
|
|
}
|
|
|
|
func menuDumpSlice(outerSlice [][]byte) {
|
|
for sliceID, slice := range outerSlice {
|
|
fmt.Print(strconv.Itoa(sliceID) + ":[")
|
|
for _, char := range slice {
|
|
fmt.Print(string(char))
|
|
}
|
|
fmt.Print("] ")
|
|
}
|
|
}
|
|
|
|
func (hold *MenuListHolder) Parse(name string, tmplData []byte) (menuTmpl MenuTmpl) {
|
|
//fmt.Println("tmplData: ", string(tmplData))
|
|
var textBuffer, variableBuffer [][]byte
|
|
var renderList []menuRenderItem
|
|
var subBuffer []byte
|
|
|
|
// ? We only support simple properties on MenuItem right now
|
|
var addVariable = func(name []byte) {
|
|
//fmt.Println("appending subBuffer: ", string(subBuffer))
|
|
textBuffer = append(textBuffer, subBuffer)
|
|
subBuffer = nil
|
|
|
|
//fmt.Println("adding variable: ", string(name))
|
|
variableBuffer = append(variableBuffer, name)
|
|
renderList = append(renderList, menuRenderItem{0, len(textBuffer) - 1})
|
|
renderList = append(renderList, menuRenderItem{1, len(variableBuffer) - 1})
|
|
}
|
|
|
|
for i := 0; i < len(tmplData); i++ {
|
|
char := tmplData[i]
|
|
if char == '{' && nextCharIs(tmplData, i, '{') {
|
|
dotIndex, hasDot := skipUntilIfExists(tmplData, i, '.')
|
|
if !hasDot {
|
|
// Template function style
|
|
langIndex, hasChars := skipUntilCharsExist(tmplData, i+2, []byte("lang"))
|
|
if hasChars {
|
|
startIndex, hasStart := skipUntilIfExists(tmplData, langIndex, '"')
|
|
endIndex, hasEnd := skipUntilIfExists(tmplData, startIndex+1, '"')
|
|
if hasStart && hasEnd {
|
|
fenceIndex, hasFence := skipUntilIfExists(tmplData, endIndex, '}')
|
|
if !hasFence || !nextCharIs(tmplData, fenceIndex, '}') {
|
|
break
|
|
}
|
|
//fmt.Println("tmplData[startIndex:endIndex]: ", tmplData[startIndex+1:endIndex])
|
|
prefix := []byte("lang.")
|
|
addVariable(append(prefix, tmplData[startIndex+1:endIndex]...))
|
|
i = fenceIndex + 1
|
|
continue
|
|
}
|
|
}
|
|
break
|
|
}
|
|
fenceIndex, hasFence := skipUntilIfExists(tmplData, dotIndex, '}')
|
|
if !hasFence || !nextCharIs(tmplData, fenceIndex, '}') {
|
|
break
|
|
}
|
|
addVariable(tmplData[dotIndex:fenceIndex])
|
|
i = fenceIndex + 1
|
|
continue
|
|
}
|
|
subBuffer = append(subBuffer, char)
|
|
}
|
|
if len(subBuffer) > 0 {
|
|
// TODO: Have a property in renderList which holds the byte slice since variableBuffers and textBuffers have the same underlying implementation?
|
|
textBuffer = append(textBuffer, subBuffer)
|
|
renderList = append(renderList, menuRenderItem{0, len(textBuffer) - 1})
|
|
}
|
|
|
|
fmt.Println("name: ", name)
|
|
fmt.Print("textBuffer: ")
|
|
menuDumpSlice(textBuffer)
|
|
fmt.Print("\nvariableBuffer: ")
|
|
menuDumpSlice(variableBuffer)
|
|
fmt.Printf("\nrenderList: %+v\n", renderList)
|
|
return MenuTmpl{name, textBuffer, variableBuffer, renderList}
|
|
}
|
|
|
|
func (hold *MenuListHolder) Scan(menuTmpls map[string]MenuTmpl, showItem func(mitem MenuItem) bool) (renderBuffer [][]byte, variableIndices []int) {
|
|
for _, mitem := range hold.List {
|
|
// Do we want this item in this variation of the menu?
|
|
if !showItem(mitem) {
|
|
continue
|
|
}
|
|
|
|
menuTmpl, ok := menuTmpls[mitem.TmplName]
|
|
if !ok {
|
|
menuTmpl = menuTmpls["menu_item"]
|
|
}
|
|
fmt.Println("menuTmpl: ", menuTmpl)
|
|
for _, renderItem := range menuTmpl.RenderList {
|
|
if renderItem.Type == 0 {
|
|
renderBuffer = append(renderBuffer, menuTmpl.TextBuffer[renderItem.Index])
|
|
continue
|
|
}
|
|
|
|
variable := menuTmpl.VariableBuffer[renderItem.Index]
|
|
fmt.Println("initial variable: ", string(variable))
|
|
dotAt, hasDot := skipUntilIfExists(variable, 0, '.')
|
|
if !hasDot {
|
|
fmt.Println("no dot")
|
|
continue
|
|
}
|
|
|
|
if bytes.Equal(variable[:dotAt], []byte("lang")) {
|
|
fmt.Println("lang: ", string(bytes.TrimPrefix(variable[dotAt:], []byte("."))))
|
|
renderBuffer = append(renderBuffer, []byte(GetTmplPhrase(string(bytes.TrimPrefix(variable[dotAt:], []byte("."))))))
|
|
} else {
|
|
var renderItem []byte
|
|
switch string(variable) {
|
|
case ".HTMLID":
|
|
renderItem = []byte(mitem.HTMLID)
|
|
case ".CSSClass":
|
|
renderItem = []byte(mitem.CSSClass)
|
|
case ".Position":
|
|
renderItem = []byte(mitem.Position)
|
|
case ".Path":
|
|
renderItem = []byte(mitem.Path)
|
|
case ".Aria":
|
|
renderItem = []byte(mitem.Aria)
|
|
case ".Tooltip":
|
|
renderItem = []byte(mitem.Tooltip)
|
|
}
|
|
|
|
_, hasInnerVar := skipUntilIfExists(renderItem, 0, '{')
|
|
if hasInnerVar {
|
|
fmt.Println("inner var: ", string(renderItem))
|
|
dotAt, hasDot := skipUntilIfExists(renderItem, 0, '.')
|
|
endFence, hasEndFence := skipUntilIfExists(renderItem, dotAt, '}')
|
|
if !hasDot || !hasEndFence || (endFence-dotAt) <= 1 {
|
|
renderBuffer = append(renderBuffer, renderItem)
|
|
variableIndices = append(variableIndices, len(renderBuffer)-1)
|
|
continue
|
|
}
|
|
|
|
if bytes.Equal(renderItem[1:dotAt], []byte("lang")) {
|
|
fmt.Println("lang var: ", string(renderItem[dotAt+1:endFence]))
|
|
renderBuffer = append(renderBuffer, []byte(GetTmplPhrase(string(renderItem[dotAt+1:endFence]))))
|
|
} else {
|
|
fmt.Println("other var: ", string(variable[:dotAt]))
|
|
if len(renderItem) > 0 {
|
|
renderBuffer = append(renderBuffer, renderItem)
|
|
variableIndices = append(variableIndices, len(renderBuffer)-1)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
fmt.Println("normal var: ", string(variable[:dotAt]))
|
|
if len(renderItem) > 0 {
|
|
renderBuffer = append(renderBuffer, renderItem)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// TODO: Need more coalescing in the renderBuffer
|
|
return renderBuffer, variableIndices
|
|
}
|
|
|
|
// TODO: Pre-render the lang stuff
|
|
func (hold *MenuListHolder) Build(w io.Writer, user *User) error {
|
|
var mTmpl menuTmpl
|
|
if !user.Loggedin {
|
|
mTmpl = hold.Variations[0]
|
|
} else if user.IsAdmin {
|
|
mTmpl = hold.Variations[3]
|
|
} else if user.IsSuperMod {
|
|
mTmpl = hold.Variations[2]
|
|
} else {
|
|
mTmpl = hold.Variations[1]
|
|
}
|
|
|
|
if len(mTmpl.VariableIndices) == 0 {
|
|
fmt.Println("no variable indices")
|
|
for _, renderItem := range mTmpl.RenderBuffer {
|
|
fmt.Printf("renderItem: %+v\n", renderItem)
|
|
w.Write(renderItem)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var nearIndex = 0
|
|
for index, renderItem := range mTmpl.RenderBuffer {
|
|
if index != mTmpl.VariableIndices[nearIndex] {
|
|
fmt.Println("wrote text: ", string(renderItem))
|
|
w.Write(renderItem)
|
|
continue
|
|
}
|
|
|
|
fmt.Println("variable: ", string(renderItem))
|
|
variable := renderItem
|
|
// ? - I can probably remove this check now that I've kicked it upstream, or we could keep it here for safety's sake?
|
|
if len(variable) == 0 {
|
|
continue
|
|
}
|
|
prevIndex := 0
|
|
for i := 0; i < len(renderItem); i++ {
|
|
fenceStart, hasFence := skipUntilIfExists(variable, i, '{')
|
|
if !hasFence {
|
|
continue
|
|
}
|
|
i = fenceStart
|
|
fenceEnd, hasFence := skipUntilIfExists(variable, fenceStart, '}')
|
|
if !hasFence {
|
|
continue
|
|
}
|
|
i = fenceEnd
|
|
dotAt, hasDot := skipUntilIfExists(variable, fenceStart, '.')
|
|
if !hasDot {
|
|
continue
|
|
}
|
|
if bytes.Equal(variable[fenceStart:dotAt], []byte("me")) {
|
|
fmt.Println("maybe me variable")
|
|
w.Write(variable[prevIndex:fenceStart])
|
|
switch string(variable[dotAt:fenceEnd]) {
|
|
case "Link":
|
|
w.Write([]byte(user.Link))
|
|
case "Session":
|
|
w.Write([]byte(user.Session))
|
|
}
|
|
prevIndex = fenceEnd
|
|
}
|
|
}
|
|
fmt.Println("prevIndex: ", prevIndex)
|
|
fmt.Println("len(variable)-1: ", len(variable)-1)
|
|
w.Write(variable[prevIndex : len(variable)-1])
|
|
if len(mTmpl.VariableIndices) > (nearIndex + 1) {
|
|
nearIndex++
|
|
}
|
|
}
|
|
return nil
|
|
}
|