some tests and major cleanup

This commit is contained in:
ston1th 2018-10-19 23:08:32 +02:00
commit 16671b3406
30 changed files with 192057 additions and 202431 deletions

View file

@ -10,13 +10,19 @@ all: $(PROGRAM)
setup:
$(CC) get github.com/client9/misspell
vendor: clean codeqa
CGO_ENABLED=0 $(CC) $(BUILD) -mod=vendor $(LDFLAGS)
$(PROGRAM): clean codeqa
CGO_ENABLED=0 $(CC) $(BUILD) $(LDFLAGS)
clean:
$(CC) clean -x
codeqa: templates gofmt misspell golint
codeqa: generate gofmt misspell golint test
generate:
$(CC) generate
gofmt:
gofmt -w .
@ -25,9 +31,14 @@ golint:
$(GOPATH)/bin/golint .
misspell:
# ignore misspellings from vendor/
$(GOPATH)/bin/misspell -i functionallity,avaliable,HALP,halp,agains,becuase,Christiaan,compatiblity,conjuction,frequence,inital,inot,occurance,overriden,pervious,preceeded,preceeds,precendence,PROPOGATE,reamining,represenation,soley,substract,supression,whcih,preceed *
templates:
$(CC) generate
test:
ifeq ($(shell go env GOARCH), $(shell go env GOHOSTARCH))
$(CC) test ./...
else
$(info skipping tests of other platforms)
endif
.PHONY: setup build clean templates gofmt misspell golint
.PHONY: setup vendor build clean codeqa generate gofmt golint misspell test

1
go.mod
View file

@ -42,4 +42,5 @@ require (
github.com/urfave/cli v1.19.1
github.com/willf/bitset v1.1.9 // indirect
golang.org/x/crypto v0.0.0-20161031180806-9477e0b78b9a
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e
)

50
pkg/cache/cache_test.go vendored Normal file
View file

@ -0,0 +1,50 @@
// Copyright (C) 2018 Marius Schellenberger
package cache
import (
"git.giftfish.de/ston1th/gowiki/pkg/core"
"testing"
)
func TestCache(t *testing.T) {
title := "test/page"
pages := []*core.Page{
&core.Page{
Title: title,
Markdown: "page v1",
},
&core.Page{
Title: title,
Markdown: "page v2",
},
}
c := NewCache()
t.Run("Add", func(t *testing.T) {
c.Add(title, pages[0])
})
t.Run("Get", func(t *testing.T) {
p := c.Get(title)
if p.Markdown != pages[0].Markdown {
t.Fail()
}
})
t.Run("Add", func(t *testing.T) {
c.Add(title, pages[1])
})
t.Run("Get", func(t *testing.T) {
p := c.Get(title)
if p.Markdown == pages[0].Markdown {
t.Fail()
}
})
t.Run("Delete", func(t *testing.T) {
c.Delete(title)
})
t.Run("Get", func(t *testing.T) {
p := c.Get(title)
if p != nil {
t.Fail()
}
})
}

View file

@ -32,6 +32,9 @@ var (
)
func initServer(conf core.Config) (err error) {
if err = core.Pledge("stdio rpath wpath cpath inet fattr flock proc exec id", ""); err != nil {
return
}
dropCfg := godrop.Config{
User: conf.User,
Group: conf.Group,
@ -47,7 +50,11 @@ func initServer(conf core.Config) (err error) {
return
}
log.NewLogger(conf.LogFile)
if err = core.Pledge("stdio rpath wpath cpath inet fattr flock", ""); err != nil {
return
}
log.InitLogger(conf.DataDir, conf.LogFile, conf.Debug)
l, err := godrop.GetListener()
if err != nil {
@ -89,7 +96,7 @@ func Run(version string) {
Action: func(c *cli.Context) error {
conf.Version = app.Version
if err := initServer(conf); err != nil {
stdlog.Fatal("server: ", err)
stdlog.Fatal(err)
}
return nil
},
@ -187,7 +194,7 @@ func serverFlags() []cli.Flag {
cli.StringFlag{
Name: "log",
Value: defLogFile,
Usage: "log file (leave empty for stdout, this only works in combination with -f)",
Usage: "log file (use - for stdout, this only works in combination with -f)",
Destination: &conf.LogFile,
},
cli.StringFlag{
@ -208,7 +215,7 @@ func serverFlags() []cli.Flag {
cli.BoolFlag{
Name: "debug",
Usage: "enable debugging",
Destination: &log.DebugEnabled,
Destination: &conf.Debug,
},
}
}

View file

@ -11,18 +11,6 @@ const (
Private
)
func ParsePerm(p int) Permission {
switch p {
case 1:
return Public
case 2:
return Internal
case 3:
return Private
}
return Invalid
}
func ParsePermString(p string) Permission {
switch p {
case "1":
@ -51,10 +39,6 @@ func WritePerm(username, section string, p *Page) bool {
if section != WikiSection && section != username {
return false
}
return writePerm(username, p)
}
func writePerm(username string, p *Page) bool {
switch p.Perm {
case Public, Internal:
if p.Owner == WikiSection {

View file

@ -0,0 +1,81 @@
// Copyright (C) 2018 Marius Schellenberger
package core
import (
"fmt"
"testing"
)
func TestParsePermString(t *testing.T) {
data := map[string]Permission{
"": Invalid,
"1": Public,
"2": Internal,
"3": Private,
"123": Invalid,
"test": Invalid,
}
for k, v := range data {
t.Run(k, func(t *testing.T) {
if ParsePermString(k) != v {
t.Fail()
}
})
}
}
func TestReadPerm(t *testing.T) {
data := []struct {
Username string
Page *Page
OK bool
}{
{"", &Page{Perm: Invalid, Owner: "admin"}, false},
{"", &Page{Perm: Public, Owner: "admin"}, true},
{"user", &Page{Perm: Public, Owner: "admin"}, true},
{"", &Page{Perm: Internal, Owner: "admin"}, false},
{"user", &Page{Perm: Internal, Owner: "admin"}, true},
{"", &Page{Perm: Private, Owner: "admin"}, false},
{"user", &Page{Perm: Private, Owner: "admin"}, false},
{"admin", &Page{Perm: Private, Owner: "admin"}, true},
}
for i, v := range data {
t.Run(fmt.Sprintf("Read:%d", i), func(t *testing.T) {
if ReadPerm(v.Username, v.Page) != v.OK {
t.Fail()
}
})
}
}
func TestWritePerm(t *testing.T) {
data := []struct {
Username string
Section string
Page *Page
OK bool
}{
{"user", "admin", &Page{Perm: Invalid, Owner: "admin"}, false},
{"", "user", &Page{Perm: Invalid, Owner: "admin"}, false},
{"user", "user", &Page{Perm: Invalid, Owner: "user"}, false},
{"user", "user", &Page{Perm: Public, Owner: "user"}, true},
{"user", "user", &Page{Perm: Internal, Owner: "user"}, true},
{"user", "user", &Page{Perm: Private, Owner: "user"}, true},
{"", WikiSection, &Page{Perm: Invalid, Owner: "admin"}, false},
{"", WikiSection, &Page{Perm: Public, Owner: "admin"}, false},
{"", WikiSection, &Page{Perm: Internal, Owner: "admin"}, false},
{"user", WikiSection, &Page{Perm: Public, Owner: WikiSection}, true},
{"user", WikiSection, &Page{Perm: Internal, Owner: WikiSection}, true},
{"", "admin", &Page{Perm: Private, Owner: "admin"}, false},
{"user", "admin", &Page{Perm: Private, Owner: "admin"}, false},
{"admin", "admin", &Page{Perm: Private, Owner: "admin"}, true},
}
for i, v := range data {
t.Run(fmt.Sprintf("Write:%d", i), func(t *testing.T) {
if WritePerm(v.Username, v.Section, v.Page) != v.OK {
t.Fail()
}
})
}
}

7
pkg/core/pledge.go Normal file
View file

@ -0,0 +1,7 @@
// Copyright (C) 2018 Marius Schellenberger
// +build !openbsd
package core
func Pledge(promises, execpromises string) error { return nil }

View file

@ -0,0 +1,11 @@
// Copyright (C) 2018 Marius Schellenberger
// +build openbsd
package core
import "golang.org/x/sys/unix"
func Pledge(promises, execpromises string) error {
return unix.Pledge(promises, execpromises)
}

View file

@ -8,7 +8,7 @@ type Page struct {
Title string `json:"title"`
StoreTitle string `json:"store_title"`
Markdown string `json:"markdown"`
PageIndex template.HTML `json:"page_index"`
TOC template.HTML `json:"toc"`
HTML template.HTML `json:"html"`
Created string `json:"created"`
Updated string `json:"updated"`
@ -52,4 +52,5 @@ type Config struct {
Secret string
Foreground bool
SecureCookie bool
Debug bool
}

View file

@ -3,13 +3,13 @@
package db
import (
"fmt"
"git.giftfish.de/ston1th/gowiki/pkg/cache"
"git.giftfish.de/ston1th/gowiki/pkg/core"
"git.giftfish.de/ston1th/gowiki/pkg/index"
"git.giftfish.de/ston1th/gowiki/pkg/render"
"git.giftfish.de/ston1th/gowiki/pkg/store"
"io"
"path/filepath"
)
const (
@ -24,24 +24,19 @@ You can customize it how you like.`
const blevePath = "bleve"
func dbErr(i interface{}) error {
return fmt.Errorf("db: %s", i)
}
type DB struct {
store store.Store
cache *cache.Cache
Index *index.Index
}
func New(datadir string) (db *DB, err error) {
func New(dir string) (db *DB, err error) {
db = &DB{cache: cache.NewCache()}
db.Index, err = index.NewIndex(datadir + "/" + blevePath)
db.store, err = store.NewBoltStore(filepath.Join(dir, storeFile), nil)
if err != nil {
return
}
db.store, err = store.NewBoltStore(storeFile, nil)
db.Index, err = index.NewIndex(filepath.Join(dir, blevePath))
if err != nil {
return
}

View file

@ -3,6 +3,7 @@
package db
import (
"errors"
"git.giftfish.de/ston1th/gowiki/pkg/core"
"git.giftfish.de/ston1th/gowiki/pkg/render"
"git.giftfish.de/ston1th/gowiki/pkg/store"
@ -13,10 +14,10 @@ const pagePrefix = "page/"
var (
errPageNotFound = store.ErrKeyNotFound
errPermissionDenied = dbErr("permission denied")
errInvalidPermission = dbErr("invalid permission")
errPrivateWikiPage = dbErr("private wiki pages are not allowed")
errDeleteIndexPage = dbErr("index page can not be deleted")
errPermissionDenied = errors.New("db: permission denied")
errInvalidPermission = errors.New("db: invalid permission")
errPrivateWikiPage = errors.New("db: private wiki pages are not allowed")
errDeleteIndexPage = errors.New("db: index page can not be deleted")
)
func (db *DB) GetAllPages(username string) (pages core.Pages) {

View file

@ -17,9 +17,9 @@ import (
const userPrefix = "user/"
var (
errUserNotFound = dbErr("user not found")
errUserExists = dbErr("user already exist")
errUserReserved = dbErr("username is reserved")
errUserNotFound = errors.New("db: user not found")
errUserExists = errors.New("db: user already exist")
errUserReserved = errors.New("db: username is reserved")
userRe = regexp.MustCompile("[^a-zA-Z0-9]+")
)

View file

@ -3,7 +3,7 @@
package index
import (
"fmt"
"errors"
"git.giftfish.de/ston1th/gowiki/pkg/core"
"github.com/blevesearch/bleve"
"html/template"
@ -18,10 +18,6 @@ type indexPage struct {
Search string `json:"search"`
}
func indexErr(i interface{}) error {
return fmt.Errorf("index: %s", i)
}
type Index struct {
i bleve.Index
}
@ -32,13 +28,13 @@ func NewIndex(path string) (i *Index, err error) {
mapping := bleve.NewIndexMapping()
bi, err = bleve.New(path, mapping)
if err != nil {
err = indexErr(err)
err = errors.New("index: " + err.Error())
return
}
} else {
bi, err = bleve.Open(path)
if err != nil {
err = indexErr(err)
err = errors.New("index: " + err.Error())
return
}
}

View file

@ -5,21 +5,20 @@ package log
import (
stdlog "log"
"os"
"path/filepath"
)
const debug = "debug: "
var (
log *stdlog.Logger
DebugEnabled = false
log = stdlog.New(os.Stdout, "", stdlog.LstdFlags)
debug = false
)
func NewLogger(file string) {
if file == "" {
log = stdlog.New(os.Stdout, "", stdlog.LstdFlags)
func InitLogger(dir, file string, debug bool) {
debug = debug
if file == "-" {
return
}
f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0640)
f, err := os.OpenFile(filepath.Join(dir, file), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0640)
if err != nil {
stdlog.Fatal(err)
}
@ -40,13 +39,13 @@ func Fatal(v ...interface{}) {
}
func Debug(v ...interface{}) {
if DebugEnabled {
log.Println(append([]interface{}{debug}, v...))
if debug {
log.Println(append([]interface{}{"debug:"}, v...))
}
}
func Debugf(fmt string, v ...interface{}) {
if DebugEnabled {
log.Printf(debug+fmt, v...)
if debug {
log.Printf("debug: "+fmt, v...)
}
}

View file

@ -30,21 +30,17 @@ var (
func Render(p *core.Page) string {
rend := blackfriday.MarkdownCommon([]byte(p.Markdown))
index, html := buildTOC(string(rend))
toc, html := buildTOC(string(rend))
html = strings.Replace(html, "<table>", `<table class="table">`, -1)
html = strings.Replace(html, "</h1>", `</h1><hr>`, -1)
html = strings.Replace(html, "<a", `<a target="_blank"`, -1)
p.PageIndex = template.HTML(index)
p.TOC = template.HTML(toc)
p.HTML = template.HTML(html)
return whiteSpace.ReplaceAllString(string(filter.Filter(rend)), " ")
}
func spaceReplace(s string) string {
return whiteSpace.ReplaceAllString(s, "+")
}
func Title(title string) string {
return spaceReplace(titleRe.ReplaceAllString(title, ""))
return whiteSpace.ReplaceAllString(titleRe.ReplaceAllString(title, ""), "+")
}
func StoreTitle(section, title string) string {
@ -59,7 +55,7 @@ func UnstoreTitle(storeTitle string) (section, title string) {
}
func buildTOC(html string) (string, string) {
var ret string
var toc string
m := make(map[string]int)
hn := reIndex.FindAllStringSubmatch(html, -1)
html = reHeader.ReplaceAllString(html, `<a id="${2}" class="anchor"></a>${1}${2}${3}`)
@ -75,17 +71,17 @@ func buildTOC(html string) (string, string) {
m[link] = i + 1
newLink := link + strconv.Itoa(i)
html = strings.Replace(html, link+`"`, newLink+`"`, 1)
ret += before(hn, k)
ret += `<li><a href="#` + newLink + `">` + v[2] + "</a>"
ret += after(hn, k)
toc += before(hn, k)
toc += `<li><a href="#` + newLink + `">` + v[2] + "</a>"
toc += after(hn, k)
}
return ret, html
return toc, html
}
func before(arr [][]string, i int) string {
c, _ := strconv.Atoi(arr[i][1])
if c == 1 {
return `<ul type="circle">`
return `<ul class="toc">`
}
return ""
}
@ -96,13 +92,14 @@ func after(arr [][]string, i int) string {
return closeTag(c-1) + "</ul>"
}
n, _ := strconv.Atoi(arr[i+1][1])
if n == 1 {
switch {
case n == 1:
return closeTag(c-n) + "</ul>"
} else if n < c {
case n < c:
return closeTag(c - n)
} else if n > c {
case n > c:
return openTag(n - c)
} else if n == c {
case n == c:
return "</li>"
}
return ""
@ -110,11 +107,11 @@ func after(arr [][]string, i int) string {
func openTag(j int) (ret string) {
for i := 0; i < j; i++ {
ret += `<ul type="circle"><li>`
}
if j > 1 {
ret = strings.Replace(ret, "circle", "none", j-1)
ret += `<ul class="toc"><li>`
}
//if j > 1 {
// ret = strings.Replace(ret, "circle", "none", j-1)
//}
ret = ret[0 : len(ret)-4]
return
}

29
pkg/render/render_test.go Normal file
View file

@ -0,0 +1,29 @@
// Copyright (C) 2018 Marius Schellenberger
package render
import (
"testing"
)
func TestTitleFunctions(t *testing.T) {
section := "section"
title := "title"
combined := "section/title"
t.Run("StoreTitle", func(t *testing.T) {
if StoreTitle(section, title) != combined {
t.Fail()
}
})
t.Run("UnstoreTitle", func(t *testing.T) {
se, ti := UnstoreTitle(combined)
if se != section || ti != title {
t.Fail()
}
})
t.Run("Title", func(t *testing.T) {
if Title("9ßw$%4h4t-8v74 287(G /SV&3[45v}=94#+?)") != "9w4h4t-8v74+287G+SV345v94" {
t.Fail()
}
})
}

View file

@ -5,6 +5,7 @@ package server
import (
"bytes"
"context"
"errors"
"git.giftfish.de/ston1th/gowiki/pkg/core"
"git.giftfish.de/ston1th/gowiki/pkg/db"
"git.giftfish.de/ston1th/gowiki/pkg/log"
@ -54,7 +55,7 @@ func NewHTTPServer(l net.Listener, conf core.Config) (srv *HTTPServer) {
func (s *HTTPServer) Start() (err error) {
s.DB, err = db.New(s.DataDir)
if err != nil {
return
return errors.New("db: " + err.Error())
}
if s.Secret != "" {
s.JWT, err = jwt.New(jwt.DefaultExpiry, s.DB, bytes.NewBufferString(s.Secret))
@ -62,7 +63,7 @@ func (s *HTTPServer) Start() (err error) {
s.JWT, err = jwt.New(jwt.DefaultExpiry, s.DB, nil)
}
if err != nil {
return
return errors.New("server: " + err.Error())
}
go func() {
log.Println(s.srv.Serve(s.listener))

View file

@ -48,7 +48,7 @@ const (
<head>
<title>GoWiki | {{.Title}}</title>
<link rel="stylesheet" type="text/css" href="/bootstrap.css" media="screen" integrity="sha256-NeLmQ7cX66J4MdtgGlG3O/TgvsSyP1b9WbaQtxYZUbQ="></link>
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-1LNAeuiM/rtsY1jIgLovUZ7HbYnOCkj3ZyailNA3rFQ="></link>
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-/SZfW8eVqFuioMg+QXacvK1Zc3XLtT5LGu9tMle65Jc="></link>
</head>
<body id="back-to-top">
<div class="navbar navbar-expand fixed-top navbar-dark bg-primary">
@ -259,7 +259,7 @@ const (
</div>
<div class="row mt-5">
<div class="col">
{{.Data.PageIndex}}
{{.Data.TOC}}
</div>
<div class="col">
<div class="btn-group float-md-right">
@ -388,7 +388,7 @@ const (
</div>
<div class="row mt-5">
<div class="col">
{{.Data.PageIndex}}
{{.Data.TOC}}
</div>
<div class="col">
<div class="btn-group float-md-right">
@ -680,6 +680,10 @@ h6 {
font-family: 'Courier New';
font-size: 14;
}
ul.toc {
list-style-type: disc;
margin-bottom: 0 !important;
}
pre {
display: block;
padding: 10px;

View file

@ -30,6 +30,9 @@ func checkXsrf(xsrf string, secret []byte) bool {
if err != nil {
return false
}
if len(t) != jwt.KeySize {
return false
}
return subtle.ConstantTimeCompare(secret, xor(t[keySize:], t[:keySize])) == 1
}

31
pkg/server/xsrf_test.go Normal file
View file

@ -0,0 +1,31 @@
// Copyright (C) 2018 Marius Schellenberger
package server
import (
"testing"
)
func TestXsrf(t *testing.T) {
sec := genKey(keySize)
if len(sec) != keySize {
t.Fatal("len(sec) != keySize")
}
xsrf := newXsrf(sec)
if xsrf == "" {
t.Fatal("xsrf is empty")
}
if !checkXsrf(xsrf, sec) {
t.Fatal("valid xsrf check failed")
}
if checkXsrf("test", sec) {
t.Fatal("invalid xsrf check succeeded")
}
rnd := genKey(keySize)
if checkXsrf(xsrf, rnd) {
t.Fatal("invalid xsrf check succeeded")
}
}

View file

@ -8,7 +8,10 @@ import (
"io"
)
const defaultBoltBucket = "default"
const (
defaultBoltBucket = "default"
fileMode = 0640
)
type BoltStore struct {
Marshaler
@ -19,7 +22,7 @@ func NewBoltStore(file string, m Marshaler) (bs *BoltStore, err error) {
if m == nil {
m = NewGOB()
}
db, err := bolt.Open(file, 0640, nil)
db, err := bolt.Open(file, fileMode, nil)
if err != nil {
return
}

View file

@ -0,0 +1,99 @@
// Copyright (C) 2018 Marius Schellenberger
package store
import (
"bytes"
"errors"
"io/ioutil"
"os"
"path/filepath"
"testing"
)
type testData struct {
Data string
}
func TestBoltStore(t *testing.T) {
data := []testData{
{"123"},
{"hello"},
}
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
bs, err := NewBoltStore(filepath.Join(dir, "bolt.db"), nil)
if err != nil {
t.Fatal(err)
}
err = bs.Set("key0", data[0])
if err != nil {
t.Fatal(err)
}
err = bs.Set("key1", data[1])
if err != nil {
t.Fatal(err)
}
t.Run("GetKey0", func(t *testing.T) {
var d testData
err := bs.Get("key0", &d)
if err != nil {
t.Fatal(err)
}
if d != data[0] {
t.Error("d is not data[0]")
}
})
t.Run("ForEach", func(t *testing.T) {
err := bs.ForEach(func(k string, v []byte) error {
if k == "key0" || k == "key1" {
return nil
}
return errors.New("key0 or key1 not found")
})
if err != nil {
t.Fatal(err)
}
})
t.Run("DumpRestore", func(t *testing.T) {
buf := new(bytes.Buffer)
err := bs.Dump(buf)
if err != nil {
t.Fatal(err)
}
if buf.Len() == 0 {
t.Error("no dump written")
}
err = bs.Restore(buf)
if err != nil {
t.Fatal(err)
}
})
t.Run("DeleteKey1", func(t *testing.T) {
err := bs.Delete("key1")
if err != nil {
t.Fatal(err)
}
})
t.Run("GetKey1", func(t *testing.T) {
var d testData
err := bs.Get("key1", &d)
if err == nil {
t.Error("key1 should be deleted")
}
if d == data[1] {
t.Error("d is not data[1]")
}
})
t.Run("Close", func(t *testing.T) {
err := bs.Close()
if err != nil {
t.Fatal(err)
}
})
}

View file

@ -2,16 +2,12 @@
package store
import "fmt"
func storeErr(i interface{}) error {
return fmt.Errorf("store: %s", i)
}
import "errors"
var (
ErrKeyNotFound = storeErr("key not found")
ErrWriterIsNil = storeErr("writer is nil")
ErrReaderIsNil = storeErr("reader is nil")
ErrKeyNotFound = errors.New("store: key not found")
ErrWriterIsNil = errors.New("store: writer is nil")
ErrReaderIsNil = errors.New("store: reader is nil")
)
type Store interface {

View file

@ -6,7 +6,7 @@
# of the templates directory
templates_go="pkg/server/templates.go"
echo "Generating templates.go"
echo "Generating ${templates_go}"
if [ ! -d templates ]; then
echo "error: templates directory not found" 1>&2

View file

@ -75,6 +75,10 @@ h6 {
font-family: 'Courier New';
font-size: 14;
}
ul.toc {
list-style-type: disc;
margin-bottom: 0 !important;
}
pre {
display: block;
padding: 10px;

View file

@ -3,7 +3,7 @@
<head>
<title>GoWiki | {{.Title}}</title>
<link rel="stylesheet" type="text/css" href="/bootstrap.css" media="screen" integrity="sha256-NeLmQ7cX66J4MdtgGlG3O/TgvsSyP1b9WbaQtxYZUbQ="></link>
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-1LNAeuiM/rtsY1jIgLovUZ7HbYnOCkj3ZyailNA3rFQ="></link>
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-/SZfW8eVqFuioMg+QXacvK1Zc3XLtT5LGu9tMle65Jc="></link>
</head>
<body id="back-to-top">
<div class="navbar navbar-expand fixed-top navbar-dark bg-primary">

View file

@ -25,7 +25,7 @@
</div>
<div class="row mt-5">
<div class="col">
{{.Data.PageIndex}}
{{.Data.TOC}}
</div>
<div class="col">
<div class="btn-group float-md-right">

View file

@ -25,7 +25,7 @@
</div>
<div class="row mt-5">
<div class="col">
{{.Data.PageIndex}}
{{.Data.TOC}}
</div>
<div class="col">
<div class="btn-group float-md-right">

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff