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
410 lines
11 KiB
Go
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
|
|
}
|