gosora/common/menus.go

410 lines
11 KiB
Go
Raw Normal View History

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
}
//fmt.Println("checking me: ", string(variable[fenceStart+1:dotAt]))
if bytes.Equal(variable[fenceStart+1:dotAt], []byte("me")) {
//fmt.Println("maybe me variable")
w.Write(variable[prevIndex:fenceStart])
switch string(variable[dotAt+1 : 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
}