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
|
|
}
|