gosora/common/menus.go
Azareal dbf2b8606e All the menu items are dynamic now, all that needs to be done now is the Control Panel part of the Menu Manager.
Travis should now build the patcher to see if that works.
Merged menu.html into header.html
Removed a redundant SetAdapter call in mysql.go.
Fixed a bug where inserting will sometimes strip the spaces in strings.
Bumped the minimum Go Version in schema.json to 1.10
Bumped the DBVersion to 1, so that we don't install these patches twice.

The patcher was botched in the previous commit, drop menus and menu_items and run it again
2018-04-23 09:38:25 +01:00

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