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 }