package common import ( "bytes" "database/sql" "fmt" "io" "io/ioutil" "strconv" "strings" "github.com/Azareal/Gosora/common/phrases" "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 { 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, pathList := hold.Scan(tmpls, callback) hold.Variations[index] = menuTmpl{renderBuffer, variableIndices, pathList} } // 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 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 _, char := range slice { fmt.Print(string(char)) } fmt.Print("] ") } } func (hold *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 var 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 (hold *MenuListHolder) Scan(menuTmpls map[string]MenuTmpl, showItem func(mitem MenuItem) bool) (renderBuffer [][]byte, variableIndices []int, pathList []menuPath) { 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) 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 (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"] } 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 (hold *MenuListHolder) Build(w io.Writer, user *User, pathPrefix string) 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 pathPrefix == "" { pathPrefix = Config.DefaultPath } if len(mTmpl.VariableIndices) == 0 { for _, renderItem := range mTmpl.RenderBuffer { w.Write(renderItem) } return nil } var 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 }