Client templates can now be overriden like every other template.

The client templates load earlier now for a smoother user experience.
Added a setting for setting a Google Site Verification meta tag without editing header.html
Added support for favicons. /static/favicon.ico will be mapped to favicon.ico, if it exists.
The parent forum is now visible on the topics list for Nox.

Language headers which contain the wildcard character are no longer considered unknowns.
Meta descriptions and open graph descriptions are no longer emitted for logged in users.
Slimmed down topics_topic slightly for Nox.
Pre-parsed widgets are now minified.
Stale WebSockets connections should be cleaned up far quicker now.
Template generation is now logged separately.
Commented out some obsolete template logic.
Marked a few template generator fields as unexported.

Fixed the styling for the ban page in the profile for Nox.
Fixed the styling for colline for Cosora and Tempra Simple.
Fixed the sidebar overflowing outside of the box on Nox.
Fixed the meta description text overflowing the box in the Setting Manager on Nox.
Fixed excessive padding in the Page Manager.
Fixed a few missing border on the profiles for Tempra Simple.
Fixed the sidebar appearing in places it shouldn't on Tempra Simple.
Fixed the status code emitted by NotFoundJS
Fixed a bug where Gosora kept falling back to interpreted templates.
Fixed a bug where WebSockets connections weren't getting closed properly if the user cache overflowed.
Fixed a bug where WebSocket connections weren't getting initialised for guests.
Fixed a bug where template overrides weren't always getting applied.
Fixed a bug where root template overrides weren't always getting applied.

Added the google_site_verify setting.
Added the google_site_verify phrase.

You will need to run the patcher or updater for this commit.
This commit is contained in:
Azareal 2019-02-28 17:28:17 +10:00
parent fe33112827
commit cc1d0f089a
37 changed files with 671 additions and 190 deletions

View File

@ -125,6 +125,7 @@ func seedTables(adapter qgen.Adapter) error {
qgen.Install.SimpleInsert("settings", "name, content, type", "'megapost_min_words','1000','int'") qgen.Install.SimpleInsert("settings", "name, content, type", "'megapost_min_words','1000','int'")
qgen.Install.SimpleInsert("settings", "name, content, type", "'meta_desc','','html-attribute'") qgen.Install.SimpleInsert("settings", "name, content, type", "'meta_desc','','html-attribute'")
qgen.Install.SimpleInsert("settings", "name, content, type", "'rapid_loading','1','bool'") qgen.Install.SimpleInsert("settings", "name, content, type", "'rapid_loading','1','bool'")
qgen.Install.SimpleInsert("settings", "name, content, type", "'google_site_verify','','html-attribute'")
qgen.Install.SimpleInsert("themes", "uname, default", "'cosora',1") qgen.Install.SimpleInsert("themes", "uname, default", "'cosora',1")
qgen.Install.SimpleInsert("emails", "email, uid, validated", "'admin@localhost',1,1") // ? - Use a different default email or let the admin input it during installation? qgen.Install.SimpleInsert("emails", "email, uid, validated", "'admin@localhost',1,1") // ? - Use a different default email or let the admin input it during installation?

View File

@ -311,6 +311,12 @@ func SecurityError(w http.ResponseWriter, r *http.Request, user User) RouteError
return HandledRouteError() return HandledRouteError()
} }
func MicroNotFound(w http.ResponseWriter, r *http.Request) RouteError {
w.WriteHeader(404)
_, _ = w.Write([]byte("file not found"))
return HandledRouteError()
}
// NotFound is used when the requested page doesn't exist // NotFound is used when the requested page doesn't exist
// ? - Add a JSQ version of this? // ? - Add a JSQ version of this?
// ? - Add a user parameter? // ? - Add a user parameter?
@ -320,7 +326,7 @@ func NotFound(w http.ResponseWriter, r *http.Request, header *Header) RouteError
// ? - Add a user parameter? // ? - Add a user parameter?
func NotFoundJS(w http.ResponseWriter, r *http.Request) RouteError { func NotFoundJS(w http.ResponseWriter, r *http.Request) RouteError {
w.WriteHeader(401) w.WriteHeader(404)
writeJsonError(phrases.GetErrorPhrase("not_found_body"), w) writeJsonError(phrases.GetErrorPhrase("not_found_body"), w)
return HandledRouteError() return HandledRouteError()
} }

View File

@ -39,17 +39,6 @@ type CSSData struct {
func (list SFileList) JSTmplInit() error { func (list SFileList) JSTmplInit() error {
DebugLog("Initialising the client side templates") DebugLog("Initialising the client side templates")
var fragMap = make(map[string][][]byte)
var parseFrags = func(name string) {
fragMap[name] = tmpl.GetFrag(name)
}
parseFrags("alert")
parseFrags("forum")
parseFrags("topics_topic")
parseFrags("topic_posts")
parseFrags("topic_alt_posts")
parseFrags("paginator")
DebugLog("fragMap: ", fragMap)
return filepath.Walk("./tmpl_client", func(path string, f os.FileInfo, err error) error { return filepath.Walk("./tmpl_client", func(path string, f os.FileInfo, err error) error {
if f.IsDir() || strings.HasSuffix(path, "template_list.go") || strings.HasSuffix(path, "stub.go") { if f.IsDir() || strings.HasSuffix(path, "template_list.go") || strings.HasSuffix(path, "stub.go") {
return nil return nil
@ -75,7 +64,7 @@ func (list SFileList) JSTmplInit() error {
} }
data = data[startIndex-len([]byte("func init() {")):] data = data[startIndex-len([]byte("func init() {")):]
data = replace(data, "func ", "function ") data = replace(data, "func ", "function ")
data = replace(data, "function init() {", "tmplInits[\""+tmplName+"\"] = ") data = replace(data, "function init() {", "if(tmplInits===undefined) var tmplInits = {};\ntmplInits[\""+tmplName+"\"] = ")
data = replace(data, " error {\n", " {\nlet out = \"\"\n") data = replace(data, " error {\n", " {\nlet out = \"\"\n")
funcIndex, hasFunc := skipAllUntilCharsExist(data, 0, []byte("function Template_")) funcIndex, hasFunc := skipAllUntilCharsExist(data, 0, []byte("function Template_"))
if !hasFunc { if !hasFunc {
@ -220,19 +209,23 @@ func (list SFileList) JSTmplInit() error {
data = replace(data, `= data = replace(data, `=
}`, "= []") }`, "= []")
fragset, ok := fragMap[shortName] fragset := tmpl.GetFrag(shortName)
if !ok { if fragset != nil {
DebugLog("tmplName: ", tmplName)
return errors.New("couldn't find template in fragmap")
}
var sfrags = []byte("let " + shortName + "_frags = [];\n") var sfrags = []byte("let " + shortName + "_frags = [];\n")
for _, frags := range fragset { for _, frags := range fragset {
sfrags = append(sfrags, []byte(shortName+"_frags.push(`"+string(frags)+"`);\n")...) sfrags = append(sfrags, []byte(shortName+"_frags.push(`"+string(frags)+"`);\n")...)
} }
data = append(sfrags, data...) data = append(sfrags, data...)
}
data = replace(data, "\n;", "\n") data = replace(data, "\n;", "\n")
for name, _ := range Themes {
if strings.HasSuffix(shortName, "_"+name) {
data = append(data, "\nlet Template_"+strings.TrimSuffix(shortName, "_"+name)+" = Template_"+shortName+";"...)
break
}
}
path = tmplName + ".js" path = tmplName + ".js"
DebugLog("js path: ", path) DebugLog("js path: ", path)
var ext = filepath.Ext("/tmpl_client/" + path) var ext = filepath.Ext("/tmpl_client/" + path)

View File

@ -17,6 +17,7 @@ type Header struct {
//Title []byte // Experimenting with []byte for increased efficiency, let's avoid converting too many things to []byte, as it involves a lot of extra boilerplate //Title []byte // Experimenting with []byte for increased efficiency, let's avoid converting too many things to []byte, as it involves a lot of extra boilerplate
NoticeList []string NoticeList []string
Scripts []string Scripts []string
PreScriptsAsync []string
//Preload []string //Preload []string
Stylesheets []string Stylesheets []string
Widgets PageWidgets Widgets PageWidgets
@ -34,6 +35,7 @@ type Header struct {
MetaDesc string MetaDesc string
//OGImage string //OGImage string
OGDesc string OGDesc string
GoogSiteVerify string
LooseCSP bool LooseCSP bool
StartedAt time.Time StartedAt time.Time
Elapsed1 string Elapsed1 string
@ -45,6 +47,10 @@ func (header *Header) AddScript(name string) {
header.Scripts = append(header.Scripts, name) header.Scripts = append(header.Scripts, name)
} }
func (header *Header) AddPreScriptAsync(name string) {
header.PreScriptsAsync = append(header.PreScriptsAsync, name)
}
/*func (header *Header) Preload(name string) { /*func (header *Header) Preload(name string) {
header.Preload = append(header.Preload, name) header.Preload = append(header.Preload, name)
}*/ }*/

View File

@ -150,6 +150,18 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
stats.Themes = len(Themes) stats.Themes = len(Themes)
stats.Reports = 0 // TODO: Do the report count. Only show open threads? stats.Reports = 0 // TODO: Do the report count. Only show open threads?
var addPreScript = func(name string) {
var tname string
if theme.OverridenMap != nil {
_, ok := theme.OverridenMap[name+".html"]
if ok {
tname = "_" + theme.Name
}
}
header.AddPreScriptAsync("template_" + name + tname + ".js")
}
addPreScript("alert")
return header, stats, nil return header, stats, nil
} }
@ -190,6 +202,7 @@ func userCheck(w http.ResponseWriter, r *http.Request, user *User) (header *Head
Hooks: GetHookTable(), Hooks: GetHookTable(),
Zone: "frontend", Zone: "frontend",
Writer: w, Writer: w,
GoogSiteVerify: header.Settings["google_site_verify"].(string),
} }
if user.IsBanned { if user.IsBanned {
@ -222,6 +235,22 @@ func userCheck(w http.ResponseWriter, r *http.Request, user *User) (header *Head
} }
} }
var addPreScript = func(name string) {
var tname string
if theme.OverridenMap != nil {
//fmt.Printf("name %+v\n", name)
//fmt.Printf("theme.OverridenMap %+v\n", theme.OverridenMap)
_, ok := theme.OverridenMap[name+".html"]
if ok {
tname = "_" + theme.Name
}
}
header.AddPreScriptAsync("template_" + name + tname + ".js")
}
addPreScript("topics_topic")
addPreScript("paginator")
addPreScript("alert")
return header, nil return header, nil
} }

View File

@ -108,8 +108,9 @@ func tmplInitHeaders(user User, user2 User, user3 User) (*Header, *Header, *Head
Theme: Themes[DefaultThemeBox.Load().(string)], Theme: Themes[DefaultThemeBox.Load().(string)],
CurrentUser: user, CurrentUser: user,
NoticeList: []string{"test"}, NoticeList: []string{"test"},
Stylesheets: []string{"panel"}, Stylesheets: []string{"panel.css"},
Scripts: []string{"whatever"}, Scripts: []string{"whatever.js"},
PreScriptsAsync: []string{"whatever.js"},
Widgets: PageWidgets{ Widgets: PageWidgets{
LeftSidebar: template.HTML("lalala"), LeftSidebar: template.HTML("lalala"),
}, },
@ -168,7 +169,7 @@ func CompileTemplates() error {
config.Debug = Dev.DebugMode config.Debug = Dev.DebugMode
config.SuperDebug = Dev.TemplateDebug config.SuperDebug = Dev.TemplateDebug
c := tmpl.NewCTemplateSet() c := tmpl.NewCTemplateSet("normal")
c.SetConfig(config) c.SetConfig(config)
c.SetBaseImportMap(map[string]string{ c.SetBaseImportMap(map[string]string{
"io": "io", "io": "io",
@ -189,6 +190,7 @@ func CompileTemplates() error {
log.Print("Compiling the per-theme templates") log.Print("Compiling the per-theme templates")
for theme, tmpls := range oroots { for theme, tmpls := range oroots {
c.ResetLogs("normal-" + theme)
c.SetThemeName(theme) c.SetThemeName(theme)
c.SetPerThemeTmpls(tmpls) c.SetPerThemeTmpls(tmpls)
log.Print("theme: ", theme) log.Print("theme: ", theme)
@ -366,7 +368,156 @@ func compileTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName string
return nil return nil
} }
// ? - Add template hooks?
func CompileJSTemplates() error { func CompileJSTemplates() error {
log.Print("Compiling the JS templates")
// TODO: Implement per-theme template overrides here too
var overriden = make(map[string]map[string]bool)
for _, theme := range Themes {
overriden[theme.Name] = make(map[string]bool)
log.Printf("theme.OverridenTemplates: %+v\n", theme.OverridenTemplates)
for _, override := range theme.OverridenTemplates {
overriden[theme.Name][override] = true
}
}
log.Printf("overriden: %+v\n", overriden)
var config tmpl.CTemplateConfig
config.Minify = Config.MinifyTemplates
config.Debug = Dev.DebugMode
config.SuperDebug = Dev.TemplateDebug
config.SkipHandles = true
config.SkipTmplPtrMap = true
config.SkipInitBlock = false
config.PackageName = "tmpl"
c := tmpl.NewCTemplateSet("js")
c.SetConfig(config)
c.SetBuildTags("!no_templategen")
c.SetOverrideTrack(overriden)
c.SetPerThemeTmpls(make(map[string]bool))
log.Print("Compiling the default templates")
var wg sync.WaitGroup
err := compileJSTemplates(&wg, c, "")
if err != nil {
return err
}
oroots := c.GetOverridenRoots()
log.Printf("oroots: %+v\n", oroots)
log.Print("Compiling the per-theme templates")
for theme, tmpls := range oroots {
c.SetThemeName(theme)
c.SetPerThemeTmpls(tmpls)
log.Print("theme: ", theme)
log.Printf("perThemeTmpls: %+v\n", tmpls)
err = compileJSTemplates(&wg, c, theme)
if err != nil {
return err
}
}
var dirPrefix = "./tmpl_client/"
writeTemplateList(c, &wg, dirPrefix)
return nil
}
func compileJSTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName string) error {
user, user2, user3 := tmplInitUsers()
header, _, _ := tmplInitHeaders(user, user2, user3)
now := time.Now()
var varList = make(map[string]tmpl.VarItem)
c.SetBaseImportMap(map[string]string{
"io": "io",
"github.com/Azareal/Gosora/common/alerts": "github.com/Azareal/Gosora/common/alerts",
})
// TODO: Check what sort of path is sent exactly and use it here
alertItem := alerts.AlertItem{Avatar: "", ASID: 1, Path: "/", Message: "uh oh, something happened"}
alertTmpl, err := c.Compile("alert.html", "templates/", "alerts.AlertItem", alertItem, varList)
if err != nil {
return err
}
c.SetBaseImportMap(map[string]string{
"io": "io",
"github.com/Azareal/Gosora/common": "github.com/Azareal/Gosora/common",
})
// TODO: Fix the import loop so we don't have to use this hack anymore
c.SetBuildTags("!no_templategen,tmplgentopic")
tmpls := TItemHold(make(map[string]TItem))
var topicsRow = &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, user3.ID, 1, 1, "", "127.0.0.1", 1, 0, 1, 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"}
tmpls.AddStd("topics_topic", "common.TopicsRow", topicsRow)
poll := Poll{ID: 1, Type: 0, Options: map[int]string{0: "Nothing", 1: "Something"}, Results: map[int]int{0: 5, 1: 2}, QuickOptions: []PollOption{
PollOption{0, "Nothing"},
PollOption{1, "Something"},
}, VoteCount: 7}
avatar, microAvatar := BuildAvatar(62, "")
miniAttach := []*MiniAttachment{&MiniAttachment{Path: "/"}}
topic := TopicUser{1, "blah", "Blah", "Hey there!", 62, false, false, now, now, 1, 1, 0, "", "127.0.0.1", 1, 0, 1, 0, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", "", "", 58, false, miniAttach}
var replyList []ReplyUser
// TODO: Do we really want the UID here to be zero?
avatar, microAvatar = BuildAvatar(0, "")
replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", Config.DefaultGroup, now, 0, 0, avatar, microAvatar, "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, 1, "", "", miniAttach})
varList = make(map[string]tmpl.VarItem)
header.Title = "Topic Name"
tpage := TopicPage{header, replyList, topic, &Forum{ID: 1, Name: "Hahaha"}, poll, Paginator{[]int{1}, 1, 1}}
tpage.Forum.Link = BuildForumURL(NameToSlug(tpage.Forum.Name), tpage.Forum.ID)
tmpls.AddStd("topic_posts", "common.TopicPage", tpage)
tmpls.AddStd("topic_alt_posts", "common.TopicPage", tpage)
itemsPerPage := 25
_, page, lastPage := PageOffset(20, 1, itemsPerPage)
pageList := Paginate(20, itemsPerPage, 5)
tmpls.AddStd("paginator", "common.Paginator", Paginator{pageList, page, lastPage})
var dirPrefix = "./tmpl_client/"
var writeTemplate = func(name string, content string) {
log.Print("Writing template '" + name + "'")
if content == "" {
return //log.Fatal("No content body")
}
wg.Add(1)
go func() {
tname := themeName
if tname != "" {
tname = "_" + tname
}
err := writeFile(dirPrefix+"template_"+name+tname+".go", content)
if err != nil {
log.Fatal(err)
}
wg.Done()
}()
}
log.Print("Writing the templates")
for name, titem := range tmpls {
log.Print("Writing " + name)
varList := make(map[string]tmpl.VarItem)
tmpl, err := c.Compile(name+".html", "templates/", titem.Expects, titem.ExpectsInt, varList)
if err != nil {
return err
}
writeTemplate(name, tmpl)
}
writeTemplate("alert", alertTmpl)
/*//writeTemplate("forum", forumTmpl)
writeTemplate("topics_topic", topicListItemTmpl)
writeTemplate("topic_posts", topicPostsTmpl)
writeTemplate("topic_alt_posts", topicAltPostsTmpl)
writeTemplate("paginator", paginatorTmpl)
//writeTemplate("panel_themes_widgets_widget", panelWidgetsWidgetTmpl)
writeTemplateList(c, &wg, dirPrefix)*/
return nil
}
/*func CompileJSTemplates() error {
log.Print("Compiling the JS templates") log.Print("Compiling the JS templates")
var config tmpl.CTemplateConfig var config tmpl.CTemplateConfig
config.Minify = Config.MinifyTemplates config.Minify = Config.MinifyTemplates
@ -443,12 +594,6 @@ func CompileJSTemplates() error {
return err return err
} }
/*widget := &Widget{ID: 0}
panelWidgetsWidgetTmpl, err := c.Compile("panel_themes_widgets_widget.html", "templates/", "*common.Widget", widget, varList)
if err != nil {
return err
}*/
var dirPrefix = "./tmpl_client/" var dirPrefix = "./tmpl_client/"
var wg sync.WaitGroup var wg sync.WaitGroup
var writeTemplate = func(name string, content string) { var writeTemplate = func(name string, content string) {
@ -474,9 +619,10 @@ func CompileJSTemplates() error {
//writeTemplate("panel_themes_widgets_widget", panelWidgetsWidgetTmpl) //writeTemplate("panel_themes_widgets_widget", panelWidgetsWidgetTmpl)
writeTemplateList(c, &wg, dirPrefix) writeTemplateList(c, &wg, dirPrefix)
return nil return nil
} }*/
func getTemplateList(c *tmpl.CTemplateSet, wg *sync.WaitGroup, prefix string) string { func getTemplateList(c *tmpl.CTemplateSet, wg *sync.WaitGroup, prefix string) string {
DebugLog("in getTemplateList")
pout := "\n// nolint\nfunc init() {\n" pout := "\n// nolint\nfunc init() {\n"
var tFragCount = make(map[string]int) var tFragCount = make(map[string]int)
var bodyMap = make(map[string]string) //map[body]fragmentPrefix var bodyMap = make(map[string]string) //map[body]fragmentPrefix
@ -484,6 +630,8 @@ func getTemplateList(c *tmpl.CTemplateSet, wg *sync.WaitGroup, prefix string) st
var tmpCount = 0 var tmpCount = 0
for _, frag := range c.FragOut { for _, frag := range c.FragOut {
front := frag.TmplName + "_frags[" + strconv.Itoa(frag.Index) + "]" front := frag.TmplName + "_frags[" + strconv.Itoa(frag.Index) + "]"
DebugLog("front: ", front)
DebugLog("frag.Body: ", frag.Body)
/*bodyMap, tok := tmplMap[frag.TmplName] /*bodyMap, tok := tmplMap[frag.TmplName]
if !tok { if !tok {
tmplMap[frag.TmplName] = make(map[string]string) tmplMap[frag.TmplName] = make(map[string]string)
@ -493,9 +641,12 @@ func getTemplateList(c *tmpl.CTemplateSet, wg *sync.WaitGroup, prefix string) st
if !ok { if !ok {
bodyMap[frag.Body] = front bodyMap[frag.Body] = front
var bits string var bits string
DebugLog("encoding frag.Body")
for _, char := range []byte(frag.Body) { for _, char := range []byte(frag.Body) {
if char == '\'' { if char == '\'' {
bits += "'\\" + string(char) + "'," bits += "'\\" + string(char) + "',"
} else if char < 32 {
bits += strconv.Itoa(int(char)) + ","
} else { } else {
bits += "'" + string(char) + "'," bits += "'" + string(char) + "',"
} }
@ -506,6 +657,7 @@ func getTemplateList(c *tmpl.CTemplateSet, wg *sync.WaitGroup, prefix string) st
tmpCount++ tmpCount++
//pout += front + " = []byte(`" + frag.Body + "`)\n" //pout += front + " = []byte(`" + frag.Body + "`)\n"
} else { } else {
DebugLog("encoding cached index " + fp)
pout += front + " = " + fp + "\n" pout += front + " = " + fp + "\n"
} }

View File

@ -6,7 +6,7 @@ import (
) )
// TODO: Write unit tests for this // TODO: Write unit tests for this
func minify(data string) string { func Minify(data string) string {
data = strings.Replace(data, "\t", "", -1) data = strings.Replace(data, "\t", "", -1)
data = strings.Replace(data, "\v", "", -1) data = strings.Replace(data, "\v", "", -1)
data = strings.Replace(data, "\n", "", -1) data = strings.Replace(data, "\n", "", -1)
@ -19,7 +19,7 @@ func minify(data string) string {
// TODO: Handle CSS nested in <style> tags? // TODO: Handle CSS nested in <style> tags?
// TODO: Write unit tests for this // TODO: Write unit tests for this
func minifyHTML(data string) string { func minifyHTML(data string) string {
return minify(data) return Minify(data)
} }
// TODO: Have static files use this // TODO: Have static files use this
@ -27,7 +27,7 @@ func minifyHTML(data string) string {
// TODO: Convert the rgb()s to hex codes? // TODO: Convert the rgb()s to hex codes?
// TODO: Write unit tests for this // TODO: Write unit tests for this
func minifyCSS(data string) string { func minifyCSS(data string) string {
return minify(data) return Minify(data)
} }
// TODO: Convert this to three character hex strings whenever possible? // TODO: Convert this to three character hex strings whenever possible?

View File

@ -2,14 +2,15 @@ package tmpl
import ( import (
"bytes" "bytes"
"fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"text/template/parse" "text/template/parse"
"time"
) )
// TODO: Turn this file into a library // TODO: Turn this file into a library
@ -46,8 +47,8 @@ type CTemplateSet struct {
fileDir string fileDir string
funcMap map[string]interface{} funcMap map[string]interface{}
importMap map[string]string importMap map[string]string
TemplateFragmentCount map[string]int //templateFragmentCount map[string]int
FragOnce map[string]bool fragOnce map[string]bool
fragmentCursor map[string]int fragmentCursor map[string]int
FragOut []OutFrag FragOut []OutFrag
fragBuf []Fragment fragBuf []Fragment
@ -68,9 +69,15 @@ type CTemplateSet struct {
overridenRoots map[string]map[string]bool overridenRoots map[string]map[string]bool
themeName string themeName string
perThemeTmpls map[string]bool perThemeTmpls map[string]bool
logger *log.Logger
} }
func NewCTemplateSet() *CTemplateSet { func NewCTemplateSet(in string) *CTemplateSet {
f, err := os.OpenFile("./logs/tmpls-"+in+"-"+strconv.FormatInt(time.Now().Unix(), 10)+".log", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0755)
if err != nil {
panic(err)
}
return &CTemplateSet{ return &CTemplateSet{
config: CTemplateConfig{ config: CTemplateConfig{
PackageName: "main", PackageName: "main",
@ -102,6 +109,7 @@ func NewCTemplateSet() *CTemplateSet {
"dyntmpl": true, "dyntmpl": true,
"index": true, "index": true,
}, },
logger: log.New(f, "", log.LstdFlags),
} }
} }
@ -140,6 +148,14 @@ func (c *CTemplateSet) SetPerThemeTmpls(perThemeTmpls map[string]bool) {
c.perThemeTmpls = perThemeTmpls c.perThemeTmpls = perThemeTmpls
} }
func (c *CTemplateSet) ResetLogs(in string) {
f, err := os.OpenFile("./logs/tmpls-"+in+"-"+strconv.FormatInt(time.Now().Unix(), 10)+".log", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0755)
if err != nil {
panic(err)
}
c.logger = log.New(f, "", log.LstdFlags)
}
type SkipBlock struct { type SkipBlock struct {
Frags map[int]int Frags map[int]int
LastCount int LastCount int
@ -187,13 +203,15 @@ import "errors"
if !c.config.SkipInitBlock { if !c.config.SkipInitBlock {
stub += "// nolint\nfunc init() {\n" stub += "// nolint\nfunc init() {\n"
if !c.config.SkipHandles { if !c.config.SkipHandles && c.themeName == "" {
stub += "\tcommon.Template_" + fname + "_handle = Template_" + fname + "\n" stub += "\tcommon.Template_" + fname + "_handle = Template_" + fname + "\n"
stub += "\tcommon.Ctemplates = append(common.Ctemplates,\"" + fname + "\")\n\tcommon.TmplPtrMap[\"" + fname + "\"] = &common.Template_" + fname + "_handle\n" stub += "\tcommon.Ctemplates = append(common.Ctemplates,\"" + fname + "\")\n"
} }
if !c.config.SkipTmplPtrMap { if !c.config.SkipTmplPtrMap {
stub += "\tcommon.TmplPtrMap[\"o_" + fname + "\"] = Template_" + fname + "\n" stub += "tmpl := Template_" + fname + "\n"
stub += "\tcommon.TmplPtrMap[\"" + fname + "\"] = &tmpl\n"
stub += "\tcommon.TmplPtrMap[\"o_" + fname + "\"] = tmpl\n"
} }
stub += "}\n\n" stub += "}\n\n"
@ -216,6 +234,7 @@ func Template_` + fname + `(tmpl_` + fname + `_i interface{}, w io.Writer) error
c.fileDir = fileDir c.fileDir = fileDir
content, err := c.loadTemplate(c.fileDir, name) content, err := c.loadTemplate(c.fileDir, name)
if err != nil { if err != nil {
c.detail("bailing out: ", err)
return "", "", "", err return "", "", "", err
} }
@ -235,11 +254,12 @@ func Template_` + fname + `(tmpl_` + fname + `_i interface{}, w io.Writer) error
func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (out string, err error) { func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (out string, err error) {
if c.config.Debug { if c.config.Debug {
fmt.Println("Compiling template '" + name + "'") c.logger.Println("Compiling template '" + name + "'")
} }
c.fileDir = fileDir c.fileDir = fileDir
content, err := c.loadTemplate(c.fileDir, name) content, err := c.loadTemplate(c.fileDir, name)
if err != nil { if err != nil {
c.detail("bailing out: ", err)
return "", err return "", err
} }
@ -247,6 +267,8 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe
} }
func (c *CTemplateSet) compile(name string, content string, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (out string, err error) { func (c *CTemplateSet) compile(name string, content string, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (out string, err error) {
//c.dumpCall("compile", name, content, expects, expectsInt, varList, imports)
//c.detailf("c: %+v\n", c)
c.importMap = map[string]string{} c.importMap = map[string]string{}
for index, item := range c.baseImportMap { for index, item := range c.baseImportMap {
c.importMap[index] = item c.importMap[index] = item
@ -273,6 +295,8 @@ func (c *CTemplateSet) compile(name string, content string, expects string, expe
if c.themeName != "" { if c.themeName != "" {
_, ok := c.perThemeTmpls[fname] _, ok := c.perThemeTmpls[fname]
if !ok { if !ok {
c.detail("fname not in c.perThemeTmpls")
c.detail("c.perThemeTmpls", c.perThemeTmpls)
return "", nil return "", nil
} }
fname += "_" + c.themeName fname += "_" + c.themeName
@ -283,6 +307,35 @@ func (c *CTemplateSet) compile(name string, content string, expects string, expe
fname += "_member" fname += "_member"
} }
c.detail("root overridenTrack loop")
c.detail("fname:", fname)
for themeName, track := range c.overridenTrack {
c.detail("themeName:", themeName)
c.detailf("track: %+v\n", track)
croot, ok := c.overridenRoots[themeName]
if !ok {
croot = make(map[string]bool)
c.overridenRoots[themeName] = croot
}
c.detailf("croot: %+v\n", croot)
for tmplName, _ := range track {
cname := tmplName
if c.guestOnly {
cname += "_guest"
} else if c.memberOnly {
cname += "_member"
}
c.detail("cname:", cname)
if fname == cname {
c.detail("match")
croot[strings.TrimSuffix(strings.TrimSuffix(fname, "_guest"), "_member")] = true
} else {
c.detail("no match")
}
}
}
c.detailf("c.overridenRoots: %+v\n", c.overridenRoots)
var outBuf []OutBufferFrame var outBuf []OutBufferFrame
var rootHold = "tmpl_" + fname + "_vars" var rootHold = "tmpl_" + fname + "_vars"
con := CContext{ con := CContext{
@ -298,27 +351,28 @@ func (c *CTemplateSet) compile(name string, content string, expects string, expe
c.localVars = make(map[string]map[string]VarItemReflect) c.localVars = make(map[string]map[string]VarItemReflect)
c.localVars[fname] = make(map[string]VarItemReflect) c.localVars[fname] = make(map[string]VarItemReflect)
c.localVars[fname]["."] = VarItemReflect{".", con.VarHolder, con.HoldReflect} c.localVars[fname]["."] = VarItemReflect{".", con.VarHolder, con.HoldReflect}
if c.FragOnce == nil { if c.fragOnce == nil {
c.FragOnce = make(map[string]bool) c.fragOnce = make(map[string]bool)
} }
c.fragmentCursor = map[string]int{fname: 0} c.fragmentCursor = map[string]int{fname: 0}
c.fragBuf = nil c.fragBuf = nil
c.langIndexToName = nil c.langIndexToName = nil
// TODO: Is this the first template loaded in? We really should have some sort of constructor for CTemplateSet // TODO: Is this the first template loaded in? We really should have some sort of constructor for CTemplateSet
if c.TemplateFragmentCount == nil { //if c.templateFragmentCount == nil {
c.TemplateFragmentCount = make(map[string]int) // c.templateFragmentCount = make(map[string]int)
} //}
//c.detailf("c: %+v\n", c)
startIndex := con.StartTemplate("") startIndex := con.StartTemplate("")
c.rootIterate(c.templateList[fname], con) c.rootIterate(c.templateList[fname], con)
con.EndTemplate("") con.EndTemplate("")
c.afterTemplate(con, startIndex) c.afterTemplate(con, startIndex)
c.TemplateFragmentCount[fname] = c.fragmentCursor[fname] + 1 //c.templateFragmentCount[fname] = c.fragmentCursor[fname] + 1
_, ok := c.FragOnce[fname] _, ok := c.fragOnce[fname]
if !ok { if !ok {
c.FragOnce[fname] = true c.fragOnce[fname] = true
} }
if len(c.langIndexToName) > 0 { if len(c.langIndexToName) > 0 {
c.importMap[langPkg] = langPkg c.importMap[langPkg] = langPkg
@ -346,13 +400,15 @@ func (c *CTemplateSet) compile(name string, content string, expects string, expe
} }
fout += "// nolint\nfunc init() {\n" fout += "// nolint\nfunc init() {\n"
if !c.config.SkipHandles { if !c.config.SkipHandles && c.themeName == "" {
fout += "\tcommon.Template_" + fname + "_handle = Template_" + fname + "\n" fout += "\tcommon.Template_" + fname + "_handle = Template_" + fname + "\n"
fout += "\tcommon.Ctemplates = append(common.Ctemplates,\"" + fname + "\")\n\tcommon.TmplPtrMap[\"" + fname + "\"] = &common.Template_" + fname + "_handle\n" fout += "\tcommon.Ctemplates = append(common.Ctemplates,\"" + fname + "\")\n"
} }
if !c.config.SkipTmplPtrMap { if !c.config.SkipTmplPtrMap {
fout += "\tcommon.TmplPtrMap[\"o_" + fname + "\"] = Template_" + fname + "\n" fout += "tmpl := Template_" + fname + "\n"
fout += "\tcommon.TmplPtrMap[\"" + fname + "\"] = &tmpl\n"
fout += "\tcommon.TmplPtrMap[\"o_" + fname + "\"] = tmpl\n"
} }
if len(c.langIndexToName) > 0 { if len(c.langIndexToName) > 0 {
fout += "\t" + fname + "_tmpl_phrase_id = phrases.RegisterTmplPhraseNames([]string{\n" fout += "\t" + fname + "_tmpl_phrase_id = phrases.RegisterTmplPhraseNames([]string{\n"
@ -458,9 +514,9 @@ w.Write([]byte(`, " + ", -1)
if c.config.Debug { if c.config.Debug {
for index, count := range c.stats { for index, count := range c.stats {
fmt.Println(index+": ", strconv.Itoa(count)) c.logger.Println(index+": ", strconv.Itoa(count))
} }
fmt.Println(" ") c.logger.Println(" ")
} }
c.detail("Output!") c.detail("Output!")
c.detail(fout) c.detail(fout)
@ -579,13 +635,15 @@ func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) {
} }
func (c *CTemplateSet) addText(con CContext, text []byte) { func (c *CTemplateSet) addText(con CContext, text []byte) {
c.dumpCall("addText", con, text)
tmpText := bytes.TrimSpace(text) tmpText := bytes.TrimSpace(text)
if len(tmpText) == 0 { if len(tmpText) == 0 {
return return
} }
nodeText := string(text) nodeText := string(text)
c.detail("con.TemplateName: ", con.TemplateName)
fragIndex := c.fragmentCursor[con.TemplateName] fragIndex := c.fragmentCursor[con.TemplateName]
_, ok := c.FragOnce[con.TemplateName] _, ok := c.fragOnce[con.TemplateName]
c.fragBuf = append(c.fragBuf, Fragment{nodeText, con.TemplateName, fragIndex, ok}) c.fragBuf = append(c.fragBuf, Fragment{nodeText, con.TemplateName, fragIndex, ok})
con.PushText(strconv.Itoa(fragIndex), fragIndex, len(c.fragBuf)-1) con.PushText(strconv.Itoa(fragIndex), fragIndex, len(c.fragBuf)-1)
c.fragmentCursor[con.TemplateName] = fragIndex + 1 c.fragmentCursor[con.TemplateName] = fragIndex + 1
@ -816,9 +874,9 @@ func (c *CTemplateSet) compileExprSwitch(con CContext, node *parse.CommandNode)
switch n := firstWord.(type) { switch n := firstWord.(type) {
case *parse.FieldNode: case *parse.FieldNode:
if c.config.SuperDebug { if c.config.SuperDebug {
fmt.Println("Field Node:", n.Ident) c.logger.Println("Field Node:", n.Ident)
for _, id := range n.Ident { for _, id := range n.Ident {
fmt.Println("Field Bit:", id) c.logger.Println("Field Bit:", id)
} }
} }
/* Use reflect to determine if the field is for a method, otherwise assume it's a variable. Coming Soon. */ /* Use reflect to determine if the field is for a method, otherwise assume it's a variable. Coming Soon. */
@ -851,8 +909,8 @@ func (c *CTemplateSet) compileExprSwitch(con CContext, node *parse.CommandNode)
} }
func (c *CTemplateSet) unknownNode(node parse.Node) { func (c *CTemplateSet) unknownNode(node parse.Node) {
fmt.Println("Unknown Kind:", reflect.ValueOf(node).Elem().Kind()) c.logger.Println("Unknown Kind:", reflect.ValueOf(node).Elem().Kind())
fmt.Println("Unknown Type:", reflect.ValueOf(node).Elem().Type().Name()) c.logger.Println("Unknown Type:", reflect.ValueOf(node).Elem().Type().Name())
panic("I don't know what node this is! Grr...") panic("I don't know what node this is! Grr...")
} }
@ -893,12 +951,12 @@ func (c *CTemplateSet) simpleMath(con CContext, pos int, node *parse.CommandNode
func (c *CTemplateSet) compareJoin(con CContext, pos int, node *parse.CommandNode, symbol string) (pos2 int, out string) { func (c *CTemplateSet) compareJoin(con CContext, pos int, node *parse.CommandNode, symbol string) (pos2 int, out string) {
c.detailf("Building %s function", symbol) c.detailf("Building %s function", symbol)
if pos == 0 { if pos == 0 {
fmt.Println("pos:", pos) c.logger.Println("pos:", pos)
panic(symbol + " is missing a left operand") panic(symbol + " is missing a left operand")
} }
if len(node.Args) <= pos { if len(node.Args) <= pos {
fmt.Println("post pos:", pos) c.logger.Println("post pos:", pos)
fmt.Println("len(node.Args):", len(node.Args)) c.logger.Println("len(node.Args):", len(node.Args))
panic(symbol + " is missing a right operand") panic(symbol + " is missing a right operand")
} }
@ -1090,9 +1148,9 @@ func (c *CTemplateSet) compileReflectSwitch(con CContext, node *parse.CommandNod
switch n := firstWord.(type) { switch n := firstWord.(type) {
case *parse.FieldNode: case *parse.FieldNode:
if c.config.SuperDebug { if c.config.SuperDebug {
fmt.Println("Field Node:", n.Ident) c.logger.Println("Field Node:", n.Ident)
for _, id := range n.Ident { for _, id := range n.Ident {
fmt.Println("Field Bit:", id) c.logger.Println("Field Bit:", id)
} }
} }
/* Use reflect to determine if the field is for a method, otherwise assume it's a variable. Coming Soon. */ /* Use reflect to determine if the field is for a method, otherwise assume it's a variable. Coming Soon. */
@ -1176,12 +1234,12 @@ func (c *CTemplateSet) compileIfVarSub(con CContext, varname string) (out string
cur = cur.FieldByName(bit) cur = cur.FieldByName(bit)
out += "." + bit out += "." + bit
if !cur.IsValid() { if !cur.IsValid() {
fmt.Println("cur: ", cur) c.logger.Println("cur: ", cur)
panic(out + "^\n" + "Invalid value. Maybe, it doesn't exist?") panic(out + "^\n" + "Invalid value. Maybe, it doesn't exist?")
} }
stepInterface() stepInterface()
if !cur.IsValid() { if !cur.IsValid() {
fmt.Println("cur: ", cur) c.logger.Println("cur: ", cur)
panic(out + "^\n" + "Invalid value. Maybe, it doesn't exist?") panic(out + "^\n" + "Invalid value. Maybe, it doesn't exist?")
} }
dumpKind("Data") dumpKind("Data")
@ -1219,9 +1277,9 @@ func (c *CTemplateSet) compileBoolSub(con CContext, varname string) string {
case reflect.Slice, reflect.Map: case reflect.Slice, reflect.Map:
out = "len(" + out + ") != 0" out = "len(" + out + ") != 0"
default: default:
fmt.Println("Variable Name:", varname) c.logger.Println("Variable Name:", varname)
fmt.Println("Variable Holder:", con.VarHolder) c.logger.Println("Variable Holder:", con.VarHolder)
fmt.Println("Variable Kind:", con.HoldReflect.Kind()) c.logger.Println("Variable Kind:", con.HoldReflect.Kind())
panic("I don't know what this variable's type is o.o\n") panic("I don't know what this variable's type is o.o\n")
} }
c.retCall("compileBoolSub", out) c.retCall("compileBoolSub", out)
@ -1398,17 +1456,17 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V
if !val.IsValid() { if !val.IsValid() {
panic(assLines + varname + "^\n" + "Invalid value. Maybe, it doesn't exist?") panic(assLines + varname + "^\n" + "Invalid value. Maybe, it doesn't exist?")
} }
fmt.Println("Unknown Struct Name:", varname) c.logger.Println("Unknown Struct Name:", varname)
fmt.Println("Unknown Struct:", val.Type().Name()) c.logger.Println("Unknown Struct:", val.Type().Name())
panic("-- I don't know what this variable's type is o.o\n") panic("-- I don't know what this variable's type is o.o\n")
} }
default: default:
if !val.IsValid() { if !val.IsValid() {
panic(assLines + varname + "^\n" + "Invalid value. Maybe, it doesn't exist?") panic(assLines + varname + "^\n" + "Invalid value. Maybe, it doesn't exist?")
} }
fmt.Println("Unknown Variable Name:", varname) c.logger.Println("Unknown Variable Name:", varname)
fmt.Println("Unknown Kind:", val.Kind()) c.logger.Println("Unknown Kind:", val.Kind())
fmt.Println("Unknown Type:", val.Type().Name()) c.logger.Println("Unknown Type:", val.Type().Name())
panic("-- I don't know what this variable's type is o.o\n") panic("-- I don't know what this variable's type is o.o\n")
} }
c.detail("base: ", base) c.detail("base: ", base)
@ -1427,17 +1485,25 @@ func (c *CTemplateSet) compileSubTemplate(pcon CContext, node *parse.TemplateNod
// TODO: Cascade errors back up the tree to the caller? // TODO: Cascade errors back up the tree to the caller?
content, err := c.loadTemplate(c.fileDir, node.Name) content, err := c.loadTemplate(c.fileDir, node.Name)
if err != nil { if err != nil {
log.Fatal(err) c.logger.Fatal(err)
} }
tree := parse.New(node.Name, c.funcMap) tree := parse.New(node.Name, c.funcMap)
var treeSet = make(map[string]*parse.Tree) var treeSet = make(map[string]*parse.Tree)
tree, err = tree.Parse(content, "{{", "}}", treeSet, c.funcMap) tree, err = tree.Parse(content, "{{", "}}", treeSet, c.funcMap)
if err != nil { if err != nil {
log.Fatal(err) c.logger.Fatal(err)
} }
fname := strings.TrimSuffix(node.Name, filepath.Ext(node.Name)) fname := strings.TrimSuffix(node.Name, filepath.Ext(node.Name))
if c.themeName != "" {
_, ok := c.perThemeTmpls[fname]
if !ok {
c.detail("fname not in c.perThemeTmpls")
c.detail("c.perThemeTmpls", c.perThemeTmpls)
}
fname += "_" + c.themeName
}
if c.guestOnly { if c.guestOnly {
fname += "_guest" fname += "_guest"
} else if c.memberOnly { } else if c.memberOnly {
@ -1523,11 +1589,11 @@ func (c *CTemplateSet) compileSubTemplate(pcon CContext, node *parse.TemplateNod
con.StartTemplate(startBit) con.StartTemplate(startBit)
c.rootIterate(subtree, con) c.rootIterate(subtree, con)
con.EndTemplate(endBit) con.EndTemplate(endBit)
c.TemplateFragmentCount[fname] = c.fragmentCursor[fname] + 1 //c.templateFragmentCount[fname] = c.fragmentCursor[fname] + 1
_, ok := c.FragOnce[fname] _, ok := c.fragOnce[fname]
if !ok { if !ok {
c.FragOnce[fname] = true c.fragOnce[fname] = true
} }
// map[string]map[string]bool // map[string]map[string]bool
@ -1562,13 +1628,16 @@ func (c *CTemplateSet) compileSubTemplate(pcon CContext, node *parse.TemplateNod
} }
func (c *CTemplateSet) loadTemplate(fileDir string, name string) (content string, err error) { func (c *CTemplateSet) loadTemplate(fileDir string, name string) (content string, err error) {
c.dumpCall("loadTemplate", fileDir, name)
c.detail("c.themeName: ", c.themeName)
if c.themeName != "" { if c.themeName != "" {
c.detail("per-theme override: ", "./themes/"+c.themeName+"/overrides/"+name) c.detail("per-theme override: ", "./themes/"+c.themeName+"/overrides/"+name)
res, err := ioutil.ReadFile("./themes/" + c.themeName + "/overrides/" + name) res, err := ioutil.ReadFile("./themes/" + c.themeName + "/overrides/" + name)
if err == nil { if err == nil {
content = string(res) content = string(res)
if c.config.Minify { if c.config.Minify {
content = minify(content) content = Minify(content)
} }
return content, nil return content, nil
} }
@ -1586,7 +1655,7 @@ func (c *CTemplateSet) loadTemplate(fileDir string, name string) (content string
} }
content = string(res) content = string(res)
if c.config.Minify { if c.config.Minify {
content = minify(content) content = Minify(content)
} }
return content, nil return content, nil
} }
@ -1664,22 +1733,22 @@ func (c *CTemplateSet) afterTemplate(con CContext, startIndex int) {
func (c *CTemplateSet) detail(args ...interface{}) { func (c *CTemplateSet) detail(args ...interface{}) {
if c.config.SuperDebug { if c.config.SuperDebug {
log.Println(args...) c.logger.Println(args...)
} }
} }
func (c *CTemplateSet) detailf(left string, args ...interface{}) { func (c *CTemplateSet) detailf(left string, args ...interface{}) {
if c.config.SuperDebug { if c.config.SuperDebug {
log.Printf(left, args...) c.logger.Printf(left, args...)
} }
} }
func (c *CTemplateSet) error(args ...interface{}) { func (c *CTemplateSet) error(args ...interface{}) {
if c.config.Debug { if c.config.Debug {
log.Println(args...) c.logger.Println(args...)
} }
} }
func (c *CTemplateSet) critical(args ...interface{}) { func (c *CTemplateSet) critical(args ...interface{}) {
log.Println(args...) c.logger.Println(args...)
} }

View File

@ -41,7 +41,9 @@ type Theme struct {
Docks []string // Allowed Values: leftSidebar, rightSidebar, footer Docks []string // Allowed Values: leftSidebar, rightSidebar, footer
Settings map[string]ThemeSetting Settings map[string]ThemeSetting
IntTmplHandle *htmpl.Template IntTmplHandle *htmpl.Template
// TODO: Do we really need both OverridenTemplates AND OverridenMap?
OverridenTemplates []string OverridenTemplates []string
OverridenMap map[string]bool
Templates []TemplateMapping Templates []TemplateMapping
TemplatesMap map[string]string TemplatesMap map[string]string
TmplPtr map[string]interface{} TmplPtr map[string]interface{}
@ -297,6 +299,7 @@ func (theme *Theme) RunTmpl(template string, pi interface{}, w io.Writer) error
case func(interface{}, io.Writer) error: case func(interface{}, io.Writer) error:
return tmplO(pi, w) return tmplO(pi, w)
case nil, string: case nil, string:
//fmt.Println("falling back to interpreted for " + template)
mapping, ok := theme.TemplatesMap[template] mapping, ok := theme.TemplatesMap[template]
if !ok { if !ok {
mapping = template mapping = template
@ -330,6 +333,10 @@ func (theme *Theme) GetTmpl(template string) interface{} {
if ok { if ok {
return tmpl return tmpl
} }
tmpl, ok = TmplPtrMap[template+"_"+theme.Name]
if ok {
return tmpl
}
tmpl, ok = TmplPtrMap[template] tmpl, ok = TmplPtrMap[template]
if ok { if ok {
return tmpl return tmpl

View File

@ -146,6 +146,7 @@ func NewThemeList() (themes ThemeList, err error) {
} }
if len(overrides) > 0 { if len(overrides) > 0 {
var overCount = 0 var overCount = 0
theme.OverridenMap = make(map[string]bool)
for _, override := range overrides { for _, override := range overrides {
if override.IsDir() { if override.IsDir() {
continue continue
@ -157,7 +158,10 @@ func NewThemeList() (themes ThemeList, err error) {
continue continue
} }
overCount++ overCount++
theme.OverridenTemplates = append(theme.OverridenTemplates, strings.TrimSuffix(override.Name(), ext)) nosuf := strings.TrimSuffix(override.Name(), ext)
theme.OverridenTemplates = append(theme.OverridenTemplates, nosuf)
theme.OverridenMap[nosuf] = true
//theme.TmplPtr[nosuf] = TmplPtrMap["o_"+nosuf]
log.Print("succeeded") log.Print("succeeded")
} }
@ -247,7 +251,6 @@ func ResetTemplateOverrides() {
log.Print("The origin template doesn't exist!") log.Print("The origin template doesn't exist!")
return return
} }
destTmplPtr, ok := TmplPtrMap[name] destTmplPtr, ok := TmplPtrMap[name]
if !ok { if !ok {
log.Print("The destination template doesn't exist!") log.Print("The destination template doesn't exist!")

View File

@ -49,8 +49,10 @@ func RouteWebsockets(w http.ResponseWriter, r *http.Request, user User) RouteErr
// TODO: Spit out a 500 instead of nil? // TODO: Spit out a 500 instead of nil?
conn, err := wsUpgrader.Upgrade(w, r, nil) conn, err := wsUpgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
return nil return LocalError("unable to upgrade", w, r, user)
} }
defer conn.Close()
wsUser, err := WsHub.AddConn(user, conn) wsUser, err := WsHub.AddConn(user, conn)
if err != nil { if err != nil {
return nil return nil
@ -96,7 +98,6 @@ func RouteWebsockets(w http.ResponseWriter, r *http.Request, user User) RouteErr
}*/ }*/
} }
} }
conn.Close()
DebugLog("Closing connection for user " + strconv.Itoa(user.ID)) DebugLog("Closing connection for user " + strconv.Itoa(user.ID))
return nil return nil
} }

View File

@ -9,6 +9,8 @@ import (
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
min "github.com/Azareal/Gosora/common/templates"
) )
// TODO: Clean this file up // TODO: Clean this file up
@ -49,7 +51,11 @@ func preparseWidget(widget *Widget, wdata string) (err error) {
prebuildWidget := func(name string, data interface{}) (string, error) { prebuildWidget := func(name string, data interface{}) (string, error) {
var b bytes.Buffer var b bytes.Buffer
err := DefaultTemplates.ExecuteTemplate(&b, name+".html", data) err := DefaultTemplates.ExecuteTemplate(&b, name+".html", data)
return string(b.Bytes()), err content := string(b.Bytes())
if Config.MinifyTemplates {
content = min.Minify(content)
}
return content, err
} }
sbytes := []byte(wdata) sbytes := []byte(wdata)

View File

@ -38,6 +38,28 @@ func init() {
} }
func (hub *WsHubImpl) Start() { func (hub *WsHubImpl) Start() {
ticker := time.NewTicker(time.Minute * 5)
defer func() {
ticker.Stop()
}()
go func() {
for {
var item = func(lock *sync.RWMutex, userMap map[int]*WSUser) {
lock.RLock()
defer lock.RUnlock()
// TODO: Copy to temporary slice for less contention?
for _, user := range userMap {
user.Ping()
}
}
select {
case <-ticker.C:
item(&hub.evenUserLock, hub.evenOnlineUsers)
item(&hub.oddUserLock, hub.oddOnlineUsers)
}
}
}()
if Config.DisableLiveTopicList { if Config.DisableLiveTopicList {
return return
} }
@ -298,15 +320,10 @@ func (hub *WsHubImpl) removeUser(uid int) {
} }
func (hub *WsHubImpl) AddConn(user User, conn *websocket.Conn) (*WSUser, error) { func (hub *WsHubImpl) AddConn(user User, conn *websocket.Conn) (*WSUser, error) {
// TODO: How should we handle user state changes if we're holding a pointer which never changes?
userptr, err := Users.Get(user.ID)
if err != nil && err != ErrStoreCapacityOverflow {
return nil, err
}
if user.ID == 0 { if user.ID == 0 {
wsUser := new(WSUser) wsUser := new(WSUser)
wsUser.User = userptr wsUser.User = new(User)
*wsUser.User = user
wsUser.AddSocket(conn, "") wsUser.AddSocket(conn, "")
WsHub.GuestLock.Lock() WsHub.GuestLock.Lock()
WsHub.OnlineGuests[wsUser] = true WsHub.OnlineGuests[wsUser] = true
@ -314,6 +331,12 @@ func (hub *WsHubImpl) AddConn(user User, conn *websocket.Conn) (*WSUser, error)
return wsUser, nil return wsUser, nil
} }
// TODO: How should we handle user state changes if we're holding a pointer which never changes?
userptr, err := Users.Get(user.ID)
if err != nil && err != ErrStoreCapacityOverflow {
return nil, err
}
var mutex *sync.RWMutex var mutex *sync.RWMutex
var theMap map[int]*WSUser var theMap map[int]*WSUser
if user.ID%2 == 0 { if user.ID%2 == 0 {

View File

@ -3,6 +3,7 @@ package common
import ( import (
"errors" "errors"
"sync" "sync"
"time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
@ -21,6 +22,20 @@ type WSUserSocket struct {
Page string Page string
} }
func (wsUser *WSUser) Ping() error {
for _, socket := range wsUser.Sockets {
if socket == nil {
continue
}
socket.conn.SetWriteDeadline(time.Now().Add(time.Minute))
err := socket.conn.WriteMessage(websocket.PingMessage, nil)
if err != nil {
socket.conn.Close()
}
}
return nil
}
func (wsUser *WSUser) WriteAll(msg string) error { func (wsUser *WSUser) WriteAll(msg string) error {
msgbytes := []byte(msg) msgbytes := []byte(msg)
for _, socket := range wsUser.Sockets { for _, socket := range wsUser.Sockets {

View File

@ -862,12 +862,23 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if lang != "" { if lang != "" {
lang = strings.TrimSpace(lang) lang = strings.TrimSpace(lang)
lLang := strings.Split(lang,"-") lLang := strings.Split(lang,"-")
llLang := strings.Split(strings.Split(lLang[0],";")[0],",") tLang := strings.Split(strings.Split(lLang[0],";")[0],",")
common.DebugDetail("llLang:", llLang) common.DebugDetail("tLang:", tLang)
validCode := counters.LangViewCounter.Bump(llLang[0]) var llLang string
for _, seg := range tLang {
if seg == "*" {
continue
}
llLang = seg
}
if llLang == "" {
counters.LangViewCounter.Bump("none")
} else {
validCode := counters.LangViewCounter.Bump(llLang)
if !validCode { if !validCode {
r.DumpRequest(req,"Invalid ISO Code") r.DumpRequest(req,"Invalid ISO Code")
} }
}
} else { } else {
counters.LangViewCounter.Bump("none") counters.LangViewCounter.Bump("none")
} }
@ -2104,6 +2115,11 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
case "robots.txt": case "robots.txt":
counters.RouteViewCounter.Bump(134) counters.RouteViewCounter.Bump(134)
return routes.RobotsTxt(w,req) return routes.RobotsTxt(w,req)
case "favicon.ico":
req.URL.Path = "/static"+req.URL.Path+extraData
//log.Print("req.URL.Path: ",req.URL.Path)
routes.StaticFile(w,req)
return nil
/*case "sitemap.xml": /*case "sitemap.xml":
counters.RouteViewCounter.Bump(135) counters.RouteViewCounter.Bump(135)
return routes.SitemapXml(w,req)*/ return routes.SitemapXml(w,req)*/

View File

@ -51,7 +51,8 @@
"bigpost_min_words":"Big Post Minimum Words", "bigpost_min_words":"Big Post Minimum Words",
"megapost_min_words":"Mega Post Minimum Words", "megapost_min_words":"Mega Post Minimum Words",
"meta_desc":"Meta Description", "meta_desc":"Meta Description",
"rapid_loading":"Rapid Loaded?" "rapid_loading":"Rapid Loaded?",
"google_site_verify":"Google Site Verify"
}, },
"PermPresets": { "PermPresets": {

View File

@ -26,6 +26,7 @@ func init() {
addPatch(12, patch12) addPatch(12, patch12)
addPatch(13, patch13) addPatch(13, patch13)
addPatch(14, patch14) addPatch(14, patch14)
addPatch(15, patch15)
} }
func patch0(scanner *bufio.Scanner) (err error) { func patch0(scanner *bufio.Scanner) (err error) {
@ -532,3 +533,7 @@ func patch14(scanner *bufio.Scanner) error {
return nil return nil
} }
func patch15(scanner *bufio.Scanner) error {
return execStmt(qgen.Builder.SimpleInsert("settings", "name, content, type", "'google_site_verify','','html-attribute'"))
}

View File

@ -283,8 +283,7 @@ function runWebSockets() {
(() => { (() => {
addInitHook("pre_init", () => { addInitHook("pre_init", () => {
// We can only get away with this because template_alert has no phrases, otherwise it too would have to be part of the "dance", I miss Go concurrency :( // We can only get away with this because template_alert has no phrases, otherwise it too would have to be part of the "dance", I miss Go concurrency :(
loadScript("template_alert.js", () => { notifyOnScriptW("/static/template_alert", () => {}, () => {
console.log("Loaded template_alert.js");
addInitHook("after_phrases", () => { addInitHook("after_phrases", () => {
// TODO: The load part of loadAlerts could be done asynchronously while the update of the DOM could be deferred // TODO: The load part of loadAlerts could be done asynchronously while the update of the DOM could be deferred
$(document).ready(() => { $(document).ready(() => {
@ -471,6 +470,7 @@ function mainInit(){
let out = ""; let out = "";
for(let i = 0; i < topics.length;i++) out += Template_topics_topic(topics[i]); for(let i = 0; i < topics.length;i++) out += Template_topics_topic(topics[i]);
$(".topic_list").html(out); $(".topic_list").html(out);
//$(".topic_list").addClass("single_forum");
let obj = {Title: document.title, Url: url}; let obj = {Title: document.title, Url: url};
history.pushState(obj, obj.Title, obj.Url); history.pushState(obj, obj.Title, obj.Url);

View File

@ -2,7 +2,7 @@
var me = {}; var me = {};
var phraseBox = {}; var phraseBox = {};
var tmplInits = {}; if(tmplInits===undefined) var tmplInits = {};
var tmplPhrases = []; // [key] array of phrases indexed by order of use var tmplPhrases = []; // [key] array of phrases indexed by order of use
var hooks = { var hooks = {
"pre_iffe": [], "pre_iffe": [],
@ -52,42 +52,102 @@ function len(item) {
return item.length; return item.length;
} }
const asyncGetScript = (source) => { function asyncGetScript(source) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let script = document.createElement('script'); let script = document.createElement('script');
script.async = true; script.async = true;
const onloadHander = (haha, isAbort) => { const onloadHandler = (e, isAbort) => {
if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) { if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) {
script.onload = null; script.onload = null;
script.onreadystatechange = null; script.onreadystatechange = null;
script = undefined; script = undefined;
isAbort ? reject(haha) : resolve(); isAbort ? reject(e) : resolve();
} }
} }
script.onerror = (haha) => { script.onerror = (e) => {
reject(haha); reject(e);
}; };
script.onload = onloadHander; script.onload = onloadHandler;
script.onreadystatechange = onloadHander; script.onreadystatechange = onloadHandler;
script.src = source; script.src = source;
const prior = document.getElementsByTagName('script')[0]; const prior = document.getElementsByTagName('script')[0];
prior.parentNode.insertBefore(script, prior); prior.parentNode.insertBefore(script, prior);
}); });
}; }
function loadScript(name, callback) { function notifyOnScript(source) {
let url = "/static/"+name return new Promise((resolve, reject) => {
let script = document.querySelectorAll('[src^="'+source+'"]')[0];
if(script===undefined) {
reject("no script found");
return;
}
if(!script.readyState) {
resolve();
return;
}
const onloadHandler = (e, isAbort) => {
if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) {
script.onload = null;
script.onreadystatechange = null;
isAbort ? reject(e) : resolve();
}
}
script.onerror = (e) => {
reject(e);
};
script.onload = onloadHandler;
script.onreadystatechange = onloadHandler;
script.src = source;
});
}
function notifyOnScriptW(name, complete, success) {
notifyOnScript(name)
.then(() => {
console.log("Loaded " +name+".js");
complete();
if(success!==undefined) success();
}).catch((e) => {
console.log("Unable to get script name '"+name+"'");
console.log("e: ", e);
console.trace();
complete();
});
}
// TODO: Send data at load time so we don't have to rely on a fallback template here
function loadScript(name, callback,fail) {
let fname = name;
let value = "; " + document.cookie;
let parts = value.split("; current_theme=");
if (parts.length == 2) fname += "_"+ parts.pop().split(";").shift();
let url = "/static/"+fname+".js"
let iurl = "/static/"+name+".js"
asyncGetScript(url) asyncGetScript(url)
.then(callback) .then(callback)
.catch((haha) => { .catch((e) => {
console.log("Unable to get script '"+url+"'"); console.log("Unable to get script '"+url+"'");
console.log("haha: ", haha); if(fname!=name) {
asyncGetScript(iurl)
.then(callback)
.catch((e) => {
console.log("Unable to get script '"+iurl+"'");
console.log("e: ", e);
console.trace(); console.trace();
}); });
}
console.log("e: ", e);
console.trace();
fail(e);
});
} }
/* /*
@ -106,6 +166,7 @@ function RelativeTime(date) {
} }
function initPhrases() { function initPhrases() {
console.log("in initPhrases")
fetchPhrases("status,topic_list,alerts,paginator") fetchPhrases("status,topic_list,alerts,paginator")
} }
@ -144,13 +205,11 @@ function fetchPhrases(plist) {
runInitHook("pre_iife"); runInitHook("pre_iife");
let toLoad = 2; let toLoad = 2;
// TODO: Shunt this into loggedIn if there aren't any search and filter widgets? // TODO: Shunt this into loggedIn if there aren't any search and filter widgets?
loadScript("template_topics_topic.js", () => { notifyOnScriptW("/static/template_topics_topic", () => {
console.log("Loaded template_topics_topic.js");
toLoad--; toLoad--;
if(toLoad===0) initPhrases(); if(toLoad===0) initPhrases();
}); });
loadScript("template_paginator.js", () => { notifyOnScriptW("/static/template_paginator", () => {
console.log("Loaded template_paginator.js");
toLoad--; toLoad--;
if(toLoad===0) initPhrases(); if(toLoad===0) initPhrases();
}); });

View File

@ -654,12 +654,23 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if lang != "" { if lang != "" {
lang = strings.TrimSpace(lang) lang = strings.TrimSpace(lang)
lLang := strings.Split(lang,"-") lLang := strings.Split(lang,"-")
llLang := strings.Split(strings.Split(lLang[0],";")[0],",") tLang := strings.Split(strings.Split(lLang[0],";")[0],",")
common.DebugDetail("llLang:", llLang) common.DebugDetail("tLang:", tLang)
validCode := counters.LangViewCounter.Bump(llLang[0]) var llLang string
for _, seg := range tLang {
if seg == "*" {
continue
}
llLang = seg
}
if llLang == "" {
counters.LangViewCounter.Bump("none")
} else {
validCode := counters.LangViewCounter.Bump(llLang)
if !validCode { if !validCode {
r.DumpRequest(req,"Invalid ISO Code") r.DumpRequest(req,"Invalid ISO Code")
} }
}
} else { } else {
counters.LangViewCounter.Bump("none") counters.LangViewCounter.Bump("none")
} }
@ -734,6 +745,11 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
case "robots.txt": case "robots.txt":
counters.RouteViewCounter.Bump({{index .AllRouteMap "routes.RobotsTxt"}}) counters.RouteViewCounter.Bump({{index .AllRouteMap "routes.RobotsTxt"}})
return routes.RobotsTxt(w,req) return routes.RobotsTxt(w,req)
case "favicon.ico":
req.URL.Path = "/static"+req.URL.Path+extraData
//log.Print("req.URL.Path: ",req.URL.Path)
routes.StaticFile(w,req)
return nil
/*case "sitemap.xml": /*case "sitemap.xml":
counters.RouteViewCounter.Bump({{index .AllRouteMap "routes.SitemapXml"}}) counters.RouteViewCounter.Bump({{index .AllRouteMap "routes.SitemapXml"}})
return routes.SitemapXml(w,req)*/ return routes.SitemapXml(w,req)*/

View File

@ -21,7 +21,10 @@ func ParseSEOURL(urlBit string) (slug string, id int, err error) {
} }
func renderTemplate(tmplName string, w http.ResponseWriter, r *http.Request, header *common.Header, pi interface{}) common.RouteError { func renderTemplate(tmplName string, w http.ResponseWriter, r *http.Request, header *common.Header, pi interface{}) common.RouteError {
if header.MetaDesc != "" && header.OGDesc == "" { if header.CurrentUser.Loggedin {
header.MetaDesc = ""
header.OGDesc = ""
} else if header.MetaDesc != "" && header.OGDesc == "" {
header.OGDesc = header.MetaDesc header.OGDesc = header.MetaDesc
} }
// TODO: Expand this to non-HTTPS requests too // TODO: Expand this to non-HTTPS requests too

View File

@ -19,7 +19,7 @@ var cacheControlMaxAge = "max-age=" + strconv.Itoa(int(common.Day)) // TODO: Mak
func StaticFile(w http.ResponseWriter, r *http.Request) { func StaticFile(w http.ResponseWriter, r *http.Request) {
file, ok := common.StaticFiles.Get(r.URL.Path) file, ok := common.StaticFiles.Get(r.URL.Path)
if !ok { if !ok {
common.DebugLogf("Failed to find '%s'", r.URL.Path) common.DebugLogf("Failed to find '%s'", r.URL.Path) // TODO: Use MicroNotFound? Might be better than the unneccessary overhead of sprintf
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }

View File

@ -4,6 +4,7 @@ INSERT INTO [settings] ([name],[content],[type]) VALUES ('bigpost_min_words','25
INSERT INTO [settings] ([name],[content],[type]) VALUES ('megapost_min_words','1000','int'); INSERT INTO [settings] ([name],[content],[type]) VALUES ('megapost_min_words','1000','int');
INSERT INTO [settings] ([name],[content],[type]) VALUES ('meta_desc','','html-attribute'); INSERT INTO [settings] ([name],[content],[type]) VALUES ('meta_desc','','html-attribute');
INSERT INTO [settings] ([name],[content],[type]) VALUES ('rapid_loading','1','bool'); INSERT INTO [settings] ([name],[content],[type]) VALUES ('rapid_loading','1','bool');
INSERT INTO [settings] ([name],[content],[type]) VALUES ('google_site_verify','','html-attribute');
INSERT INTO [themes] ([uname],[default]) VALUES ('cosora',1); INSERT INTO [themes] ([uname],[default]) VALUES ('cosora',1);
INSERT INTO [emails] ([email],[uid],[validated]) VALUES ('admin@localhost',1,1); INSERT INTO [emails] ([email],[uid],[validated]) VALUES ('admin@localhost',1,1);
INSERT INTO [users_groups] ([name],[permissions],[plugin_perms],[is_mod],[is_admin],[tag]) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditUserGroupAdmin":false,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"EditGroupAdmin":false,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,1,'Admin'); INSERT INTO [users_groups] ([name],[permissions],[plugin_perms],[is_mod],[is_admin],[tag]) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditUserGroupAdmin":false,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"EditGroupAdmin":false,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,1,'Admin');

View File

@ -12,6 +12,7 @@ INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('bigpost_min_words','250
INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('megapost_min_words','1000','int'); INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('megapost_min_words','1000','int');
INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('meta_desc','','html-attribute'); INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('meta_desc','','html-attribute');
INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('rapid_loading','1','bool'); INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('rapid_loading','1','bool');
INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('google_site_verify','','html-attribute');
INSERT INTO `themes`(`uname`,`default`) VALUES ('cosora',1); INSERT INTO `themes`(`uname`,`default`) VALUES ('cosora',1);
INSERT INTO `emails`(`email`,`uid`,`validated`) VALUES ('admin@localhost',1,1); INSERT INTO `emails`(`email`,`uid`,`validated`) VALUES ('admin@localhost',1,1);
INSERT INTO `users_groups`(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`tag`) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditUserGroupAdmin":false,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"EditGroupAdmin":false,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,1,'Admin'); INSERT INTO `users_groups`(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`tag`) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditUserGroupAdmin":false,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"EditGroupAdmin":false,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,1,'Admin');

View File

@ -4,6 +4,7 @@ INSERT INTO "settings"("name","content","type") VALUES ('bigpost_min_words','250
INSERT INTO "settings"("name","content","type") VALUES ('megapost_min_words','1000','int'); INSERT INTO "settings"("name","content","type") VALUES ('megapost_min_words','1000','int');
INSERT INTO "settings"("name","content","type") VALUES ('meta_desc','','html-attribute'); INSERT INTO "settings"("name","content","type") VALUES ('meta_desc','','html-attribute');
INSERT INTO "settings"("name","content","type") VALUES ('rapid_loading','1','bool'); INSERT INTO "settings"("name","content","type") VALUES ('rapid_loading','1','bool');
INSERT INTO "settings"("name","content","type") VALUES ('google_site_verify','','html-attribute');
INSERT INTO "themes"("uname","default") VALUES ('cosora',1); INSERT INTO "themes"("uname","default") VALUES ('cosora',1);
INSERT INTO "emails"("email","uid","validated") VALUES ('admin@localhost',1,1); INSERT INTO "emails"("email","uid","validated") VALUES ('admin@localhost',1,1);
INSERT INTO "users_groups"("name","permissions","plugin_perms","is_mod","is_admin","tag") VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditUserGroupAdmin":false,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"EditGroupAdmin":false,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,1,'Admin'); INSERT INTO "users_groups"("name","permissions","plugin_perms","is_mod","is_admin","tag") VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditUserGroupAdmin":false,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"EditGroupAdmin":false,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true,"MoveTopic":true}','{}',1,1,'Admin');

View File

@ -45,7 +45,7 @@
</div> </div>
{{end}} {{end}}
{{end}} {{end}}
<div id="forum_topic_list" class="rowblock topic_list" aria-label="{{lang "forum_list_aria"}}"> <div id="forum_topic_list" class="rowblock topic_list single_forum" aria-label="{{lang "forum_list_aria"}}">
{{range .ItemList}}<div class="topic_row{{if .Sticky}} topic_sticky{{else if .IsClosed}} topic_closed{{end}}" data-tid="{{.ID}}"> {{range .ItemList}}<div class="topic_row{{if .Sticky}} topic_sticky{{else if .IsClosed}} topic_closed{{end}}" data-tid="{{.ID}}">
<div class="rowitem topic_left passive datarow"> <div class="rowitem topic_left passive datarow">
<span class="selector"></span> <span class="selector"></span>

View File

@ -5,6 +5,8 @@
<link href="/static/{{.Header.Theme.Name}}/main.css" rel="stylesheet" type="text/css"> <link href="/static/{{.Header.Theme.Name}}/main.css" rel="stylesheet" type="text/css">
{{range .Header.Stylesheets}} {{range .Header.Stylesheets}}
<link href="/static/{{.}}" rel="stylesheet" type="text/css">{{end}} <link href="/static/{{.}}" rel="stylesheet" type="text/css">{{end}}
{{range .Header.PreScriptsAsync}}
<script async type="text/javascript" src="/static/{{.}}"></script>{{end}}
<meta property="x-loggedin" content="{{.CurrentUser.Loggedin}}" /> <meta property="x-loggedin" content="{{.CurrentUser.Loggedin}}" />
<script type="text/javascript" src="/static/init.js"></script> <script type="text/javascript" src="/static/init.js"></script>
<script type="text/javascript" src="/static/jquery-3.1.1.min.js"></script> <script type="text/javascript" src="/static/jquery-3.1.1.min.js"></script>
@ -20,6 +22,7 @@
<meta name="twitter:title" content="{{.Title}} | {{.Header.Site.Name}}" /> <meta name="twitter:title" content="{{.Title}} | {{.Header.Site.Name}}" />
{{if .OGDesc}}<meta property="og:description" content="{{.OGDesc}}" /> {{if .OGDesc}}<meta property="og:description" content="{{.OGDesc}}" />
<meta property="twitter:description" content="{{.OGDesc}}" />{{end}} <meta property="twitter:description" content="{{.OGDesc}}" />{{end}}
{{if .GoogSiteVerify}}<meta name="google-site-verification" content="{{.GoogSiteVerify}}" />{{end}}
</head> </head>
<body> <body>
{{if not .CurrentUser.IsSuperMod}}<style>.supermod_only { display: none !important; }</style>{{end}} {{if not .CurrentUser.IsSuperMod}}<style>.supermod_only { display: none !important; }</style>{{end}}

View File

@ -5,7 +5,7 @@
<div class="colstack_item colstack_head"> <div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_pages_head"}}</h1></div> <div class="rowitem"><h1>{{lang "panel_pages_head"}}</h1></div>
</div> </div>
<div id="panel_settings" class="colstack_item rowlist"> <div id="panel_pages" class="colstack_item rowlist">
{{range .ItemList}} {{range .ItemList}}
<div class="rowitem panel_compactrow"> <div class="rowitem panel_compactrow">
<a href="/panel/pages/edit/{{.ID}}" class="panel_upshift">{{.Title}}</a>&nbsp;<a href="/pages/{{.Name}}">[{{.Name}}]</a> <a href="/panel/pages/edit/{{.ID}}" class="panel_upshift">{{.Title}}</a>&nbsp;<a href="/pages/{{.Name}}">[{{.Name}}]</a>

View File

@ -51,6 +51,7 @@
<div class="rowitem"><h1><a>{{lang "profile_ban_user_head"}}</a></h1></div> <div class="rowitem"><h1><a>{{lang "profile_ban_user_head"}}</a></h1></div>
</div> </div>
<form id="ban_user_form" class="hash_hide ban_user_hash" action="/users/ban/submit/{{.ProfileOwner.ID}}?session={{.CurrentUser.Session}}" method="post" style="display: none;"> <form id="ban_user_form" class="hash_hide ban_user_hash" action="/users/ban/submit/{{.ProfileOwner.ID}}?session={{.CurrentUser.Session}}" method="post" style="display: none;">
<div class="the_form">
{{/** TODO: Put a JS duration calculator here instead of this text? **/}} {{/** TODO: Put a JS duration calculator here instead of this text? **/}}
<div class="colline">{{lang "profile_ban_user_notice"}}</div> <div class="colline">{{lang "profile_ban_user_notice"}}</div>
<div class="colstack_item"> <div class="colstack_item">
@ -80,6 +81,7 @@
<div class="formitem"><button name="ban-button" class="formbutton form_middle_button">{{lang "profile_ban_user_button"}}</button></div> <div class="formitem"><button name="ban-button" class="formbutton form_middle_button">{{lang "profile_ban_user_button"}}</button></div>
</div> </div>
</div> </div>
</div>
</form> </form>
{{end}} {{end}}
{{end}} {{end}}

View File

@ -0,0 +1,8 @@
.colline {
border: 1px solid var(--element-border-color);
border-bottom: 2px solid var(--element-border-color);
background-color: var(--element-background-color);
padding: 8px;
margin-left: 16px;
margin-bottom: 12px;
}

View File

@ -0,0 +1,25 @@
<div class="topic_row{{if .Sticky}} topic_sticky{{else if .IsClosed}} topic_closed{{end}}" data-tid="{{.ID}}">
<div class="rowitem topic_left passive datarow">
<a href="{{.Creator.Link}}"><img src="{{.Creator.MicroAvatar}}" height=64 alt="{{.Creator.Name}}'s Avatar" title="{{.Creator.Name}}'s Avatar" /></a>
<span class="topic_inner_left">
<span class="rowtopic" itemprop="itemListElement" title="{{.Title}}"><a href="{{.Link}}">{{.Title}}</a>{{if .ForumName}}<a class="parent_forum_sep">-</a><a href="{{.ForumLink}}" title="{{.ForumName}}" class="rowsmall parent_forum">{{.ForumName}}</a>{{end}}</span>
<br /><a class="rowsmall starter" href="{{.Creator.Link}}" title="{{.Creator.Name}}">{{.Creator.Name}}</a>
</span>
</div>
<div class="topic_middle">
<div class="topic_middle_inside rowsmall">
<span class="replyCount">{{.PostCount}}&nbsp;{{lang "topic_list.replies_suffix"}}</span>
<span class="likeCount">{{.LikeCount}}&nbsp;{{lang "topic_list.likes_suffix"}}</span>
<span class="viewCount">{{.ViewCount}}&nbsp;{{lang "topic_list.views_suffix"}}</span>
</div>
</div>
<div class="rowitem topic_right passive datarow">
<div class="topic_right_inside">
<a href="{{.LastUser.Link}}"><img src="{{.LastUser.MicroAvatar}}" height=64 alt="{{.LastUser.Name}}'s Avatar" title="{{.LastUser.Name}}'s Avatar" /></a>
<span>
<a href="{{.LastUser.Link}}" class="lastName" title="{{.LastUser.Name}}">{{.LastUser.Name}}</a><br>
<a href="{{.Link}}?page={{.LastPage}}{{if .LastReplyID}}#post-{{.LastReplyID}}{{end}}" class="rowsmall lastReplyAt" title="{{abstime .LastReplyAt}}">{{reltime .LastReplyAt}}</a>
</span>
</div>
</div>
</div>

View File

@ -1,3 +1,6 @@
#main {
max-width: none;
}
.colstack_left { .colstack_left {
width: 200px; width: 200px;
padding-bottom: 6px; padding-bottom: 6px;

View File

@ -209,6 +209,8 @@ li a {
} }
.filter_item a { .filter_item a {
color: #BBBBBB; color: #BBBBBB;
text-overflow: ellipsis;
overflow: hidden;
} }
.colstack_right .colstack_item:not(.colstack_head):not(.rowhead) .rowitem:not(:last-child) { .colstack_right .colstack_item:not(.colstack_head):not(.rowhead) .rowitem:not(:last-child) {
margin-bottom: 8px; margin-bottom: 8px;
@ -512,8 +514,9 @@ h2 {
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
} }
.topic_inner_left .parent_forum { .parent_forum_sep {
display: none; /* Comment this until we figure out how to make it work */ margin-left: 6px;
margin-right: 6px;
} }
.topic_right_inside { .topic_right_inside {
display: flex; display: flex;
@ -1176,6 +1179,9 @@ input[type=checkbox]:checked + label .sel {
.topic_list_title_block .mod_opt a:before { .topic_list_title_block .mod_opt a:before {
content: "{{lang "topic_list.moderate_short" . }}"; content: "{{lang "topic_list.moderate_short" . }}";
} }
.topic_inner_left .parent_forum, .parent_forum_sep {
display: none;
}
} }
@media(max-width: 601px) { @media(max-width: 601px) {
@ -1274,6 +1280,9 @@ input[type=checkbox]:checked + label .sel {
.menu_profile { .menu_profile {
display: none; display: none;
} }
#main {
max-width: calc(100% - 180px);
}
} }
@media(max-width: 850px) { @media(max-width: 850px) {

View File

@ -165,6 +165,8 @@ button, .formbutton, .panel_right_button:not(.has_inner_button), #panel_users .p
border-radius: 5px; border-radius: 5px;
padding-left: 5px; padding-left: 5px;
padding: 12px; padding: 12px;
overflow: hidden;
text-overflow: ellipsis;
} }
#panel_settings.rowlist.bgavatars.micro_grid, .micro_grid { #panel_settings.rowlist.bgavatars.micro_grid, .micro_grid {
grid-gap: 24px; grid-gap: 24px;

View File

@ -1,3 +1,6 @@
#main {
max-width: none;
}
#profile_left_lane { #profile_left_lane {
margin-right: 24px; margin-right: 24px;
} }

View File

@ -168,6 +168,9 @@ li a {
.rowmenu { .rowmenu {
border: 1px solid hsl(0, 0%, 80%); border: 1px solid hsl(0, 0%, 80%);
} }
.rowmenu > div:not(:last-child) {
border-bottom: 1px solid hsl(0, 0%, 80%);
}
.rowsmall { .rowsmall {
font-size: 12px; font-size: 12px;
} }

View File

@ -0,0 +1,9 @@
.sidebar {
display: none;
}
.colline {
border-left: 1px solid hsl(0, 0%, 80%);
padding: 10px;
border-right: 1px solid hsl(0, 0%, 80%);
background-color: white;
}