some tests and major cleanup
This commit is contained in:
parent
8ef2a2547c
commit
16671b3406
30 changed files with 192057 additions and 202431 deletions
19
Makefile
19
Makefile
|
|
@ -10,13 +10,19 @@ all: $(PROGRAM)
|
||||||
setup:
|
setup:
|
||||||
$(CC) get github.com/client9/misspell
|
$(CC) get github.com/client9/misspell
|
||||||
|
|
||||||
|
vendor: clean codeqa
|
||||||
|
CGO_ENABLED=0 $(CC) $(BUILD) -mod=vendor $(LDFLAGS)
|
||||||
|
|
||||||
$(PROGRAM): clean codeqa
|
$(PROGRAM): clean codeqa
|
||||||
CGO_ENABLED=0 $(CC) $(BUILD) $(LDFLAGS)
|
CGO_ENABLED=0 $(CC) $(BUILD) $(LDFLAGS)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
$(CC) clean -x
|
$(CC) clean -x
|
||||||
|
|
||||||
codeqa: templates gofmt misspell golint
|
codeqa: generate gofmt misspell golint test
|
||||||
|
|
||||||
|
generate:
|
||||||
|
$(CC) generate
|
||||||
|
|
||||||
gofmt:
|
gofmt:
|
||||||
gofmt -w .
|
gofmt -w .
|
||||||
|
|
@ -25,9 +31,14 @@ golint:
|
||||||
$(GOPATH)/bin/golint .
|
$(GOPATH)/bin/golint .
|
||||||
|
|
||||||
misspell:
|
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 *
|
$(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:
|
test:
|
||||||
$(CC) generate
|
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
1
go.mod
|
|
@ -42,4 +42,5 @@ require (
|
||||||
github.com/urfave/cli v1.19.1
|
github.com/urfave/cli v1.19.1
|
||||||
github.com/willf/bitset v1.1.9 // indirect
|
github.com/willf/bitset v1.1.9 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20161031180806-9477e0b78b9a
|
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
50
pkg/cache/cache_test.go
vendored
Normal 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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -32,6 +32,9 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func initServer(conf core.Config) (err error) {
|
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{
|
dropCfg := godrop.Config{
|
||||||
User: conf.User,
|
User: conf.User,
|
||||||
Group: conf.Group,
|
Group: conf.Group,
|
||||||
|
|
@ -47,7 +50,11 @@ func initServer(conf core.Config) (err error) {
|
||||||
return
|
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()
|
l, err := godrop.GetListener()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -89,7 +96,7 @@ func Run(version string) {
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
conf.Version = app.Version
|
conf.Version = app.Version
|
||||||
if err := initServer(conf); err != nil {
|
if err := initServer(conf); err != nil {
|
||||||
stdlog.Fatal("server: ", err)
|
stdlog.Fatal(err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|
@ -187,7 +194,7 @@ func serverFlags() []cli.Flag {
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "log",
|
Name: "log",
|
||||||
Value: defLogFile,
|
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,
|
Destination: &conf.LogFile,
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
|
|
@ -208,7 +215,7 @@ func serverFlags() []cli.Flag {
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "debug",
|
Name: "debug",
|
||||||
Usage: "enable debugging",
|
Usage: "enable debugging",
|
||||||
Destination: &log.DebugEnabled,
|
Destination: &conf.Debug,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,6 @@ const (
|
||||||
Private
|
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 {
|
func ParsePermString(p string) Permission {
|
||||||
switch p {
|
switch p {
|
||||||
case "1":
|
case "1":
|
||||||
|
|
@ -51,10 +39,6 @@ func WritePerm(username, section string, p *Page) bool {
|
||||||
if section != WikiSection && section != username {
|
if section != WikiSection && section != username {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return writePerm(username, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func writePerm(username string, p *Page) bool {
|
|
||||||
switch p.Perm {
|
switch p.Perm {
|
||||||
case Public, Internal:
|
case Public, Internal:
|
||||||
if p.Owner == WikiSection {
|
if p.Owner == WikiSection {
|
||||||
|
|
|
||||||
81
pkg/core/permission_test.go
Normal file
81
pkg/core/permission_test.go
Normal 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
7
pkg/core/pledge.go
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
// Copyright (C) 2018 Marius Schellenberger
|
||||||
|
|
||||||
|
// +build !openbsd
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
func Pledge(promises, execpromises string) error { return nil }
|
||||||
11
pkg/core/pledge_openbsd.go
Normal file
11
pkg/core/pledge_openbsd.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ type Page struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
StoreTitle string `json:"store_title"`
|
StoreTitle string `json:"store_title"`
|
||||||
Markdown string `json:"markdown"`
|
Markdown string `json:"markdown"`
|
||||||
PageIndex template.HTML `json:"page_index"`
|
TOC template.HTML `json:"toc"`
|
||||||
HTML template.HTML `json:"html"`
|
HTML template.HTML `json:"html"`
|
||||||
Created string `json:"created"`
|
Created string `json:"created"`
|
||||||
Updated string `json:"updated"`
|
Updated string `json:"updated"`
|
||||||
|
|
@ -52,4 +52,5 @@ type Config struct {
|
||||||
Secret string
|
Secret string
|
||||||
Foreground bool
|
Foreground bool
|
||||||
SecureCookie bool
|
SecureCookie bool
|
||||||
|
Debug bool
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
pkg/db/db.go
13
pkg/db/db.go
|
|
@ -3,13 +3,13 @@
|
||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"git.giftfish.de/ston1th/gowiki/pkg/cache"
|
"git.giftfish.de/ston1th/gowiki/pkg/cache"
|
||||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||||
"git.giftfish.de/ston1th/gowiki/pkg/index"
|
"git.giftfish.de/ston1th/gowiki/pkg/index"
|
||||||
"git.giftfish.de/ston1th/gowiki/pkg/render"
|
"git.giftfish.de/ston1th/gowiki/pkg/render"
|
||||||
"git.giftfish.de/ston1th/gowiki/pkg/store"
|
"git.giftfish.de/ston1th/gowiki/pkg/store"
|
||||||
"io"
|
"io"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -24,24 +24,19 @@ You can customize it how you like.`
|
||||||
|
|
||||||
const blevePath = "bleve"
|
const blevePath = "bleve"
|
||||||
|
|
||||||
func dbErr(i interface{}) error {
|
|
||||||
return fmt.Errorf("db: %s", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
type DB struct {
|
type DB struct {
|
||||||
store store.Store
|
store store.Store
|
||||||
cache *cache.Cache
|
cache *cache.Cache
|
||||||
Index *index.Index
|
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 = &DB{cache: cache.NewCache()}
|
||||||
db.Index, err = index.NewIndex(datadir + "/" + blevePath)
|
db.store, err = store.NewBoltStore(filepath.Join(dir, storeFile), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
db.Index, err = index.NewIndex(filepath.Join(dir, blevePath))
|
||||||
db.store, err = store.NewBoltStore(storeFile, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||||
"git.giftfish.de/ston1th/gowiki/pkg/render"
|
"git.giftfish.de/ston1th/gowiki/pkg/render"
|
||||||
"git.giftfish.de/ston1th/gowiki/pkg/store"
|
"git.giftfish.de/ston1th/gowiki/pkg/store"
|
||||||
|
|
@ -13,10 +14,10 @@ const pagePrefix = "page/"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errPageNotFound = store.ErrKeyNotFound
|
errPageNotFound = store.ErrKeyNotFound
|
||||||
errPermissionDenied = dbErr("permission denied")
|
errPermissionDenied = errors.New("db: permission denied")
|
||||||
errInvalidPermission = dbErr("invalid permission")
|
errInvalidPermission = errors.New("db: invalid permission")
|
||||||
errPrivateWikiPage = dbErr("private wiki pages are not allowed")
|
errPrivateWikiPage = errors.New("db: private wiki pages are not allowed")
|
||||||
errDeleteIndexPage = dbErr("index page can not be deleted")
|
errDeleteIndexPage = errors.New("db: index page can not be deleted")
|
||||||
)
|
)
|
||||||
|
|
||||||
func (db *DB) GetAllPages(username string) (pages core.Pages) {
|
func (db *DB) GetAllPages(username string) (pages core.Pages) {
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ import (
|
||||||
const userPrefix = "user/"
|
const userPrefix = "user/"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errUserNotFound = dbErr("user not found")
|
errUserNotFound = errors.New("db: user not found")
|
||||||
errUserExists = dbErr("user already exist")
|
errUserExists = errors.New("db: user already exist")
|
||||||
errUserReserved = dbErr("username is reserved")
|
errUserReserved = errors.New("db: username is reserved")
|
||||||
|
|
||||||
userRe = regexp.MustCompile("[^a-zA-Z0-9]+")
|
userRe = regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
package index
|
package index
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"errors"
|
||||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||||
"github.com/blevesearch/bleve"
|
"github.com/blevesearch/bleve"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
|
@ -18,10 +18,6 @@ type indexPage struct {
|
||||||
Search string `json:"search"`
|
Search string `json:"search"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func indexErr(i interface{}) error {
|
|
||||||
return fmt.Errorf("index: %s", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Index struct {
|
type Index struct {
|
||||||
i bleve.Index
|
i bleve.Index
|
||||||
}
|
}
|
||||||
|
|
@ -32,13 +28,13 @@ func NewIndex(path string) (i *Index, err error) {
|
||||||
mapping := bleve.NewIndexMapping()
|
mapping := bleve.NewIndexMapping()
|
||||||
bi, err = bleve.New(path, mapping)
|
bi, err = bleve.New(path, mapping)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = indexErr(err)
|
err = errors.New("index: " + err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bi, err = bleve.Open(path)
|
bi, err = bleve.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = indexErr(err)
|
err = errors.New("index: " + err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,20 @@ package log
|
||||||
import (
|
import (
|
||||||
stdlog "log"
|
stdlog "log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
const debug = "debug: "
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
log *stdlog.Logger
|
log = stdlog.New(os.Stdout, "", stdlog.LstdFlags)
|
||||||
DebugEnabled = false
|
debug = false
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewLogger(file string) {
|
func InitLogger(dir, file string, debug bool) {
|
||||||
if file == "" {
|
debug = debug
|
||||||
log = stdlog.New(os.Stdout, "", stdlog.LstdFlags)
|
if file == "-" {
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
stdlog.Fatal(err)
|
stdlog.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -40,13 +39,13 @@ func Fatal(v ...interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Debug(v ...interface{}) {
|
func Debug(v ...interface{}) {
|
||||||
if DebugEnabled {
|
if debug {
|
||||||
log.Println(append([]interface{}{debug}, v...))
|
log.Println(append([]interface{}{"debug:"}, v...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Debugf(fmt string, v ...interface{}) {
|
func Debugf(fmt string, v ...interface{}) {
|
||||||
if DebugEnabled {
|
if debug {
|
||||||
log.Printf(debug+fmt, v...)
|
log.Printf("debug: "+fmt, v...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,21 +30,17 @@ var (
|
||||||
|
|
||||||
func Render(p *core.Page) string {
|
func Render(p *core.Page) string {
|
||||||
rend := blackfriday.MarkdownCommon([]byte(p.Markdown))
|
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, "<table>", `<table class="table">`, -1)
|
||||||
html = strings.Replace(html, "</h1>", `</h1><hr>`, -1)
|
html = strings.Replace(html, "</h1>", `</h1><hr>`, -1)
|
||||||
html = strings.Replace(html, "<a", `<a target="_blank"`, -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)
|
p.HTML = template.HTML(html)
|
||||||
return whiteSpace.ReplaceAllString(string(filter.Filter(rend)), " ")
|
return whiteSpace.ReplaceAllString(string(filter.Filter(rend)), " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func spaceReplace(s string) string {
|
|
||||||
return whiteSpace.ReplaceAllString(s, "+")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Title(title string) string {
|
func Title(title string) string {
|
||||||
return spaceReplace(titleRe.ReplaceAllString(title, ""))
|
return whiteSpace.ReplaceAllString(titleRe.ReplaceAllString(title, ""), "+")
|
||||||
}
|
}
|
||||||
|
|
||||||
func StoreTitle(section, title string) string {
|
func StoreTitle(section, title string) string {
|
||||||
|
|
@ -59,7 +55,7 @@ func UnstoreTitle(storeTitle string) (section, title string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildTOC(html string) (string, string) {
|
func buildTOC(html string) (string, string) {
|
||||||
var ret string
|
var toc string
|
||||||
m := make(map[string]int)
|
m := make(map[string]int)
|
||||||
hn := reIndex.FindAllStringSubmatch(html, -1)
|
hn := reIndex.FindAllStringSubmatch(html, -1)
|
||||||
html = reHeader.ReplaceAllString(html, `<a id="${2}" class="anchor"></a>${1}${2}${3}`)
|
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
|
m[link] = i + 1
|
||||||
newLink := link + strconv.Itoa(i)
|
newLink := link + strconv.Itoa(i)
|
||||||
html = strings.Replace(html, link+`"`, newLink+`"`, 1)
|
html = strings.Replace(html, link+`"`, newLink+`"`, 1)
|
||||||
ret += before(hn, k)
|
toc += before(hn, k)
|
||||||
ret += `<li><a href="#` + newLink + `">` + v[2] + "</a>"
|
toc += `<li><a href="#` + newLink + `">` + v[2] + "</a>"
|
||||||
ret += after(hn, k)
|
toc += after(hn, k)
|
||||||
}
|
}
|
||||||
return ret, html
|
return toc, html
|
||||||
}
|
}
|
||||||
|
|
||||||
func before(arr [][]string, i int) string {
|
func before(arr [][]string, i int) string {
|
||||||
c, _ := strconv.Atoi(arr[i][1])
|
c, _ := strconv.Atoi(arr[i][1])
|
||||||
if c == 1 {
|
if c == 1 {
|
||||||
return `<ul type="circle">`
|
return `<ul class="toc">`
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -96,13 +92,14 @@ func after(arr [][]string, i int) string {
|
||||||
return closeTag(c-1) + "</ul>"
|
return closeTag(c-1) + "</ul>"
|
||||||
}
|
}
|
||||||
n, _ := strconv.Atoi(arr[i+1][1])
|
n, _ := strconv.Atoi(arr[i+1][1])
|
||||||
if n == 1 {
|
switch {
|
||||||
|
case n == 1:
|
||||||
return closeTag(c-n) + "</ul>"
|
return closeTag(c-n) + "</ul>"
|
||||||
} else if n < c {
|
case n < c:
|
||||||
return closeTag(c - n)
|
return closeTag(c - n)
|
||||||
} else if n > c {
|
case n > c:
|
||||||
return openTag(n - c)
|
return openTag(n - c)
|
||||||
} else if n == c {
|
case n == c:
|
||||||
return "</li>"
|
return "</li>"
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -110,11 +107,11 @@ func after(arr [][]string, i int) string {
|
||||||
|
|
||||||
func openTag(j int) (ret string) {
|
func openTag(j int) (ret string) {
|
||||||
for i := 0; i < j; i++ {
|
for i := 0; i < j; i++ {
|
||||||
ret += `<ul type="circle"><li>`
|
ret += `<ul class="toc"><li>`
|
||||||
}
|
|
||||||
if j > 1 {
|
|
||||||
ret = strings.Replace(ret, "circle", "none", j-1)
|
|
||||||
}
|
}
|
||||||
|
//if j > 1 {
|
||||||
|
// ret = strings.Replace(ret, "circle", "none", j-1)
|
||||||
|
//}
|
||||||
ret = ret[0 : len(ret)-4]
|
ret = ret[0 : len(ret)-4]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
29
pkg/render/render_test.go
Normal file
29
pkg/render/render_test.go
Normal 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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ package server
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||||
"git.giftfish.de/ston1th/gowiki/pkg/db"
|
"git.giftfish.de/ston1th/gowiki/pkg/db"
|
||||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
"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) {
|
func (s *HTTPServer) Start() (err error) {
|
||||||
s.DB, err = db.New(s.DataDir)
|
s.DB, err = db.New(s.DataDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return errors.New("db: " + err.Error())
|
||||||
}
|
}
|
||||||
if s.Secret != "" {
|
if s.Secret != "" {
|
||||||
s.JWT, err = jwt.New(jwt.DefaultExpiry, s.DB, bytes.NewBufferString(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)
|
s.JWT, err = jwt.New(jwt.DefaultExpiry, s.DB, nil)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return errors.New("server: " + err.Error())
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
log.Println(s.srv.Serve(s.listener))
|
log.Println(s.srv.Serve(s.listener))
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ const (
|
||||||
<head>
|
<head>
|
||||||
<title>GoWiki | {{.Title}}</title>
|
<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="/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>
|
</head>
|
||||||
<body id="back-to-top">
|
<body id="back-to-top">
|
||||||
<div class="navbar navbar-expand fixed-top navbar-dark bg-primary">
|
<div class="navbar navbar-expand fixed-top navbar-dark bg-primary">
|
||||||
|
|
@ -259,7 +259,7 @@ const (
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-5">
|
<div class="row mt-5">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{{.Data.PageIndex}}
|
{{.Data.TOC}}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group float-md-right">
|
<div class="btn-group float-md-right">
|
||||||
|
|
@ -388,7 +388,7 @@ const (
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-5">
|
<div class="row mt-5">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{{.Data.PageIndex}}
|
{{.Data.TOC}}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group float-md-right">
|
<div class="btn-group float-md-right">
|
||||||
|
|
@ -680,6 +680,10 @@ h6 {
|
||||||
font-family: 'Courier New';
|
font-family: 'Courier New';
|
||||||
font-size: 14;
|
font-size: 14;
|
||||||
}
|
}
|
||||||
|
ul.toc {
|
||||||
|
list-style-type: disc;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
pre {
|
pre {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ func checkXsrf(xsrf string, secret []byte) bool {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if len(t) != jwt.KeySize {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return subtle.ConstantTimeCompare(secret, xor(t[keySize:], t[:keySize])) == 1
|
return subtle.ConstantTimeCompare(secret, xor(t[keySize:], t[:keySize])) == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
31
pkg/server/xsrf_test.go
Normal file
31
pkg/server/xsrf_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,10 @@ import (
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultBoltBucket = "default"
|
const (
|
||||||
|
defaultBoltBucket = "default"
|
||||||
|
fileMode = 0640
|
||||||
|
)
|
||||||
|
|
||||||
type BoltStore struct {
|
type BoltStore struct {
|
||||||
Marshaler
|
Marshaler
|
||||||
|
|
@ -19,7 +22,7 @@ func NewBoltStore(file string, m Marshaler) (bs *BoltStore, err error) {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
m = NewGOB()
|
m = NewGOB()
|
||||||
}
|
}
|
||||||
db, err := bolt.Open(file, 0640, nil)
|
db, err := bolt.Open(file, fileMode, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
99
pkg/store/boltstore_test.go
Normal file
99
pkg/store/boltstore_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -2,16 +2,12 @@
|
||||||
|
|
||||||
package store
|
package store
|
||||||
|
|
||||||
import "fmt"
|
import "errors"
|
||||||
|
|
||||||
func storeErr(i interface{}) error {
|
|
||||||
return fmt.Errorf("store: %s", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrKeyNotFound = storeErr("key not found")
|
ErrKeyNotFound = errors.New("store: key not found")
|
||||||
ErrWriterIsNil = storeErr("writer is nil")
|
ErrWriterIsNil = errors.New("store: writer is nil")
|
||||||
ErrReaderIsNil = storeErr("reader is nil")
|
ErrReaderIsNil = errors.New("store: reader is nil")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Store interface {
|
type Store interface {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
# of the templates directory
|
# of the templates directory
|
||||||
|
|
||||||
templates_go="pkg/server/templates.go"
|
templates_go="pkg/server/templates.go"
|
||||||
echo "Generating templates.go"
|
echo "Generating ${templates_go}"
|
||||||
|
|
||||||
if [ ! -d templates ]; then
|
if [ ! -d templates ]; then
|
||||||
echo "error: templates directory not found" 1>&2
|
echo "error: templates directory not found" 1>&2
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,10 @@ h6 {
|
||||||
font-family: 'Courier New';
|
font-family: 'Courier New';
|
||||||
font-size: 14;
|
font-size: 14;
|
||||||
}
|
}
|
||||||
|
ul.toc {
|
||||||
|
list-style-type: disc;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
pre {
|
pre {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<title>GoWiki | {{.Title}}</title>
|
<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="/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>
|
</head>
|
||||||
<body id="back-to-top">
|
<body id="back-to-top">
|
||||||
<div class="navbar navbar-expand fixed-top navbar-dark bg-primary">
|
<div class="navbar navbar-expand fixed-top navbar-dark bg-primary">
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-5">
|
<div class="row mt-5">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{{.Data.PageIndex}}
|
{{.Data.TOC}}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group float-md-right">
|
<div class="btn-group float-md-right">
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-5">
|
<div class="row mt-5">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{{.Data.PageIndex}}
|
{{.Data.TOC}}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="btn-group float-md-right">
|
<div class="btn-group float-md-right">
|
||||||
|
|
|
||||||
38496
vendor/github.com/blevesearch/segment/segment_words.go
generated
vendored
38496
vendor/github.com/blevesearch/segment/segment_words.go
generated
vendored
File diff suppressed because it is too large
Load diff
71309
vendor/github.com/blevesearch/segment/segment_words_prod.go
generated
vendored
71309
vendor/github.com/blevesearch/segment/segment_words_prod.go
generated
vendored
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue