493 lines
14 KiB
Go
493 lines
14 KiB
Go
package common
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"strconv"
|
|
|
|
"../query_gen/lib"
|
|
)
|
|
|
|
type MenuItemList []MenuItem
|
|
|
|
type MenuListHolder struct {
|
|
MenuID int
|
|
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
|
|
MenuID int
|
|
|
|
Name string
|
|
HTMLID string
|
|
CSSClass string
|
|
Position string
|
|
Path string
|
|
Aria string
|
|
Tooltip string
|
|
Order int
|
|
TmplName string
|
|
|
|
GuestOnly bool
|
|
MemberOnly bool
|
|
SuperModOnly bool
|
|
AdminOnly bool
|
|
}
|
|
|
|
// TODO: Move the menu item stuff to it's own file
|
|
type MenuItemStmts struct {
|
|
update *sql.Stmt
|
|
insert *sql.Stmt
|
|
delete *sql.Stmt
|
|
updateOrder *sql.Stmt
|
|
}
|
|
|
|
var menuItemStmts MenuItemStmts
|
|
|
|
func init() {
|
|
DbInits.Add(func(acc *qgen.Accumulator) error {
|
|
menuItemStmts = MenuItemStmts{
|
|
update: acc.Update("menu_items").Set("name = ?, htmlID = ?, cssClass = ?, position = ?, path = ?, aria = ?, tooltip = ?, tmplName = ?, guestOnly = ?, memberOnly = ?, staffOnly = ?, adminOnly = ?").Where("miid = ?").Prepare(),
|
|
insert: acc.Insert("menu_items").Columns("mid, name, htmlID, cssClass, position, path, aria, tooltip, tmplName, guestOnly, memberOnly, staffOnly, adminOnly").Fields("?,?,?,?,?,?,?,?,?,?,?,?,?").Prepare(),
|
|
delete: acc.Delete("menu_items").Where("miid = ?").Prepare(),
|
|
updateOrder: acc.Update("menu_items").Set("order = ?").Where("miid = ?").Prepare(),
|
|
}
|
|
return acc.FirstError()
|
|
})
|
|
}
|
|
|
|
func (item MenuItem) Commit() error {
|
|
_, err := menuItemStmts.update.Exec(item.Name, item.HTMLID, item.CSSClass, item.Position, item.Path, item.Aria, item.Tooltip, item.TmplName, item.GuestOnly, item.MemberOnly, item.SuperModOnly, item.AdminOnly, item.ID)
|
|
Menus.Load(item.MenuID)
|
|
return err
|
|
}
|
|
|
|
func (item MenuItem) Create() (int, error) {
|
|
res, err := menuItemStmts.insert.Exec(item.MenuID, item.Name, item.HTMLID, item.CSSClass, item.Position, item.Path, item.Aria, item.Tooltip, item.TmplName, item.GuestOnly, item.MemberOnly, item.SuperModOnly, item.AdminOnly)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
Menus.Load(item.MenuID)
|
|
|
|
miid64, err := res.LastInsertId()
|
|
return int(miid64), err
|
|
}
|
|
|
|
func (item MenuItem) Delete() error {
|
|
_, err := menuItemStmts.delete.Exec(item.ID)
|
|
Menus.Load(item.MenuID)
|
|
return err
|
|
}
|
|
|
|
func (hold *MenuListHolder) LoadTmpl(name string) (menuTmpl MenuTmpl, err error) {
|
|
data, err := ioutil.ReadFile("./templates/" + name + ".html")
|
|
if err != nil {
|
|
return menuTmpl, err
|
|
}
|
|
return hold.Parse(name, data), nil
|
|
}
|
|
|
|
// TODO: Make this atomic, maybe with a transaction or store the order on the menu itself?
|
|
func (hold *MenuListHolder) UpdateOrder(updateMap map[int]int) error {
|
|
for miid, order := range updateMap {
|
|
_, err := menuItemStmts.updateOrder.Exec(order, miid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
Menus.Load(hold.MenuID)
|
|
return nil
|
|
}
|
|
|
|
func (hold *MenuListHolder) LoadTmpls() (tmpls map[string]MenuTmpl, err error) {
|
|
tmpls = make(map[string]MenuTmpl)
|
|
var loadTmpl = func(name string) error {
|
|
menuTmpl, err := hold.LoadTmpl(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmpls[name] = menuTmpl
|
|
return nil
|
|
}
|
|
err = loadTmpl("menu_item")
|
|
if err != nil {
|
|
return tmpls, err
|
|
}
|
|
err = loadTmpl("menu_alerts")
|
|
return tmpls, err
|
|
}
|
|
|
|
// 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 {
|
|
tmpls, err := hold.LoadTmpls()
|
|
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 peekNextChar(tmplData []byte, i int) byte {
|
|
if len(tmplData) <= (i + 1) {
|
|
return 0
|
|
}
|
|
return tmplData[i+1]
|
|
}
|
|
|
|
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++ {
|
|
//fmt.Println("tmplData[j]: ", string(tmplData[j]))
|
|
if tmplData[j] != expects[expectIndex] {
|
|
return j, false
|
|
}
|
|
//fmt.Printf("found %+v at %d\n", string(expects[expectIndex]), expectIndex)
|
|
expectIndex++
|
|
}
|
|
return j, true
|
|
}
|
|
|
|
func skipAllUntilCharsExist(tmplData []byte, i int, expects []byte) (newI int, hasIt bool) {
|
|
j := i
|
|
expectIndex := 0
|
|
//fmt.Printf("tmplData: %+v\n", string(tmplData))
|
|
for ; j < len(tmplData) && expectIndex < len(expects); j++ {
|
|
//fmt.Println("tmplData[j]: ", string(tmplData[j]) + " ")
|
|
if tmplData[j] == expects[expectIndex] {
|
|
//fmt.Printf("expects[expectIndex]: %+v - %d\n", string(expects[expectIndex]), expectIndex)
|
|
expectIndex++
|
|
if len(expects) <= expectIndex {
|
|
//fmt.Println("breaking")
|
|
break
|
|
}
|
|
} else {
|
|
/*if expectIndex != 0 {
|
|
fmt.Println("broke expectations")
|
|
fmt.Println("expected: ", string(expects[expectIndex]))
|
|
fmt.Println("got: ", string(tmplData[j]))
|
|
fmt.Println("next: ", string(peekNextChar(tmplData, j)))
|
|
fmt.Println("next: ", string(peekNextChar(tmplData, j+1)))
|
|
fmt.Println("next: ", string(peekNextChar(tmplData, j+2)))
|
|
fmt.Println("next: ", string(peekNextChar(tmplData, j+3)))
|
|
}*/
|
|
expectIndex = 0
|
|
}
|
|
}
|
|
//fmt.Println("len(expects): ", len(expects))
|
|
//fmt.Println("expectIndex: ", expectIndex)
|
|
return j, len(expects) == expectIndex
|
|
}
|
|
|
|
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))
|
|
// TODO: Check if the subBuffer has any items or is empty
|
|
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})
|
|
}
|
|
|
|
tmplData = bytes.Replace(tmplData, []byte("{{"), []byte("{"), -1)
|
|
tmplData = bytes.Replace(tmplData, []byte("}}"), []byte("}}"), -1)
|
|
for i := 0; i < len(tmplData); i++ {
|
|
char := tmplData[i]
|
|
if char == '{' {
|
|
//fmt.Println("found open fence")
|
|
dotIndex, hasDot := skipUntilIfExists(tmplData, i, '.')
|
|
if !hasDot {
|
|
//fmt.Println("no dot, assumed template function style")
|
|
// Template function style
|
|
langIndex, hasChars := skipUntilCharsExist(tmplData, i+1, []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 {
|
|
//fmt.Println("no end fence")
|
|
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
|
|
}
|
|
renderBuffer, variableIndices = hold.ScanItem(menuTmpls, mitem, renderBuffer, variableIndices)
|
|
}
|
|
// TODO: Need more coalescing in the renderBuffer
|
|
return renderBuffer, variableIndices
|
|
}
|
|
|
|
// Note: This doesn't do a visibility check like hold.Scan() does
|
|
func (hold *MenuListHolder) ScanItem(menuTmpls map[string]MenuTmpl, mitem MenuItem, renderBuffer [][]byte, variableIndices []int) ([][]byte, []int) {
|
|
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 ".ID":
|
|
renderItem = []byte(strconv.Itoa(mitem.ID))
|
|
case ".Name":
|
|
renderItem = []byte(mitem.Name)
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|