1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-08-02 10:56:25 -07:00

Start making templates more readable

Also change most remaining underscore var names to camelCase
This commit is contained in:
Eggbertx 2019-12-06 20:03:37 -08:00
parent 6a96420e3e
commit 267a5eeb6f
16 changed files with 446 additions and 465 deletions

View file

@ -21,8 +21,8 @@ func buildFrontPage() string {
var recentPostsArr []interface{}
os.Remove(path.Join(config.DocumentRoot, "index.html"))
front_file, err := os.OpenFile(path.Join(config.DocumentRoot, "index.html"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
defer closeHandle(front_file)
frontFile, err := os.OpenFile(path.Join(config.DocumentRoot, "index.html"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
defer closeHandle(frontFile)
if err != nil {
return handleError(1, "Failed opening front page for writing: "+err.Error()) + "<br />\n"
}
@ -71,7 +71,7 @@ func buildFrontPage() string {
}
}
if err = frontPageTmpl.Execute(front_file, map[string]interface{}{
if err = frontPageTmpl.Execute(frontFile, map[string]interface{}{
"config": config,
"sections": allSections,
"boards": allBoards,
@ -119,27 +119,28 @@ func buildBoardPages(board *Board) (html string) {
if err != nil {
return err.Error()
}
start_time := benchmarkTimer("buildBoard"+strconv.Itoa(board.ID), time.Now(), true)
var current_page_file *os.File
startTime := benchmarkTimer("buildBoard"+strconv.Itoa(board.ID), time.Now(), true)
var currentPageFile *os.File
var threads []interface{}
var thread_pages [][]interface{}
var stickied_threads []interface{}
var nonstickied_threads []interface{}
var threadPages [][]interface{}
var stickiedThreads []interface{}
var nonStickiedThreads []interface{}
var opPosts []Post
// Get all top level posts for the board.
op_posts, err := getPostArr(map[string]interface{}{
if opPosts, err = getPostArr(map[string]interface{}{
"boardid": board.ID,
"parentid": 0,
"deleted_timestamp": nilTimestamp,
}, " ORDER BY bumped DESC")
if err != nil {
}, " ORDER BY bumped DESC"); err != nil {
html += handleError(1, "Error getting OP posts for /%s/: %s", board.Dir, err.Error()) + "<br />\n"
op_posts = nil
opPosts = nil
return
}
// For each top level post, start building a Thread struct
for _, op := range op_posts {
for _, op := range opPosts {
var thread Thread
var postsInThread []Post
@ -201,43 +202,42 @@ func buildBoardPages(board *Board) (html string) {
thread.BoardReplies = reversedPosts
// Count number of images on board page
image_count := 0
imageCount := 0
for _, reply := range postsInThread {
if reply.Filesize != 0 {
image_count++
imageCount++
}
}
// Then calculate number of omitted images.
thread.OmittedImages = thread.NumImages - image_count
thread.OmittedImages = thread.NumImages - imageCount
}
// Add thread struct to appropriate list
if op.Stickied {
stickied_threads = append(stickied_threads, thread)
stickiedThreads = append(stickiedThreads, thread)
} else {
nonstickied_threads = append(nonstickied_threads, thread)
nonStickiedThreads = append(nonStickiedThreads, thread)
}
}
num, _ := deleteMatchingFiles(path.Join(config.DocumentRoot, board.Dir), "\\d.html$")
printf(2, "Number of files deleted: %d\n", num)
// Order the threads, stickied threads first, then nonstickied threads.
threads = append(stickied_threads, nonstickied_threads...)
threads = append(stickiedThreads, nonStickiedThreads...)
// If there are no posts on the board
if len(threads) == 0 {
board.CurrentPage = 1
// Open board.html for writing to the first page.
board_page_file, err := os.OpenFile(path.Join(config.DocumentRoot, board.Dir, "board.html"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
boardPageFile, err := os.OpenFile(path.Join(config.DocumentRoot, board.Dir, "board.html"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
if err != nil {
html += handleError(1,
"Failed opening /%s/board.html: %s", board.Dir, err.Error()) + "<br />"
html += handleError(1, "Failed opening /%s/board.html: %s", board.Dir, err.Error()) + "<br />"
return
}
// Render board page template to the file,
// packaging the board/section list, threads, and board info
if err = boardpageTmpl.Execute(board_page_file, map[string]interface{}{
if err = boardpageTmpl.Execute(boardPageFile, map[string]interface{}{
"config": config,
"boards": allBoards,
"sections": allSections,
@ -249,42 +249,43 @@ func buildBoardPages(board *Board) (html string) {
}
html += "/" + board.Dir + "/ built successfully, no threads to build.\n"
benchmarkTimer("buildBoard"+strconv.Itoa(board.ID), start_time, false)
benchmarkTimer("buildBoard"+strconv.Itoa(board.ID), startTime, false)
return
} else {
}
// Create the archive pages.
thread_pages = paginate(config.ThreadsPerPage, threads)
board.NumPages = len(thread_pages) - 1
threadPages = paginate(config.ThreadsPerPage, threads)
board.NumPages = len(threadPages) - 1
// Create array of page wrapper objects, and open the file.
pagesArr := make([]map[string]interface{}, board.NumPages)
catalog_json_file, err := os.OpenFile(path.Join(config.DocumentRoot, board.Dir, "catalog.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
defer closeHandle(catalog_json_file)
catalogJSONFile, err := os.OpenFile(path.Join(config.DocumentRoot, board.Dir, "catalog.json"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
defer closeHandle(catalogJSONFile)
if err != nil {
html += handleError(1, "Failed opening /"+board.Dir+"/catalog.json: "+err.Error())
return
}
currentBoardPage := board.CurrentPage
for _, page_threads := range thread_pages {
for _, pageThreads := range threadPages {
board.CurrentPage++
var current_page_filepath string
var currentPageFilepath string
pageFilename := strconv.Itoa(board.CurrentPage) + ".html"
current_page_filepath = path.Join(config.DocumentRoot, board.Dir, pageFilename)
current_page_file, err = os.OpenFile(current_page_filepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
defer closeHandle(current_page_file)
currentPageFilepath = path.Join(config.DocumentRoot, board.Dir, pageFilename)
currentPageFile, err = os.OpenFile(currentPageFilepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
defer closeHandle(currentPageFile)
if err != nil {
html += handleError(1, "Failed opening board page: "+err.Error()) + "<br />"
continue
}
// Render the boardpage template, don't forget config
if err = boardpageTmpl.Execute(current_page_file, map[string]interface{}{
if err = boardpageTmpl.Execute(currentPageFile, map[string]interface{}{
"config": config,
"boards": allBoards,
"sections": allSections,
"threads": page_threads,
"threads": pageThreads,
"board": board,
"posts": []interface{}{
Post{BoardID: board.ID},
@ -297,7 +298,7 @@ func buildBoardPages(board *Board) (html string) {
if board.CurrentPage == 1 {
boardPage := path.Join(config.DocumentRoot, board.Dir, "board.html")
os.Remove(boardPage)
if err = syscall.Symlink(current_page_filepath, boardPage); !os.IsExist(err) && err != nil {
if err = syscall.Symlink(currentPageFilepath, boardPage); !os.IsExist(err) && err != nil {
html += handleError(1, "Failed building /"+board.Dir+"/: "+err.Error()) + "<br />"
}
}
@ -305,23 +306,23 @@ func buildBoardPages(board *Board) (html string) {
// Collect up threads for this page.
pageMap := make(map[string]interface{})
pageMap["page"] = board.CurrentPage
pageMap["threads"] = page_threads
pageMap["threads"] = pageThreads
pagesArr = append(pagesArr, pageMap)
}
board.CurrentPage = currentBoardPage
catalog_json, err := json.Marshal(pagesArr)
catalogJSON, err := json.Marshal(pagesArr)
if err != nil {
html += handleError(1, "Failed to marshal to JSON: "+err.Error()) + "<br />"
return
}
if _, err = catalog_json_file.Write(catalog_json); err != nil {
if _, err = catalogJSONFile.Write(catalogJSON); err != nil {
html += handleError(1, "Failed writing /"+board.Dir+"/catalog.json: "+err.Error()) + "<br />"
return
}
html += "/" + board.Dir + "/ built successfully.\n"
}
benchmarkTimer("buildBoard"+strconv.Itoa(board.ID), start_time, false)
benchmarkTimer("buildBoard"+strconv.Itoa(board.ID), startTime, false)
return
}
@ -435,7 +436,7 @@ func buildThreadPages(op *Post) (html string) {
}
var replies []Post
var current_page_file *os.File
var currentPageFile *os.File
var board *Board
if board, err = getBoardFromID(op.BoardID); err != nil {
html += handleError(1, err.Error())
@ -457,20 +458,20 @@ func buildThreadPages(op *Post) (html string) {
repliesInterface = append(repliesInterface, reply)
}
thread_pages := paginate(config.PostsPerThreadPage, repliesInterface)
threadPages := paginate(config.PostsPerThreadPage, repliesInterface)
deleteMatchingFiles(path.Join(config.DocumentRoot, board.Dir, "res"), "^"+strconv.Itoa(op.ID)+"p")
op.NumPages = len(thread_pages)
op.NumPages = len(threadPages)
current_page_filepath := path.Join(config.DocumentRoot, board.Dir, "res", strconv.Itoa(op.ID)+".html")
current_page_file, err = os.OpenFile(current_page_filepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
currentPageFilepath := path.Join(config.DocumentRoot, board.Dir, "res", strconv.Itoa(op.ID)+".html")
currentPageFile, err = os.OpenFile(currentPageFilepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
if err != nil {
html += handleError(1, "Failed opening "+current_page_filepath+": "+err.Error())
html += handleError(1, "Failed opening "+currentPageFilepath+": "+err.Error())
return
}
// render main page
if err = threadpageTmpl.Execute(current_page_file, map[string]interface{}{
if err = threadpageTmpl.Execute(currentPageFile, map[string]interface{}{
"config": config,
"boards": allBoards,
"board": board,
@ -496,9 +497,7 @@ func buildThreadPages(op *Post) (html string) {
threadMap["posts"] = []Post{*op}
// Iterate through each reply, which are of type Post
for _, reply := range replies {
threadMap["posts"] = append(threadMap["posts"], reply)
}
threadMap["posts"] = append(threadMap["posts"], replies...)
threadJSON, err := json.Marshal(threadMap)
if err != nil {
html += handleError(1, "Failed to marshal to JSON: %s", err.Error()) + "<br />"
@ -512,21 +511,21 @@ func buildThreadPages(op *Post) (html string) {
html += fmt.Sprintf("Built /%s/%d successfully", board.Dir, op.ID)
for page_num, page_posts := range thread_pages {
op.CurrentPage = page_num + 1
current_page_filepath := path.Join(config.DocumentRoot, board.Dir, "res", strconv.Itoa(op.ID)+"p"+strconv.Itoa(op.CurrentPage)+".html")
current_page_file, err = os.OpenFile(current_page_filepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
for pageNum, pagePosts := range threadPages {
op.CurrentPage = pageNum + 1
currentPageFilepath := path.Join(config.DocumentRoot, board.Dir, "res", strconv.Itoa(op.ID)+"p"+strconv.Itoa(op.CurrentPage)+".html")
currentPageFile, err = os.OpenFile(currentPageFilepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
if err != nil {
html += handleError(1, "<br />Failed opening "+current_page_filepath+": "+err.Error()) + "<br />\n"
html += handleError(1, "<br />Failed opening "+currentPageFilepath+": "+err.Error()) + "<br />\n"
return
}
if err = threadpageTmpl.Execute(current_page_file, map[string]interface{}{
if err = threadpageTmpl.Execute(currentPageFile, map[string]interface{}{
"config": config,
"boards": allBoards,
"board": board,
"sections": allSections,
"posts": page_posts,
"posts": pagePosts,
"op": op,
}); err != nil {
html += handleError(1, "<br />Failed building /%s/%d: %s", board.Dir, op.ID, err.Error())

View file

@ -37,7 +37,7 @@ func main() {
go tempCleaner()
sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
go func() {
initServer()
}()

View file

@ -54,13 +54,13 @@ func callManageFunction(writer http.ResponseWriter, request *http.Request) {
}
}
if _, ok := manage_functions[action]; ok {
if staffRank >= manage_functions[action].Permissions {
managePageBuffer.Write([]byte(manage_functions[action].Callback(writer, request)))
} else if staffRank == 0 && manage_functions[action].Permissions == 0 {
managePageBuffer.Write([]byte(manage_functions[action].Callback(writer, request)))
if _, ok := manageFunctions[action]; ok {
if staffRank >= manageFunctions[action].Permissions {
managePageBuffer.Write([]byte(manageFunctions[action].Callback(writer, request)))
} else if staffRank == 0 && manageFunctions[action].Permissions == 0 {
managePageBuffer.Write([]byte(manageFunctions[action].Callback(writer, request)))
} else if staffRank == 0 {
managePageBuffer.Write([]byte(manage_functions["login"].Callback(writer, request)))
managePageBuffer.Write([]byte(manageFunctions["login"].Callback(writer, request)))
} else {
managePageBuffer.Write([]byte(action + " is undefined."))
}
@ -71,11 +71,7 @@ func callManageFunction(writer http.ResponseWriter, request *http.Request) {
managePageBuffer.Write([]byte("\n</body>\n</html>"))
}
/* extension := getFileExtension(request.URL.Path)
if extension == "" {
writer.Header().Add("Cache-Control", "max-age=5, must-revalidate")
} */
fmt.Fprintf(writer, managePageBuffer.String())
writer.Write(managePageBuffer.Bytes())
}
func getCurrentStaff(request *http.Request) (string, error) {
@ -148,13 +144,15 @@ func createSession(key string, username string, password string, request *http.R
if err != nil {
handleError(1, customError(err))
return 1
} else {
}
success := bcrypt.CompareHashAndPassword([]byte(staff.PasswordChecksum), []byte(password))
if success == bcrypt.ErrMismatchedHashAndPassword {
// password mismatch
modLog.Print("Failed login (password mismatch) from " + request.RemoteAddr + " at " + getSQLDateTime())
return 1
} else {
}
// successful login, add cookie that expires in one month
http.SetCookie(writer, &http.Cookie{
Name: "sessiondata",
@ -177,11 +175,9 @@ func createSession(key string, username string, password string, request *http.R
handleError(1, customError(err))
}
return 0
}
}
}
var manage_functions = map[string]ManageFunction{
var manageFunctions = map[string]ManageFunction{
"cleanup": {
Permissions: 3,
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
@ -433,7 +429,7 @@ var manage_functions = map[string]ManageFunction{
html += manageConfigBuffer.String()
return
}},
"purgeeverything": {
/*"purgeeverything": {
Permissions: 3,
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
html = "<img src=\"/css/purge.jpg\" />"
@ -494,7 +490,7 @@ var manage_functions = map[string]ManageFunction{
buildBoards() + "<hr />\n" +
buildFrontPage()
return
}},
}},*/
"executesql": {
Permissions: 3,
Callback: func(writer http.ResponseWriter, request *http.Request) (html string) {
@ -837,7 +833,6 @@ var manage_functions = map[string]ManageFunction{
println(2, "Boards rebuilt successfully")
done = true
}
break
case do == "del":
// resetBoardSectionArrays()
case do == "edit":
@ -877,7 +872,11 @@ var manage_functions = map[string]ManageFunction{
manageBoardsBuffer := bytes.NewBufferString("")
allSections, _ = getSectionArr("")
if len(allSections) == 0 {
execSQL("INSERT INTO " + config.DBprefix + "sections (hidden,name,abbreviation) VALUES(0,'Main','main')")
if _, err = execSQL(
"INSERT INTO " + config.DBprefix + "sections (hidden,name,abbreviation) VALUES(0,'Main','main')",
); err != nil {
html += handleError(1, err.Error())
}
}
allSections, _ = getSectionArr("")

View file

@ -36,7 +36,6 @@ var (
allBoards []Board
tempPosts []Post
tempCleanerTicker *time.Ticker
tempCleanerQuit = make(chan struct{})
)
// bumps the given thread on the given board and returns true if there were no errors
@ -130,7 +129,7 @@ func sinceLastPost(post *Post) int {
return int(time.Since(lastPostTime).Seconds())
}
func createImageThumbnail(image_obj image.Image, size string) image.Image {
func createImageThumbnail(imageObj image.Image, size string) image.Image {
var thumbWidth int
var thumbHeight int
@ -145,14 +144,14 @@ func createImageThumbnail(image_obj image.Image, size string) image.Image {
thumbWidth = config.ThumbWidth_catalog
thumbHeight = config.ThumbHeight_catalog
}
old_rect := image_obj.Bounds()
if thumbWidth >= old_rect.Max.X && thumbHeight >= old_rect.Max.Y {
return image_obj
oldRect := imageObj.Bounds()
if thumbWidth >= oldRect.Max.X && thumbHeight >= oldRect.Max.Y {
return imageObj
}
thumbW, thumbH := getThumbnailSize(old_rect.Max.X, old_rect.Max.Y, size)
image_obj = imaging.Resize(image_obj, thumbW, thumbH, imaging.CatmullRom) // resize to 600x400 px using CatmullRom cubic filter
return image_obj
thumbW, thumbH := getThumbnailSize(oldRect.Max.X, oldRect.Max.Y, size)
imageObj = imaging.Resize(imageObj, thumbW, thumbH, imaging.CatmullRom) // resize to 600x400 px using CatmullRom cubic filter
return imageObj
}
func createVideoThumbnail(video, thumb string, size int) error {
@ -588,10 +587,10 @@ func makePost(writer http.ResponseWriter, request *http.Request) {
if err = banpageTmpl.Execute(&banpageBuffer, map[string]interface{}{
"config": config, "ban": banStatus, "banBoards": boards[post.BoardID-1].Dir,
}); err != nil {
fmt.Fprintf(writer, handleError(1, err.Error()))
writer.Write([]byte(handleError(1, err.Error())))
return
}
fmt.Fprintf(writer, banpageBuffer.String())
writer.Write(banpageBuffer.Bytes())
return
}
@ -758,5 +757,5 @@ func banHandler(writer http.ResponseWriter, request *http.Request) {
fmt.Fprintf(writer, handleError(1, err.Error())+"\n</body>\n</html>")
return
}
fmt.Fprintf(writer, banpageBuffer.String())
writer.Write(banpageBuffer.Bytes())
}

View file

@ -35,7 +35,8 @@ func (s GochanServer) serveFile(writer http.ResponseWriter, request *http.Reques
// the requested path isn't a file or directory, 404
serveNotFound(writer, request)
return
} else {
}
//the file exists, or there is a folder here
if results.IsDir() {
//check to see if one of the specified index pages exists
@ -82,12 +83,12 @@ func (s GochanServer) serveFile(writer http.ResponseWriter, request *http.Reques
}
accessLog.Print("Success: 200 from " + getRealIP(request) + " @ " + request.URL.Path)
}
}
// serve the index page
writer.Header().Add("Cache-Control", "max-age=5, must-revalidate")
fileBytes, _ = ioutil.ReadFile(filePath)
writer.Header().Add("Cache-Control", "max-age=86400")
_, _ = writer.Write(fileBytes)
writer.Write(fileBytes)
}
func serveNotFound(writer http.ResponseWriter, request *http.Request) {

View file

@ -52,7 +52,6 @@ func connectToSQLServer() {
os.Exit(2)
}
nullTime, _ = time.Parse("2006-01-02 15:04:05", nilTimestamp)
if db, err = sql.Open(config.DBtype, connStr); err != nil {
handleError(0, "Failed to connect to the database: %s\n", customError(err))
os.Exit(2)
@ -69,8 +68,11 @@ func connectToSQLServer() {
}
var sqlVersionStr string
err = queryRowSQL("SELECT value FROM "+config.DBprefix+"info WHERE name = 'version'",
[]interface{}{}, []interface{}{&sqlVersionStr})
if err = queryRowSQL("SELECT value FROM "+config.DBprefix+"info WHERE name = 'version'",
[]interface{}{}, []interface{}{&sqlVersionStr}); err != nil {
handleError(0, "failed: %s\n", customError(err))
os.Exit(2)
}
var numBoards, numStaff int
rows, err := querySQL("SELECT COUNT(*) FROM " + config.DBprefix + "boards UNION ALL SELECT COUNT(*) FROM " + config.DBprefix + "staff")
if err != nil {
@ -106,7 +108,11 @@ func connectToSQLServer() {
buildFrontPage()
buildBoardListJSON()
buildBoards()
_, err = execSQL("INSERT INTO "+config.DBprefix+"info (name,value) VALUES('version',?)", versionStr)
if _, err = execSQL(
"INSERT INTO "+config.DBprefix+"info (name,value) VALUES('version',?)",
versionStr); err != nil {
handleError(0, "failed: %s\n", err.Error())
}
return
} else if err != nil {
handleError(0, "failed: %s\n", customError(err))
@ -146,7 +152,6 @@ func initDB(initFile string) error {
if statement != "" && statement != " " {
if _, err := db.Exec(statement + ";"); err != nil {
panic("Error with SQL statement:" + statement)
return err
}
}
}
@ -184,7 +189,6 @@ func prepareSQL(query string) (*sql.Stmt, error) {
fallthrough
case "sqlite3":
preparedStr = query
break
case "postgres":
arr := strings.Split(query, "?")
for i := range arr {
@ -194,7 +198,6 @@ func prepareSQL(query string) (*sql.Stmt, error) {
arr[i] += fmt.Sprintf("$%d", i+1)
}
preparedStr = strings.Join(arr, "")
break
}
stmt, err := db.Prepare(preparedStr)
return stmt, sqlVersionErr(err)

View file

@ -4,6 +4,7 @@ import (
"fmt"
"html"
"os"
"path"
"reflect"
"strconv"
"strings"
@ -61,10 +62,10 @@ var funcMap = template.FuncMap{
"escapeString": func(a string) string {
return html.EscapeString(a)
},
"formatFilesize": func(size_int int) string {
size := float32(size_int)
"formatFilesize": func(sizeInt int) string {
size := float32(sizeInt)
if size < 1000 {
return fmt.Sprintf("%d B", size_int)
return fmt.Sprintf("%d B", sizeInt)
} else if size <= 100000 {
return fmt.Sprintf("%0.1f KB", size/1024)
} else if size <= 100000000 {
@ -88,12 +89,12 @@ var funcMap = template.FuncMap{
}
return appended
},
"truncateMessage": func(msg string, limit int, max_lines int) string {
"truncateMessage": func(msg string, limit int, maxLines int) string {
var truncated bool
split := strings.SplitN(msg, "<br />", -1)
if len(split) > max_lines {
split = split[:max_lines]
if len(split) > maxLines {
split = split[:maxLines]
msg = strings.Join(split, "<br />")
truncated = true
}
@ -143,8 +144,8 @@ var funcMap = template.FuncMap{
"getCatalogThumbnail": func(img string) string {
return getThumbnailPath("catalog", img)
},
"getThreadID": func(post_i interface{}) (thread int) {
post, ok := post_i.(Post)
"getThreadID": func(postInterface interface{}) (thread int) {
post, ok := postInterface.(Post)
if !ok {
thread = 0
} else if post.ParentID == 0 {
@ -154,20 +155,20 @@ var funcMap = template.FuncMap{
}
return
},
"getPostURL": func(post_i interface{}, typeOf string, withDomain bool) (postURL string) {
"getPostURL": func(postInterface interface{}, typeOf string, withDomain bool) (postURL string) {
if withDomain {
postURL = config.SiteDomain
}
postURL += config.SiteWebfolder
if typeOf == "recent" {
post, ok := post_i.(*RecentPost)
post, ok := postInterface.(*RecentPost)
if !ok {
return
}
postURL = post.GetURL(withDomain)
} else {
post, ok := post_i.(*Post)
post, ok := postInterface.(*Post)
if !ok {
return
}
@ -297,7 +298,6 @@ var (
frontPageTmpl *template.Template
boardpageTmpl *template.Template
threadpageTmpl *template.Template
postFormTmpl *template.Template
postEditTmpl *template.Template
manageBansTmpl *template.Template
manageBoardsTmpl *template.Template
@ -310,10 +310,12 @@ func loadTemplate(files ...string) (*template.Template, error) {
var templates []string
for i, file := range files {
templates = append(templates, file)
if _, err := os.Stat(config.TemplateDir + "/override/" + file); !os.IsNotExist(err) {
files[i] = config.TemplateDir + "/override/" + files[i]
tmplPath := path.Join(config.TemplateDir, "override", file)
if _, err := os.Stat(tmplPath); !os.IsNotExist(err) {
files[i] = tmplPath
} else {
files[i] = config.TemplateDir + "/" + files[i]
files[i] = path.Join(config.TemplateDir, file)
}
}

View file

@ -85,9 +85,7 @@ type BanAppeal struct {
func (a *BanAppeal) GetBan() (BanInfo, error) {
var ban BanInfo
var err error
err = queryRowSQL("SELECT * FROM "+config.DBprefix+"banlist WHERE id = ? LIMIT 1",
err := queryRowSQL("SELECT * FROM "+config.DBprefix+"banlist WHERE id = ? LIMIT 1",
[]interface{}{a.ID}, []interface{}{
&ban.ID, &ban.AllowRead, &ban.IP, &ban.Name, &ban.NameIsRegex, &ban.SilentBan,
&ban.Boards, &ban.Staff, &ban.Timestamp, &ban.Expires, &ban.Permaban, &ban.Reason,

View file

@ -25,7 +25,6 @@ import (
)
var (
nullTime time.Time
errEmptyDurationString = errors.New("Empty Duration string")
errInvalidDurationString = errors.New("Invalid Duration string")
durationRegexp = regexp.MustCompile(`^((\d+)\s?ye?a?r?s?)?\s?((\d+)\s?mon?t?h?s?)?\s?((\d+)\s?we?e?k?s?)?\s?((\d+)\s?da?y?s?)?\s?((\d+)\s?ho?u?r?s?)?\s?((\d+)\s?mi?n?u?t?e?s?)?\s?((\d+)\s?s?e?c?o?n?d?s?)?$`)
@ -409,9 +408,7 @@ func resetBoardSectionArrays() {
allSections = nil
allBoardsArr, _ := getBoardArr(nil, "")
for _, b := range allBoardsArr {
allBoards = append(allBoards, b)
}
allBoards = append(allBoards, allBoardsArr...)
allSectionsArr, _ := getSectionArr("")
allSections = append(allSections, allSectionsArr...)
@ -426,20 +423,6 @@ func searchStrings(item string, arr []string, permissive bool) int {
return -1
}
func bToI(b bool) int {
if b {
return 1
}
return 0
}
func bToA(b bool) string {
if b {
return "1"
}
return "0"
}
// Checks the validity of the Akismet API key given in the config file.
func checkAkismetAPIKey(key string) error {
if key == "" {
@ -536,11 +519,6 @@ func marshalJSON(tag string, data interface{}, indent bool) (string, error) {
return string(jsonBytes), err
}
func jsonError(err string) string {
errJSON, _ := marshalJSON("error", err, false)
return errJSON
}
func limitArraySize(arr []string, maxSize int) []string {
if maxSize > len(arr)-1 || maxSize < 0 {
return arr
@ -559,26 +537,6 @@ func numReplies(boardid, threadid int) int {
return num
}
func ipMatch(newIP, existingIP string) bool {
if newIP == existingIP {
// both are single IPs and are the same
return true
}
wildcardIndex := strings.Index(existingIP, "*")
if wildcardIndex < 0 {
// single (or invalid) and they don't match
return false
}
ipRegexStr := existingIP[0:wildcardIndex]
ipRegexStr = strings.Replace(ipRegexStr, ".", "\\.", -1) + ".*"
ipRegex, err := regexp.Compile(ipRegexStr)
if err != nil {
// this shouldn't happen unless you enter an invalid IP in the db
return false
}
return ipRegex.MatchString(newIP)
}
// based on TinyBoard's parse_time function
func parseDurationString(str string) (time.Duration, error) {
if str == "" {

View file

@ -1,22 +1,20 @@
{{template "page_header.html" .}}
<header>
<header>
<h1 id="board-title">/{{$.board.Dir}}/ - {{$.board.Title}}</h1>
<span id="board-subtitle">{{$.board.Subtitle}}</span>
</header>
<hr />
<div id="right-sidelinks">
</header><hr />
<div id="right-sidelinks">
<a href="{{.config.SiteWebfolder}}{{.board.Dir}}/catalog.html">Board catalog</a><br />
</div>
{{template "postbox.html" .}}
<hr />
<div id="content">
</div>
{{- template "postbox.html" .}}
<hr />
<div id="content">
<form action="/util" method="POST" id="main-form">
{{range $t, $thread := .threads}}
{{$op := $thread.OP}}
{{- range $t, $thread := .threads}}{{$op := $thread.OP}}
<div class="thread">
<div class="op-post" id="op{{$op.ID}}">
{{if ne $op.Filename ""}}
{{if ne $op.Filename "deleted"}}
{{- if ne $op.Filename "" -}}
{{- if ne $op.Filename "deleted"}}
<div class="file-info">File: <a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/src/{{$op.Filename}}" target="_blank">{{$op.Filename}}</a> - ({{formatFilesize $op.Filesize}} , {{$op.ImageW}}x{{$op.ImageH}}, {{$op.FilenameOriginal}})</div>
<a class="upload-container" href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/src/{{$op.Filename}}"><img src="{{$.config.SiteWebfolder}}{{$.board.Dir}}/thumb/{{getThreadThumbnail $op.Filename}}" alt="{{$.config.SiteWebfolder}}{{$.board.Dir}}/src/{{$op.Filename}}" width="{{$op.ThumbW}}" height="{{$op.ThumbH}}" class="upload" /></a>
{{else}}
@ -25,27 +23,29 @@
{{end}}
<input type="checkbox" id="check{{$op.ID}}" name="check{{$op.ID}}" /><label class="post-info" for="check{{$op.ID}}"> <span class="subject">{{$op.Subject}}</span> <span class="postername">{{if ne $op.Email ""}}<a href="mailto:{{$op.Email}}">{{end}}{{if ne $op.Name ""}}{{$op.Name}}{{else}}{{if eq $op.Tripcode ""}}{{$.board.Anonymous}}{{end}}{{end}}{{if ne $op.Email ""}}</a>{{end}}</span>{{if ne $op.Tripcode ""}}<span class="tripcode">!{{$op.Tripcode}}</span>{{end}} {{formatTimestamp $op.Timestamp}} </label><a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/res/{{$op.ID}}.html#{{$op.ID}}">No.</a> <a href="javascript:quote({{$op.ID}})" class="backlink-click">{{$op.ID}}</a> <span class="post-links"> <span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span> <span>[<a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/res/{{$op.ID}}.html">View</a>]</span></span><br />
<div class="post-text">{{truncateMessage $op.MessageHTML 2222 18}}</div>
{{if gt $thread.NumReplies 3}}
{{- if gt $thread.NumReplies 3}}
<b>{{subtract $thread.NumReplies 3}} post{{if gt $thread.NumReplies 4}}s{{end}} omitted</b>
{{end}}
</div>
{{range $reply_num,$reply := $thread.BoardReplies}}
{{- range $reply_num,$reply := $thread.BoardReplies}}
<div class="reply-container" id="replycontainer{{$reply.ID}}">
<a class="anchor" id="{{$reply.ID}}"></a>
<div class="reply" id="reply{{$reply.ID}}">
<input type="checkbox" id="check{{$reply.ID}}" name="check{{$reply.ID}}" /> <label class="post-info" for="check{{$reply.ID}}"> <span class="subject">{{$reply.Subject}}</span> <span class="postername">{{if ne $reply.Email ""}}<a href="mailto:{{$reply.Email}}">{{end}}{{if ne $reply.Name ""}}{{$reply.Name}}{{else}}{{if eq $reply.Tripcode ""}}{{$.board.Anonymous}}{{end}}{{end}}{{if ne $reply.Email ""}}</a>{{end}}</span>{{if ne $reply.Tripcode ""}}<span class="tripcode">!{{$reply.Tripcode}}</span>{{end}} {{formatTimestamp $reply.Timestamp}} </label><a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/res/{{$op.ID}}.html#{{$reply.ID}}">No.</a> <a href="javascript:quote({{$reply.ID}})" class="backlink-click">{{$reply.ID}}</a> <span class="post-links"><span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span></span><br />
{{if ne $reply.Filename ""}}
{{if ne $reply.Filename "deleted"}}
{{if ne $reply.Filename "deleted" -}}
<span class="file-info">File: <a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/src/{{$reply.Filename}}" target="_blank">{{$reply.Filename}}</a> - ({{formatFilesize $reply.Filesize}} , {{$reply.ImageW}}x{{$reply.ImageH}}, {{$reply.FilenameOriginal}})</span><br />
<a class="upload-container" href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/src/{{$reply.Filename}}"><img src="{{$.config.SiteWebfolder}}{{$.board.Dir}}/thumb/{{getThreadThumbnail $reply.Filename}}" alt="{{$.config.SiteWebfolder}}{{$.board.Dir}}/src/{{$reply.Filename}}" width="{{$reply.ThumbW}}" height="{{$reply.ThumbH}}" class="upload" /></a>
{{else}}
<div class="file-deleted-box" style="text-align:center;">File removed</div>
{{end}}{{end}}
{{end}}
{{end -}}
<div class="post-text">{{$reply.MessageHTML}}</div>
</div>
</div>{{end}}
</div>
<hr />{{end}}
{{end -}}
</div><hr />
{{- end}}
<div id="right-bottom-content">
<div id="report-delbox">
<input type="hidden" name="board" value="{{.board.Dir}}" />
@ -58,7 +58,8 @@
</form>
<div id="left-bottom-content">
<table id="pages">
<tr><td>{{if gt .board.CurrentPage 2}}
<tr>
<td>{{if gt .board.CurrentPage 2}}
<form method="GET" action="{{.config.SiteWebfolder}}{{.board.Dir}}/{{subtract .board.CurrentPage 1}}.html">
<input type="submit" value="Previous" />
</form>
@ -72,11 +73,13 @@
<form method="GET" action="{{.config.SiteWebfolder}}{{.board.Dir}}/{{add .board.CurrentPage 1}}.html">
<input type="submit" value="Next" />
</form>
{{else}}Next{{end}}</td></tr>
{{else}}Next{{end}}
</td>
</tr>
</table>
<span id="boardmenu-bottom">
[{{range $i, $boardlink := $.boards}}{{if gt $i 0}}/{{end}} <a href="{{$.config.SiteWebfolder}}{{$boardlink.Dir}}/">{{$boardlink.Dir}}</a> {{end}}]
</span>
</div>
</div>
{{template "page_footer.html" .}}
</div>
{{template "page_footer.html" .}}

View file

@ -2,9 +2,11 @@
This will be used for storing configuration-dependent JS variables,
instead of loading them on every HTML page.
*/ -}}
var styles = [{{range $ii, $style := .Styles -}}
{{if gt $ii 0}}, {{end -}}
var styles = [
{{- range $ii, $style := .Styles -}}
{{if gt $ii 0}},{{end -}}
{Name: "{{js $style.Name}}", Filename: "{{js $style.Filename}}"}
{{- end}}];
{{- end -}}
];
var defaultStyle = "{{.DefaultStyle}}";
var webroot = "{{.SiteWebfolder}}";

View file

@ -1,4 +1,4 @@
{{template "page_header.html" .}}
{{- template "page_header.html" .}}
<div id="top-pane">
<span id="site-title">{{.config.SiteName}}</span><br />
<span id="site-slogan">{{.config.SiteSlogan}}</span>
@ -12,26 +12,37 @@
<div class="section-block">
<div class="section-title-block"><b>Boards</b></div>
<div class="section-body">
{{range $_, $section := .sections}}{{if not $section.Hidden}}<ul style="float:left; list-style: none">
{{- range $_, $section := .sections -}}
{{if not $section.Hidden}}
<ul style="float:left; list-style: none">
<li style="text-align: center; font-weight: bold"><b><u>{{$section.Name}}</u></b></li>
{{range $_, $board := $.boards}}{{if and (eq $board.Section $section.ID) (ne $board.Dir $.config.Modboard)}}
{{range $_, $board := $.boards}}
{{if and (eq $board.Section $section.ID) (ne $board.Dir $.config.Modboard)}}
<li><a href="{{$.config.SiteWebfolder}}{{$board.Dir}}/" title="{{$board.Description}}">/{{$board.Dir}}/</a> — {{$board.Title}}</li>
{{end}}{{end}}
</ul>{{end}}{{end}}
{{end}}
{{end}}
</ul>
{{end}}
{{end}}
</div>
</div>
{{if gt .config.MaxRecentPosts 0}}
{{- if gt .config.MaxRecentPosts 0}}
<div class="section-block">
<div class="section-title-block"><b>Recent Posts</b></div>
<div class="section-body">
<div id="recent-posts">
{{range $i, $post := $.recent_posts}}{{$postURL := getPostURL $post "recent" false}}
{{- range $i, $post := $.recent_posts}}{{$postURL := getPostURL $post "recent" false}}
<div class="recent-post">
{{if and (ne $post.Filename "deleted") (ne $post.Filename "")}}<a href="{{$postURL}}" class="front-reply" target="_blank"><img src="{{$.config.SiteWebfolder}}{{$post.BoardName}}/thumb/{{getThreadThumbnail $post.Filename}}" alt="post thumbnail"/></a><br />
{{else}}<div class="file-deleted-box" style="text-align:center; float:none;"><a href="{{$postURL}}" class="front-reply" target="_blank">No file</a></div>{{end}}<br />
<a href="{{$.config.SiteWebfolder}}{{$post.BoardName}}/">/{{$post.BoardName}}/</a>
<hr />
{{truncateMessage (stripHTML $post.Message) 40 4}}</div>{{end}}</div>
</div>{{end}}</div>
{{if and (ne $post.Filename "deleted") (ne $post.Filename "") -}}
<a href="{{$postURL}}" class="front-reply" target="_blank"><img src="{{$.config.SiteWebfolder}}{{$post.BoardName}}/thumb/{{getThreadThumbnail $post.Filename}}" alt="post thumbnail"/></a><br />
{{else}}
<div class="file-deleted-box" style="text-align:center; float:none;"><a href="{{$postURL}}" class="front-reply" target="_blank">No file</a></div>
{{- end}}<br />
<a href="{{$.config.SiteWebfolder}}{{$post.BoardName}}/">/{{$post.BoardName}}/</a><hr />
{{truncateMessage (stripHTML $post.Message) 40 4}}
</div>{{end}}
</div>
</div>{{end}}
</div>
</div>
{{template "page_footer.html" .}}

View file

@ -1,6 +1,6 @@
<div id="footer">
<a href="{{$.config.SiteWebfolder}}">Home</a> | <a href="{{$.config.SiteWebfolder}}#boards">Boards</a> | <a href="{{$.config.SiteWebfolder}}#rules">Rules</a> | <a href="{{$.config.SiteWebfolder}}#faq">FAQ</a><br />
Powered by <a href="http://github.com/eggbertx/gochan/">Gochan {{version}}</a><br />
</div>
</div>
</body>
</html>

View file

@ -3,13 +3,13 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{{with .board}}
{{with $.op}}
{{if ne $.op.Subject ""}}<title>/{{$.board.Dir}}/ - {{truncateString $.op.Subject 20 true}}</title>
{{else if ne $.op.MessageHTML ""}}<title>/{{$.board.Dir}}/ - {{truncateString $.op.MessageText 20 true}}</title>
{{else}}<title>/{{$.board.Dir}}/ - #{{$.op.ID}}</title>{{end}}
{{else}}<title>/{{$.board.Dir}}/ - {{$.board.Title}}</title>{{end}}
{{else}}<title>{{.config.SiteName}}</title>{{end}}
{{with .board -}}
{{with $.op -}}
{{if ne $.op.Subject "" -}}<title>/{{$.board.Dir}}/ - {{truncateString $.op.Subject 20 true}}</title>
{{- else if ne $.op.MessageHTML "" -}}<title>/{{$.board.Dir}}/ - {{truncateString $.op.MessageText 20 true}}</title>
{{- else}}<title>/{{$.board.Dir}}/ - #{{$.op.ID}}</title>{{end}}
{{- else}}<title>/{{$.board.Dir}}/ - {{$.board.Title}}</title>{{end}}
{{- else}}<title>{{.config.SiteName}}</title>{{end}}
<link rel="stylesheet" href="{{.config.SiteWebfolder}}css/global.css" />
<link id="theme" rel="stylesheet" href="{{.config.SiteWebfolder}}css/{{.config.DefaultStyle}}" />
<link rel="shortcut icon" href="{{.config.SiteWebfolder}}favicon.png">
@ -19,6 +19,6 @@
<script type="text/javascript" src="{{$.config.SiteWebfolder}}javascript/manage.js"></script>
</head>
<body>
<div id="topbar">
<div id="topbar">
{{range $i, $board := .boards}}<a href="{{$.config.SiteWebfolder}}{{$board.Dir}}/" class="topbar-item">/{{$board.Dir}}/</a>{{end}}
</div>
</div>

View file

@ -1,10 +1,13 @@
{{define "postbox.html"}}
<div id="postbox-area">
<form id="postform" name="postform" action="/post" method="POST" enctype="multipart/form-data">
{{with .op}}<input type="hidden" name="threadid" value="{{$.op.ID}}" />
{{- with .op}}
<input type="hidden" name="threadid" value="{{$.op.ID}}" />
<input type="hidden" name="boardid" value="{{$.board.ID}}" />
{{else}}<input type="hidden" name="threadid" value="0" />
<input type="hidden" name="boardid" value="{{$.board.ID}}" />{{end}}
{{- else -}}
<input type="hidden" name="threadid" value="0" />
<input type="hidden" name="boardid" value="{{$.board.ID}}" />
{{- end}}
<table id="postbox-static">
<tr><th class="postblock">Name</th><td><input type="text" name="postname" maxlength="100" size="28" /></td></tr>
<tr><th class="postblock">Email</th><td><input type="text" name="postemail" maxlength="100" size="28" /></td></tr>
@ -13,8 +16,7 @@
<input type="submit" value="{{with .op}}Reply{{else}}Post{{end}}"/></td></tr>
<tr><th class="postblock">Message</th><td><textarea rows="4" cols="48" name="postmsg" id="postmsg"></textarea></td></tr>
<tr><th class="postblock">File</th><td><input name="imagefile" type="file"><input type="checkbox" id="spoiler" name="spoiler"/><label for="spoiler">Spoiler</label></td></tr>
<input type="password" name="dummy2" style="display:none"/>
<tr><th class="postblock">Password</th><td><input type="password" id="postpassword" name="postpassword" size="14" /> (for post/file deletion)</td></tr>
</table>
</table><input type="password" name="dummy2" style="display:none"/>
</form>
</div>{{end}}

View file

@ -2,8 +2,7 @@
<header>
<h1 id="board-title">/{{$.board.Dir}}/ - {{$.board.Title}}</h1>
<span id="board-subtitle">{{$.board.Subtitle}}</span>
</header>
<hr />
</header><hr />
<div id="threadlinks-top">
<a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/board.html" >Return</a><br />
<select id="changepage" onchange="changePage(this)">
@ -17,23 +16,23 @@
<div id="right-sidelinks">
<a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/catalog.html">Board catalog</a><br />
</div>
{{template "postbox.html" .}}
<hr />
{{template "postbox.html" .}}<hr />
<div id="content">
<form action="/util" method="POST" id="main-form">
<div class="thread" id="{{$.op.ID}}">
<div class="op-post" id="op{{.op.ID}}">
{{if ne $.op.Filename ""}}
{{if ne $.op.Filename "deleted"}}
{{- if ne $.op.Filename "deleted" -}}
<div class="file-info">File: <a href="../src/{{.op.Filename}}" target="_blank">{{$.op.Filename}}</a> - ({{formatFilesize $.op.Filesize}} , {{$.op.ImageW}}x{{$.op.ImageH}}, {{$.op.FilenameOriginal}})</div>
<a class="upload-container" href="{{.config.SiteWebfolder}}{{.board.Dir}}/src/{{.op.Filename}}"><img src="{{.config.SiteWebfolder}}{{.board.Dir}}/thumb/{{getThreadThumbnail .op.Filename}}" alt="{{$.config.SiteWebfolder}}{{$.board.Dir}}/src/{{.op.Filename}}" width="{{.op.ThumbW}}" height="{{.op.ThumbH}}" class="upload" /></a>
{{else}}
{{- else}}
<div class="file-deleted-box" style="text-align:center;">File removed</div>
{{end}}{{end}}
{{end}}
{{end -}}
<input type="checkbox" id="check{{.op.ID}}" name="check{{.op.ID}}" /><label class="post-info" for="check{{.op.ID}}"> <span class="subject">{{.op.Subject}}</span> <span class="postername">{{if ne .op.Email ""}}<a href="mailto:{{.op.Email}}">{{end}}{{if ne .op.Name ""}}{{.op.Name}}{{else}}{{if eq .op.Tripcode ""}}{{.board.Anonymous}}{{end}}{{end}}{{if ne .op.Email ""}}</a>{{end}}</span>{{if ne .op.Tripcode ""}}<span class="tripcode">!{{.op.Tripcode}}</span>{{end}} {{formatTimestamp .op.Timestamp}} </label><a href="{{$.config.SiteWebfolder}}{{.board.Dir}}/res/{{.op.ID}}.html#{{.op.ID}}">No.</a> <a href="javascript:quote({{.op.ID}})" class="backlink-click">{{.op.ID}}</a> <span class="post-links"> <span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span></span><br />
<div class="post-text">{{.op.MessageHTML}}</div>
</div>
{{range $reply_num,$reply := .posts}}
{{range $reply_num,$reply := .posts -}}
<div class="reply-container" id="replycontainer{{$reply.ID}}">
<a class="anchor" id="{{$reply.ID}}"></a>
<div class="reply" id="reply{{$reply.ID}}">
@ -47,9 +46,9 @@
{{end}}{{end}}
<div class="post-text">{{$reply.MessageHTML}}</div>
</div>
</div>{{end}}
</div>
<hr />
{{end}}
</div><hr />
<div id="right-bottom-content">
<div id="report-delbox">
<input type="hidden" name="board" value="{{.board.Dir}}" />
@ -64,15 +63,18 @@
<table id="pages">
<tr>
<td><a href="{{.config.SiteWebfolder}}{{.board.Dir}}/">Return</a></td>
<td>{{if gt .op.CurrentPage 1}}
<td>{{- if gt .op.CurrentPage 1 -}}
<form method="GET" action="{{.config.SiteWebfolder}}{{.board.Dir}}/res/{{.op.ID}}p{{subtract .op.CurrentPage 1}}.html">
<input type="submit" value="Previous" />
</form>
{{else}}Previous{{end}}
{{- else}}Previous{{end -}}
</td>
<td>[<a href="{{.config.SiteWebfolder}}{{.board.Dir}}/res/{{.op.ID}}.html">All</a>]
{{range $i,$_ := makeLoop .op.NumPages 0}}
[{{if eq $i (subtract $.op.CurrentPage 1)}}<b>{{end}}<a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/res/{{$.op.ID}}p{{add $i 1}}.html">{{add $i 1}}</a>{{if eq $i (subtract $.op.CurrentPage 1)}}</b>{{end}}]{{end}}</td>
{{- range $i,$_ := makeLoop .op.NumPages 0 -}}
[{{if eq $i (subtract $.op.CurrentPage 1)}}<b>{{end}}
<a href="{{$.config.SiteWebfolder}}{{$.board.Dir}}/res/{{$.op.ID}}p{{add $i 1}}.html">{{add $i 1}}</a>
{{if eq $i (subtract $.op.CurrentPage 1)}}</b>{{end}}]
{{end}}</td>
<td>{{if lt .op.CurrentPage .op.NumPages}}
<form method="GET" action="{{.config.SiteWebfolder}}{{.board.Dir}}/res/{{.op.ID}}p{{add .op.CurrentPage 1}}.html">
<input type="submit" value="Next" />
@ -80,7 +82,9 @@
{{else}}Next{{end}}</td></tr>
</table>
<span id="boardmenu-bottom">
[{{range $i, $boardlink := .boards}} {{if gt $i 0}}/{{end}} <a href="/{{$boardlink.Dir}}/">{{$boardlink.Dir}}</a> {{end}}]
[{{range $i, $boardlink := .boards -}}
{{if gt $i 0}}/{{end -}} <a href="/{{$boardlink.Dir}}/">{{$boardlink.Dir}}</a>
{{- end}}]
</span>
</div>
</div>