514 lines
16 KiB
Go
514 lines
16 KiB
Go
package common
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/Azareal/Gosora/common/phrases"
|
|
tmpl "github.com/Azareal/Gosora/common/templates"
|
|
qgen "github.com/Azareal/Gosora/query_gen"
|
|
)
|
|
|
|
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 menuPath struct {
|
|
Path string
|
|
Index int
|
|
}
|
|
|
|
type menuTmpl struct {
|
|
RenderBuffer [][]byte
|
|
VariableIndices []int
|
|
PathMappings []menuPath
|
|
}
|
|
|
|
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 {
|
|
mi := "menu_items"
|
|
menuItemStmts = MenuItemStmts{
|
|
update: acc.Update(mi).Set("name=?,htmlID=?,cssClass=?,position=?,path=?,aria=?,tooltip=?,tmplName=?,guestOnly=?,memberOnly=?,staffOnly=?,adminOnly=?").Where("miid=?").Prepare(),
|
|
insert: acc.Insert(mi).Columns("mid, name, htmlID, cssClass, position, path, aria, tooltip, tmplName, guestOnly, memberOnly, staffOnly, adminOnly").Fields("?,?,?,?,?,?,?,?,?,?,?,?,?").Prepare(),
|
|
delete: acc.Delete(mi).Where("miid=?").Prepare(),
|
|
updateOrder: acc.Update(mi).Set("order=?").Where("miid=?").Prepare(),
|
|
}
|
|
return acc.FirstError()
|
|
})
|
|
}
|
|
|
|
func (i MenuItem) Commit() error {
|
|
_, e := menuItemStmts.update.Exec(i.Name, i.HTMLID, i.CSSClass, i.Position, i.Path, i.Aria, i.Tooltip, i.TmplName, i.GuestOnly, i.MemberOnly, i.SuperModOnly, i.AdminOnly, i.ID)
|
|
Menus.Load(i.MenuID)
|
|
return e
|
|
}
|
|
|
|
func (i MenuItem) Create() (int, error) {
|
|
res, e := menuItemStmts.insert.Exec(i.MenuID, i.Name, i.HTMLID, i.CSSClass, i.Position, i.Path, i.Aria, i.Tooltip, i.TmplName, i.GuestOnly, i.MemberOnly, i.SuperModOnly, i.AdminOnly)
|
|
if e != nil {
|
|
return 0, e
|
|
}
|
|
Menus.Load(i.MenuID)
|
|
|
|
miid64, e := res.LastInsertId()
|
|
return int(miid64), e
|
|
}
|
|
|
|
func (i MenuItem) Delete() error {
|
|
_, e := menuItemStmts.delete.Exec(i.ID)
|
|
Menus.Load(i.MenuID)
|
|
return e
|
|
}
|
|
|
|
func (h *MenuListHolder) LoadTmpl(name string) (t MenuTmpl, e error) {
|
|
data, e := ioutil.ReadFile("./templates/" + name + ".html")
|
|
if e != nil {
|
|
return t, e
|
|
}
|
|
return h.Parse(name, []byte(tmpl.Minify(string(data)))), nil
|
|
}
|
|
|
|
// TODO: Make this atomic, maybe with a transaction or store the order on the menu itself?
|
|
func (h *MenuListHolder) UpdateOrder(updateMap map[int]int) error {
|
|
for miid, order := range updateMap {
|
|
_, e := menuItemStmts.updateOrder.Exec(order, miid)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
}
|
|
Menus.Load(h.MenuID)
|
|
return nil
|
|
}
|
|
|
|
func (h *MenuListHolder) LoadTmpls() (tmpls map[string]MenuTmpl, e error) {
|
|
tmpls = make(map[string]MenuTmpl)
|
|
load := func(name string) error {
|
|
menuTmpl, e := h.LoadTmpl(name)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
tmpls[name] = menuTmpl
|
|
return nil
|
|
}
|
|
e = load("menu_item")
|
|
if e != nil {
|
|
return tmpls, e
|
|
}
|
|
e = load("menu_alerts")
|
|
return tmpls, e
|
|
}
|
|
|
|
// 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 (h *MenuListHolder) Preparse() error {
|
|
tmpls, err := h.LoadTmpls()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
addVariation := func(index int, callback func(i MenuItem) bool) {
|
|
renderBuffer, variableIndices, pathList := h.Scan(tmpls, callback)
|
|
h.Variations[index] = menuTmpl{renderBuffer, variableIndices, pathList}
|
|
}
|
|
|
|
// Guest Menu
|
|
addVariation(0, func(i MenuItem) bool {
|
|
return !i.MemberOnly
|
|
})
|
|
// Member Menu
|
|
addVariation(1, func(i MenuItem) bool {
|
|
return !i.SuperModOnly && !i.GuestOnly
|
|
})
|
|
// Super Mod Menu
|
|
addVariation(2, func(i MenuItem) bool {
|
|
return !i.AdminOnly && !i.GuestOnly
|
|
})
|
|
// Admin Menu
|
|
addVariation(3, func(i MenuItem) bool {
|
|
return !i.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 skipUntilIfExistsOrLine(tmplData []byte, i int, expects byte) (newI int, hasIt bool) {
|
|
j := i
|
|
for ; j < len(tmplData); j++ {
|
|
if tmplData[j] == 10 {
|
|
return j, false
|
|
} else 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
|
|
for ; j < len(tmplData) && expectIndex < len(expects); j++ {
|
|
if tmplData[j] == expects[expectIndex] {
|
|
//fmt.Printf("expects[expectIndex]: %+v - %d\n", string(expects[expectIndex]), expectIndex)
|
|
expectIndex++
|
|
if len(expects) <= expectIndex {
|
|
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
|
|
}
|
|
}
|
|
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 _, ch := range slice {
|
|
fmt.Print(string(ch))
|
|
}
|
|
fmt.Print("] ")
|
|
}
|
|
}
|
|
|
|
func (h *MenuListHolder) Parse(name string, tmplData []byte) (menuTmpl MenuTmpl) {
|
|
var textBuffer, variableBuffer [][]byte
|
|
var renderList []menuRenderItem
|
|
var subBuffer []byte
|
|
|
|
// ? We only support simple properties on MenuItem right now
|
|
addVariable := func(name []byte) {
|
|
// TODO: Check if the subBuffer has any items or is empty
|
|
textBuffer = append(textBuffer, subBuffer)
|
|
subBuffer = nil
|
|
|
|
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 == '{' {
|
|
dotIndex, hasDot := skipUntilIfExists(tmplData, i, '.')
|
|
if !hasDot {
|
|
// 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 {
|
|
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})
|
|
}
|
|
|
|
return MenuTmpl{name, textBuffer, variableBuffer, renderList}
|
|
}
|
|
|
|
func (h *MenuListHolder) Scan(tmpls map[string]MenuTmpl, showItem func(i MenuItem) bool) (renderBuffer [][]byte, variableIndices []int, pathList []menuPath) {
|
|
for _, mitem := range h.List {
|
|
// Do we want this item in this variation of the menu?
|
|
if !showItem(mitem) {
|
|
continue
|
|
}
|
|
renderBuffer, variableIndices = h.ScanItem(tmpls, mitem, renderBuffer, variableIndices)
|
|
pathList = append(pathList, menuPath{mitem.Path, len(renderBuffer) - 1})
|
|
}
|
|
|
|
// TODO: Need more coalescing in the renderBuffer
|
|
return renderBuffer, variableIndices, pathList
|
|
}
|
|
|
|
// Note: This doesn't do a visibility check like hold.Scan() does
|
|
func (h *MenuListHolder) ScanItem(tmpls map[string]MenuTmpl, mitem MenuItem, renderBuffer [][]byte, variableIndices []int) ([][]byte, []int) {
|
|
menuTmpl, ok := tmpls[mitem.TmplName]
|
|
if !ok {
|
|
menuTmpl = tmpls["menu_item"]
|
|
}
|
|
|
|
for _, renderItem := range menuTmpl.RenderList {
|
|
if renderItem.Type == 0 {
|
|
renderBuffer = append(renderBuffer, menuTmpl.TextBuffer[renderItem.Index])
|
|
continue
|
|
}
|
|
|
|
variable := menuTmpl.VariableBuffer[renderItem.Index]
|
|
dotAt, hasDot := skipUntilIfExists(variable, 0, '.')
|
|
if !hasDot {
|
|
continue
|
|
}
|
|
|
|
if bytes.Equal(variable[:dotAt], []byte("lang")) {
|
|
renderBuffer = append(renderBuffer, []byte(phrases.GetTmplPhrase(string(bytes.TrimPrefix(variable[dotAt:], []byte("."))))))
|
|
continue
|
|
}
|
|
|
|
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)
|
|
case ".CSSActive":
|
|
renderItem = []byte("{dyn.active}")
|
|
}
|
|
|
|
_, hasInnerVar := skipUntilIfExists(renderItem, 0, '{')
|
|
if hasInnerVar {
|
|
DebugLog("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(phrases.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
|
|
}
|
|
if len(renderItem) > 0 {
|
|
renderBuffer = append(renderBuffer, renderItem)
|
|
}
|
|
}
|
|
return renderBuffer, variableIndices
|
|
}
|
|
|
|
// TODO: Pre-render the lang stuff
|
|
func (h *MenuListHolder) Build(w io.Writer, user *User, pathPrefix string) error {
|
|
var mTmpl menuTmpl
|
|
if !user.Loggedin {
|
|
mTmpl = h.Variations[0]
|
|
} else if user.IsAdmin {
|
|
mTmpl = h.Variations[3]
|
|
} else if user.IsSuperMod {
|
|
mTmpl = h.Variations[2]
|
|
} else {
|
|
mTmpl = h.Variations[1]
|
|
}
|
|
if pathPrefix == "" {
|
|
pathPrefix = Config.DefaultPath
|
|
}
|
|
|
|
if len(mTmpl.VariableIndices) == 0 {
|
|
for _, renderItem := range mTmpl.RenderBuffer {
|
|
w.Write(renderItem)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
nearIndex := 0
|
|
for index, renderItem := range mTmpl.RenderBuffer {
|
|
if index != mTmpl.VariableIndices[nearIndex] {
|
|
w.Write(renderItem)
|
|
continue
|
|
}
|
|
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
|
|
}
|
|
|
|
switch string(variable[fenceStart+1 : dotAt]) {
|
|
case "me":
|
|
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
|
|
// TODO: Optimise this
|
|
case "dyn":
|
|
w.Write(variable[prevIndex:fenceStart])
|
|
var pmi int
|
|
for ii, pathItem := range mTmpl.PathMappings {
|
|
pmi = ii
|
|
if pathItem.Index > index {
|
|
break
|
|
}
|
|
}
|
|
|
|
if len(mTmpl.PathMappings) != 0 {
|
|
path := mTmpl.PathMappings[pmi].Path
|
|
if path == "" || path == "/" {
|
|
path = Config.DefaultPath
|
|
}
|
|
if strings.HasPrefix(path, pathPrefix) {
|
|
w.Write([]byte(" menu_active"))
|
|
}
|
|
}
|
|
|
|
prevIndex = fenceEnd
|
|
}
|
|
}
|
|
|
|
w.Write(variable[prevIndex : len(variable)-1])
|
|
if len(mTmpl.VariableIndices) > (nearIndex + 1) {
|
|
nearIndex++
|
|
}
|
|
}
|
|
return nil
|
|
}
|