github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/menus.go (about) 1 package common 2 3 import ( 4 "bytes" 5 "database/sql" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "strconv" 10 "strings" 11 12 "github.com/Azareal/Gosora/common/phrases" 13 tmpl "github.com/Azareal/Gosora/common/templates" 14 qgen "github.com/Azareal/Gosora/query_gen" 15 ) 16 17 type MenuItemList []MenuItem 18 19 type MenuListHolder struct { 20 MenuID int 21 List MenuItemList 22 Variations map[int]menuTmpl // 0 = Guest Menu, 1 = Member Menu, 2 = Super Mod Menu, 3 = Admin Menu 23 } 24 25 type menuPath struct { 26 Path string 27 Index int 28 } 29 30 type menuTmpl struct { 31 RenderBuffer [][]byte 32 VariableIndices []int 33 PathMappings []menuPath 34 } 35 36 type MenuItem struct { 37 ID int 38 MenuID int 39 40 Name string 41 HTMLID string 42 CSSClass string 43 Position string 44 Path string 45 Aria string 46 Tooltip string 47 Order int 48 TmplName string 49 50 GuestOnly bool 51 MemberOnly bool 52 SuperModOnly bool 53 AdminOnly bool 54 } 55 56 // TODO: Move the menu item stuff to it's own file 57 type MenuItemStmts struct { 58 update *sql.Stmt 59 insert *sql.Stmt 60 delete *sql.Stmt 61 updateOrder *sql.Stmt 62 } 63 64 var menuItemStmts MenuItemStmts 65 66 func init() { 67 DbInits.Add(func(acc *qgen.Accumulator) error { 68 mi := "menu_items" 69 menuItemStmts = MenuItemStmts{ 70 update: acc.Update(mi).Set("name=?,htmlID=?,cssClass=?,position=?,path=?,aria=?,tooltip=?,tmplName=?,guestOnly=?,memberOnly=?,staffOnly=?,adminOnly=?").Where("miid=?").Prepare(), 71 insert: acc.Insert(mi).Columns("mid, name, htmlID, cssClass, position, path, aria, tooltip, tmplName, guestOnly, memberOnly, staffOnly, adminOnly").Fields("?,?,?,?,?,?,?,?,?,?,?,?,?").Prepare(), 72 delete: acc.Delete(mi).Where("miid=?").Prepare(), 73 updateOrder: acc.Update(mi).Set("order=?").Where("miid=?").Prepare(), 74 } 75 return acc.FirstError() 76 }) 77 } 78 79 func (i MenuItem) Commit() error { 80 _, 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) 81 Menus.Load(i.MenuID) 82 return e 83 } 84 85 func (i MenuItem) Create() (int, error) { 86 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) 87 if e != nil { 88 return 0, e 89 } 90 Menus.Load(i.MenuID) 91 92 miid64, e := res.LastInsertId() 93 return int(miid64), e 94 } 95 96 func (i MenuItem) Delete() error { 97 _, e := menuItemStmts.delete.Exec(i.ID) 98 Menus.Load(i.MenuID) 99 return e 100 } 101 102 func (h *MenuListHolder) LoadTmpl(name string) (t MenuTmpl, e error) { 103 data, e := ioutil.ReadFile("./templates/" + name + ".html") 104 if e != nil { 105 return t, e 106 } 107 return h.Parse(name, []byte(tmpl.Minify(string(data)))), nil 108 } 109 110 // TODO: Make this atomic, maybe with a transaction or store the order on the menu itself? 111 func (h *MenuListHolder) UpdateOrder(updateMap map[int]int) error { 112 for miid, order := range updateMap { 113 _, e := menuItemStmts.updateOrder.Exec(order, miid) 114 if e != nil { 115 return e 116 } 117 } 118 Menus.Load(h.MenuID) 119 return nil 120 } 121 122 func (h *MenuListHolder) LoadTmpls() (tmpls map[string]MenuTmpl, e error) { 123 tmpls = make(map[string]MenuTmpl) 124 load := func(name string) error { 125 menuTmpl, e := h.LoadTmpl(name) 126 if e != nil { 127 return e 128 } 129 tmpls[name] = menuTmpl 130 return nil 131 } 132 e = load("menu_item") 133 if e != nil { 134 return tmpls, e 135 } 136 e = load("menu_alerts") 137 return tmpls, e 138 } 139 140 // 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 141 func (h *MenuListHolder) Preparse() error { 142 tmpls, err := h.LoadTmpls() 143 if err != nil { 144 return err 145 } 146 147 addVariation := func(index int, callback func(i MenuItem) bool) { 148 renderBuffer, variableIndices, pathList := h.Scan(tmpls, callback) 149 h.Variations[index] = menuTmpl{renderBuffer, variableIndices, pathList} 150 } 151 152 // Guest Menu 153 addVariation(0, func(i MenuItem) bool { 154 return !i.MemberOnly 155 }) 156 // Member Menu 157 addVariation(1, func(i MenuItem) bool { 158 return !i.SuperModOnly && !i.GuestOnly 159 }) 160 // Super Mod Menu 161 addVariation(2, func(i MenuItem) bool { 162 return !i.AdminOnly && !i.GuestOnly 163 }) 164 // Admin Menu 165 addVariation(3, func(i MenuItem) bool { 166 return !i.GuestOnly 167 }) 168 return nil 169 } 170 171 func nextCharIs(tmplData []byte, i int, expects byte) bool { 172 if len(tmplData) <= (i + 1) { 173 return false 174 } 175 return tmplData[i+1] == expects 176 } 177 178 func peekNextChar(tmplData []byte, i int) byte { 179 if len(tmplData) <= (i + 1) { 180 return 0 181 } 182 return tmplData[i+1] 183 } 184 185 func skipUntilIfExists(tmplData []byte, i int, expects byte) (newI int, hasIt bool) { 186 j := i 187 for ; j < len(tmplData); j++ { 188 if tmplData[j] == expects { 189 return j, true 190 } 191 } 192 return j, false 193 } 194 195 func skipUntilIfExistsOrLine(tmplData []byte, i int, expects byte) (newI int, hasIt bool) { 196 j := i 197 for ; j < len(tmplData); j++ { 198 if tmplData[j] == 10 { 199 return j, false 200 } else if tmplData[j] == expects { 201 return j, true 202 } 203 } 204 return j, false 205 } 206 207 func skipUntilCharsExist(tmplData []byte, i int, expects []byte) (newI int, hasIt bool) { 208 j := i 209 expectIndex := 0 210 for ; j < len(tmplData) && expectIndex < len(expects); j++ { 211 //fmt.Println("tmplData[j]: ", string(tmplData[j])) 212 if tmplData[j] != expects[expectIndex] { 213 return j, false 214 } 215 //fmt.Printf("found %+v at %d\n", string(expects[expectIndex]), expectIndex) 216 expectIndex++ 217 } 218 return j, true 219 } 220 221 func skipAllUntilCharsExist(tmplData []byte, i int, expects []byte) (newI int, hasIt bool) { 222 j := i 223 expectIndex := 0 224 for ; j < len(tmplData) && expectIndex < len(expects); j++ { 225 if tmplData[j] == expects[expectIndex] { 226 //fmt.Printf("expects[expectIndex]: %+v - %d\n", string(expects[expectIndex]), expectIndex) 227 expectIndex++ 228 if len(expects) <= expectIndex { 229 break 230 } 231 } else { 232 /*if expectIndex != 0 { 233 fmt.Println("broke expectations") 234 fmt.Println("expected: ", string(expects[expectIndex])) 235 fmt.Println("got: ", string(tmplData[j])) 236 fmt.Println("next: ", string(peekNextChar(tmplData, j))) 237 fmt.Println("next: ", string(peekNextChar(tmplData, j+1))) 238 fmt.Println("next: ", string(peekNextChar(tmplData, j+2))) 239 fmt.Println("next: ", string(peekNextChar(tmplData, j+3))) 240 }*/ 241 expectIndex = 0 242 } 243 } 244 return j, len(expects) == expectIndex 245 } 246 247 type menuRenderItem struct { 248 Type int // 0: text, 1: variable 249 Index int 250 } 251 252 type MenuTmpl struct { 253 Name string 254 TextBuffer [][]byte 255 VariableBuffer [][]byte 256 RenderList []menuRenderItem 257 } 258 259 func menuDumpSlice(outerSlice [][]byte) { 260 for sliceID, slice := range outerSlice { 261 fmt.Print(strconv.Itoa(sliceID) + ":[") 262 for _, ch := range slice { 263 fmt.Print(string(ch)) 264 } 265 fmt.Print("] ") 266 } 267 } 268 269 func (h *MenuListHolder) Parse(name string, tmplData []byte) (menuTmpl MenuTmpl) { 270 var textBuffer, variableBuffer [][]byte 271 var renderList []menuRenderItem 272 var subBuffer []byte 273 274 // ? We only support simple properties on MenuItem right now 275 addVariable := func(name []byte) { 276 // TODO: Check if the subBuffer has any items or is empty 277 textBuffer = append(textBuffer, subBuffer) 278 subBuffer = nil 279 280 variableBuffer = append(variableBuffer, name) 281 renderList = append(renderList, menuRenderItem{0, len(textBuffer) - 1}) 282 renderList = append(renderList, menuRenderItem{1, len(variableBuffer) - 1}) 283 } 284 285 tmplData = bytes.Replace(tmplData, []byte("{{"), []byte("{"), -1) 286 tmplData = bytes.Replace(tmplData, []byte("}}"), []byte("}}"), -1) 287 for i := 0; i < len(tmplData); i++ { 288 char := tmplData[i] 289 if char == '{' { 290 dotIndex, hasDot := skipUntilIfExists(tmplData, i, '.') 291 if !hasDot { 292 // Template function style 293 langIndex, hasChars := skipUntilCharsExist(tmplData, i+1, []byte("lang")) 294 if hasChars { 295 startIndex, hasStart := skipUntilIfExists(tmplData, langIndex, '"') 296 endIndex, hasEnd := skipUntilIfExists(tmplData, startIndex+1, '"') 297 if hasStart && hasEnd { 298 fenceIndex, hasFence := skipUntilIfExists(tmplData, endIndex, '}') 299 if !hasFence || !nextCharIs(tmplData, fenceIndex, '}') { 300 break 301 } 302 //fmt.Println("tmplData[startIndex:endIndex]: ", tmplData[startIndex+1:endIndex]) 303 prefix := []byte("lang.") 304 addVariable(append(prefix, tmplData[startIndex+1:endIndex]...)) 305 i = fenceIndex + 1 306 continue 307 } 308 } 309 break 310 } 311 fenceIndex, hasFence := skipUntilIfExists(tmplData, dotIndex, '}') 312 if !hasFence { 313 break 314 } 315 addVariable(tmplData[dotIndex:fenceIndex]) 316 i = fenceIndex + 1 317 continue 318 } 319 subBuffer = append(subBuffer, char) 320 } 321 if len(subBuffer) > 0 { 322 // TODO: Have a property in renderList which holds the byte slice since variableBuffers and textBuffers have the same underlying implementation? 323 textBuffer = append(textBuffer, subBuffer) 324 renderList = append(renderList, menuRenderItem{0, len(textBuffer) - 1}) 325 } 326 327 return MenuTmpl{name, textBuffer, variableBuffer, renderList} 328 } 329 330 func (h *MenuListHolder) Scan(tmpls map[string]MenuTmpl, showItem func(i MenuItem) bool) (renderBuffer [][]byte, variableIndices []int, pathList []menuPath) { 331 for _, mitem := range h.List { 332 // Do we want this item in this variation of the menu? 333 if !showItem(mitem) { 334 continue 335 } 336 renderBuffer, variableIndices = h.ScanItem(tmpls, mitem, renderBuffer, variableIndices) 337 pathList = append(pathList, menuPath{mitem.Path, len(renderBuffer) - 1}) 338 } 339 340 // TODO: Need more coalescing in the renderBuffer 341 return renderBuffer, variableIndices, pathList 342 } 343 344 // Note: This doesn't do a visibility check like hold.Scan() does 345 func (h *MenuListHolder) ScanItem(tmpls map[string]MenuTmpl, mitem MenuItem, renderBuffer [][]byte, variableIndices []int) ([][]byte, []int) { 346 menuTmpl, ok := tmpls[mitem.TmplName] 347 if !ok { 348 menuTmpl = tmpls["menu_item"] 349 } 350 351 for _, renderItem := range menuTmpl.RenderList { 352 if renderItem.Type == 0 { 353 renderBuffer = append(renderBuffer, menuTmpl.TextBuffer[renderItem.Index]) 354 continue 355 } 356 357 variable := menuTmpl.VariableBuffer[renderItem.Index] 358 dotAt, hasDot := skipUntilIfExists(variable, 0, '.') 359 if !hasDot { 360 continue 361 } 362 363 if bytes.Equal(variable[:dotAt], []byte("lang")) { 364 renderBuffer = append(renderBuffer, []byte(phrases.GetTmplPhrase(string(bytes.TrimPrefix(variable[dotAt:], []byte(".")))))) 365 continue 366 } 367 368 var renderItem []byte 369 switch string(variable) { 370 case ".ID": 371 renderItem = []byte(strconv.Itoa(mitem.ID)) 372 case ".Name": 373 renderItem = []byte(mitem.Name) 374 case ".HTMLID": 375 renderItem = []byte(mitem.HTMLID) 376 case ".CSSClass": 377 renderItem = []byte(mitem.CSSClass) 378 case ".Position": 379 renderItem = []byte(mitem.Position) 380 case ".Path": 381 renderItem = []byte(mitem.Path) 382 case ".Aria": 383 renderItem = []byte(mitem.Aria) 384 case ".Tooltip": 385 renderItem = []byte(mitem.Tooltip) 386 case ".CSSActive": 387 renderItem = []byte("{dyn.active}") 388 } 389 390 _, hasInnerVar := skipUntilIfExists(renderItem, 0, '{') 391 if hasInnerVar { 392 DebugLog("inner var: ", string(renderItem)) 393 dotAt, hasDot := skipUntilIfExists(renderItem, 0, '.') 394 endFence, hasEndFence := skipUntilIfExists(renderItem, dotAt, '}') 395 if !hasDot || !hasEndFence || (endFence-dotAt) <= 1 { 396 renderBuffer = append(renderBuffer, renderItem) 397 variableIndices = append(variableIndices, len(renderBuffer)-1) 398 continue 399 } 400 401 if bytes.Equal(renderItem[1:dotAt], []byte("lang")) { 402 //fmt.Println("lang var: ", string(renderItem[dotAt+1:endFence])) 403 renderBuffer = append(renderBuffer, []byte(phrases.GetTmplPhrase(string(renderItem[dotAt+1:endFence])))) 404 } else { 405 fmt.Println("other var: ", string(variable[:dotAt])) 406 if len(renderItem) > 0 { 407 renderBuffer = append(renderBuffer, renderItem) 408 variableIndices = append(variableIndices, len(renderBuffer)-1) 409 } 410 } 411 continue 412 } 413 if len(renderItem) > 0 { 414 renderBuffer = append(renderBuffer, renderItem) 415 } 416 } 417 return renderBuffer, variableIndices 418 } 419 420 // TODO: Pre-render the lang stuff 421 func (h *MenuListHolder) Build(w io.Writer, user *User, pathPrefix string) error { 422 var mTmpl menuTmpl 423 if !user.Loggedin { 424 mTmpl = h.Variations[0] 425 } else if user.IsAdmin { 426 mTmpl = h.Variations[3] 427 } else if user.IsSuperMod { 428 mTmpl = h.Variations[2] 429 } else { 430 mTmpl = h.Variations[1] 431 } 432 if pathPrefix == "" { 433 pathPrefix = Config.DefaultPath 434 } 435 436 if len(mTmpl.VariableIndices) == 0 { 437 for _, renderItem := range mTmpl.RenderBuffer { 438 w.Write(renderItem) 439 } 440 return nil 441 } 442 443 nearIndex := 0 444 for index, renderItem := range mTmpl.RenderBuffer { 445 if index != mTmpl.VariableIndices[nearIndex] { 446 w.Write(renderItem) 447 continue 448 } 449 variable := renderItem 450 // ? - I can probably remove this check now that I've kicked it upstream, or we could keep it here for safety's sake? 451 if len(variable) == 0 { 452 continue 453 } 454 455 prevIndex := 0 456 for i := 0; i < len(renderItem); i++ { 457 fenceStart, hasFence := skipUntilIfExists(variable, i, '{') 458 if !hasFence { 459 continue 460 } 461 i = fenceStart 462 fenceEnd, hasFence := skipUntilIfExists(variable, fenceStart, '}') 463 if !hasFence { 464 continue 465 } 466 i = fenceEnd 467 dotAt, hasDot := skipUntilIfExists(variable, fenceStart, '.') 468 if !hasDot { 469 continue 470 } 471 472 switch string(variable[fenceStart+1 : dotAt]) { 473 case "me": 474 w.Write(variable[prevIndex:fenceStart]) 475 switch string(variable[dotAt+1 : fenceEnd]) { 476 case "Link": 477 w.Write([]byte(user.Link)) 478 case "Session": 479 w.Write([]byte(user.Session)) 480 } 481 prevIndex = fenceEnd 482 // TODO: Optimise this 483 case "dyn": 484 w.Write(variable[prevIndex:fenceStart]) 485 var pmi int 486 for ii, pathItem := range mTmpl.PathMappings { 487 pmi = ii 488 if pathItem.Index > index { 489 break 490 } 491 } 492 493 if len(mTmpl.PathMappings) != 0 { 494 path := mTmpl.PathMappings[pmi].Path 495 if path == "" || path == "/" { 496 path = Config.DefaultPath 497 } 498 if strings.HasPrefix(path, pathPrefix) { 499 w.Write([]byte(" menu_active")) 500 } 501 } 502 503 prevIndex = fenceEnd 504 } 505 } 506 507 w.Write(variable[prevIndex : len(variable)-1]) 508 if len(mTmpl.VariableIndices) > (nearIndex + 1) { 509 nearIndex++ 510 } 511 } 512 return nil 513 }