Add components
This commit is contained in:
parent
972c4d81f5
commit
648ab12c5b
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
target
|
114
devserver/devserver.go
Normal file
114
devserver/devserver.go
Normal file
@ -0,0 +1,114 @@
|
||||
// Package devserver makes it easy to view statically generated websites and
|
||||
// automatically rebuild them when source data changes. When combined with the
|
||||
// "livejs" plugin, it is possible to have a live preview of your site.
|
||||
package devserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
// Builder interface should be implemented by you to contain the required
|
||||
// goldsmith chain to generate your website.
|
||||
type Builder interface {
|
||||
Build(sourceDir, targetDir, cacheDir string)
|
||||
}
|
||||
|
||||
// DevServe should be called to start a web server using the provided builder.
|
||||
// While the source directory will be watched for changes by default, it is
|
||||
// possible to pass in additional directories to watch; modification of these
|
||||
// directories will automatically trigger a site rebuild. This function does
|
||||
// not return and will continue watching for file changes and serving your
|
||||
// website until it is terminated.
|
||||
func DevServe(builder Builder, port int, sourceDir, targetDir, cacheDir string, watchDirs ...string) {
|
||||
dirs := append(watchDirs, sourceDir)
|
||||
build(dirs, func() { builder.Build(sourceDir, targetDir, cacheDir) })
|
||||
|
||||
httpAddr := fmt.Sprintf(":%d", port)
|
||||
httpHandler := http.FileServer(http.Dir(targetDir))
|
||||
|
||||
log.Fatal(http.ListenAndServe(httpAddr, httpHandler))
|
||||
}
|
||||
|
||||
func build(dirs []string, callback func()) {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var mutex sync.Mutex
|
||||
timestamp := time.Now()
|
||||
dirty := true
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event := <-watcher.Events:
|
||||
mutex.Lock()
|
||||
timestamp = time.Now()
|
||||
dirty = true
|
||||
mutex.Unlock()
|
||||
|
||||
if event.Op&fsnotify.Create == fsnotify.Create {
|
||||
info, err := os.Stat(event.Name)
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
watch(event.Name, watcher)
|
||||
} else {
|
||||
watcher.Add(event.Name)
|
||||
}
|
||||
}
|
||||
case err := <-watcher.Errors:
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for range time.Tick(10 * time.Millisecond) {
|
||||
if dirty && time.Now().Sub(timestamp) > 100*time.Millisecond {
|
||||
mutex.Lock()
|
||||
dirty = false
|
||||
mutex.Unlock()
|
||||
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for _, dir := range dirs {
|
||||
watch(dir, watcher)
|
||||
}
|
||||
}
|
||||
|
||||
func watch(dir string, watcher *fsnotify.Watcher) {
|
||||
watcher.Add(dir)
|
||||
|
||||
items, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
fullPath := path.Join(dir, item.Name())
|
||||
if item.IsDir() {
|
||||
watch(fullPath, watcher)
|
||||
} else {
|
||||
watcher.Add(fullPath)
|
||||
}
|
||||
}
|
||||
}
|
21
filters/condition/condition.go
Normal file
21
filters/condition/condition.go
Normal file
@ -0,0 +1,21 @@
|
||||
package condition
|
||||
|
||||
import (
|
||||
"git.foosoft.net/alex/goldsmith"
|
||||
)
|
||||
|
||||
type Condition struct {
|
||||
accept bool
|
||||
}
|
||||
|
||||
func New(accept bool) *Condition {
|
||||
return &Condition{accept: accept}
|
||||
}
|
||||
|
||||
func (*Condition) Name() string {
|
||||
return "condition"
|
||||
}
|
||||
|
||||
func (self *Condition) Accept(file *goldsmith.File) bool {
|
||||
return self.accept
|
||||
}
|
28
filters/condition/condition_test.go
Normal file
28
filters/condition/condition_test.go
Normal file
@ -0,0 +1,28 @@
|
||||
package condition
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.foosoft.net/alex/goldsmith"
|
||||
"git.foosoft.net/alex/goldsmith/harness"
|
||||
)
|
||||
|
||||
func TestEnabled(self *testing.T) {
|
||||
harness.ValidateCase(
|
||||
self,
|
||||
"true",
|
||||
func(gs *goldsmith.Goldsmith) {
|
||||
gs.FilterPush(New(true))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestDisabled(self *testing.T) {
|
||||
harness.ValidateCase(
|
||||
self,
|
||||
"false",
|
||||
func(gs *goldsmith.Goldsmith) {
|
||||
gs.FilterPush(New(false))
|
||||
},
|
||||
)
|
||||
}
|
0
filters/condition/testdata/false/source/file_1.txt
vendored
Normal file
0
filters/condition/testdata/false/source/file_1.txt
vendored
Normal file
0
filters/condition/testdata/false/source/file_2.txt
vendored
Normal file
0
filters/condition/testdata/false/source/file_2.txt
vendored
Normal file
0
filters/condition/testdata/true/reference/file_1.txt
vendored
Normal file
0
filters/condition/testdata/true/reference/file_1.txt
vendored
Normal file
0
filters/condition/testdata/true/reference/file_2.txt
vendored
Normal file
0
filters/condition/testdata/true/reference/file_2.txt
vendored
Normal file
0
filters/condition/testdata/true/source/file_1.txt
vendored
Normal file
0
filters/condition/testdata/true/source/file_1.txt
vendored
Normal file
0
filters/condition/testdata/true/source/file_2.txt
vendored
Normal file
0
filters/condition/testdata/true/source/file_2.txt
vendored
Normal file
69
filters/operator/operator.go
Normal file
69
filters/operator/operator.go
Normal file
@ -0,0 +1,69 @@
|
||||
package operator
|
||||
|
||||
import (
|
||||
"git.foosoft.net/alex/goldsmith"
|
||||
)
|
||||
|
||||
type Operator interface {
|
||||
goldsmith.Filter
|
||||
}
|
||||
|
||||
func And(filters ...goldsmith.Filter) Operator {
|
||||
return &operatorAnd{filters}
|
||||
}
|
||||
|
||||
type operatorAnd struct {
|
||||
filters []goldsmith.Filter
|
||||
}
|
||||
|
||||
func (*operatorAnd) Name() string {
|
||||
return "operator"
|
||||
}
|
||||
|
||||
func (self *operatorAnd) Accept(file *goldsmith.File) bool {
|
||||
for _, filter := range self.filters {
|
||||
if !filter.Accept(file) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func Not(self goldsmith.Filter) Operator {
|
||||
return &operatorNot{self}
|
||||
}
|
||||
|
||||
type operatorNot struct {
|
||||
filter goldsmith.Filter
|
||||
}
|
||||
|
||||
func (*operatorNot) Name() string {
|
||||
return "operator"
|
||||
}
|
||||
|
||||
func (self *operatorNot) Accept(file *goldsmith.File) bool {
|
||||
return !self.filter.Accept(file)
|
||||
}
|
||||
|
||||
func Or(self ...goldsmith.Filter) Operator {
|
||||
return &operatorOr{self}
|
||||
}
|
||||
|
||||
type operatorOr struct {
|
||||
filters []goldsmith.Filter
|
||||
}
|
||||
|
||||
func (*operatorOr) Name() string {
|
||||
return "operator"
|
||||
}
|
||||
|
||||
func (self *operatorOr) Accept(file *goldsmith.File) bool {
|
||||
for _, filter := range self.filters {
|
||||
if filter.Accept(file) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
109
filters/operator/operator_test.go
Normal file
109
filters/operator/operator_test.go
Normal file
@ -0,0 +1,109 @@
|
||||
package operator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.foosoft.net/alex/goldsmith"
|
||||
"git.foosoft.net/alex/goldsmith/filters/condition"
|
||||
"git.foosoft.net/alex/goldsmith/harness"
|
||||
)
|
||||
|
||||
func TestAndFalse(t *testing.T) {
|
||||
harness.ValidateCase(
|
||||
t,
|
||||
"and_false",
|
||||
func(gs *goldsmith.Goldsmith) {
|
||||
gs.FilterPush(And(condition.New(false)))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestAndFalseTrue(t *testing.T) {
|
||||
harness.ValidateCase(
|
||||
t,
|
||||
"and_false_true",
|
||||
func(gs *goldsmith.Goldsmith) {
|
||||
gs.FilterPush(And(condition.New(false), condition.New(true)))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestAndTrueFalse(t *testing.T) {
|
||||
harness.ValidateCase(
|
||||
t,
|
||||
"and_true_false",
|
||||
func(gs *goldsmith.Goldsmith) {
|
||||
gs.FilterPush(And(condition.New(true), condition.New(false)))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestAndTrue(t *testing.T) {
|
||||
harness.ValidateCase(
|
||||
t,
|
||||
"and_true",
|
||||
func(gs *goldsmith.Goldsmith) {
|
||||
gs.FilterPush(And(condition.New(true)))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestOrFalse(t *testing.T) {
|
||||
harness.ValidateCase(
|
||||
t,
|
||||
"or_false",
|
||||
func(gs *goldsmith.Goldsmith) {
|
||||
gs.FilterPush(Or(condition.New(false)))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestOrFalseTrue(t *testing.T) {
|
||||
harness.ValidateCase(
|
||||
t,
|
||||
"or_false_true",
|
||||
func(gs *goldsmith.Goldsmith) {
|
||||
gs.FilterPush(Or(condition.New(false), condition.New(true)))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestOrTrueFalse(t *testing.T) {
|
||||
harness.ValidateCase(
|
||||
t,
|
||||
"or_true_false",
|
||||
func(gs *goldsmith.Goldsmith) {
|
||||
gs.FilterPush(Or(condition.New(true), condition.New(false)))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestOrTrue(t *testing.T) {
|
||||
harness.ValidateCase(
|
||||
t,
|
||||
"or_true",
|
||||
func(gs *goldsmith.Goldsmith) {
|
||||
gs.FilterPush(Or(condition.New(true)))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestNotFalse(t *testing.T) {
|
||||
harness.ValidateCase(
|
||||
t,
|
||||
"not_false",
|
||||
func(gs *goldsmith.Goldsmith) {
|
||||
gs.FilterPush(Not(condition.New(false)))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestNotTrue(t *testing.T) {
|
||||
harness.ValidateCase(
|
||||
t,
|
||||
"not_true",
|
||||
func(gs *goldsmith.Goldsmith) {
|
||||
gs.FilterPush(Not(condition.New(true)))
|
||||
},
|
||||
)
|
||||
}
|
0
filters/operator/testdata/and_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_false_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_false_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_false_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_false_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_true/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_true/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_true/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_true/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_true_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_true_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_true_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_true_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/not_false/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/not_false/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/not_false/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/not_false/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/not_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/not_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/not_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/not_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/not_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/not_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/not_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/not_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_false_true/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_false_true/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_false_true/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_false_true/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_false_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_false_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_false_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_false_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_true/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_true/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_true/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_true/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_true_false/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_true_false/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_true_false/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_true_false/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_true_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_true_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_true_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_true_false/source/file_2.txt
vendored
Normal file
0
filters/wildcard/testdata/reference/child/test_5.txt
vendored
Normal file
0
filters/wildcard/testdata/reference/child/test_5.txt
vendored
Normal file
0
filters/wildcard/testdata/reference/test_1.txt
vendored
Normal file
0
filters/wildcard/testdata/reference/test_1.txt
vendored
Normal file
0
filters/wildcard/testdata/reference/test_2.md
vendored
Normal file
0
filters/wildcard/testdata/reference/test_2.md
vendored
Normal file
0
filters/wildcard/testdata/source/child/test_3.json
vendored
Normal file
0
filters/wildcard/testdata/source/child/test_3.json
vendored
Normal file
0
filters/wildcard/testdata/source/child/test_5.txt
vendored
Normal file
0
filters/wildcard/testdata/source/child/test_5.txt
vendored
Normal file
0
filters/wildcard/testdata/source/child/test_7.md
vendored
Normal file
0
filters/wildcard/testdata/source/child/test_7.md
vendored
Normal file
0
filters/wildcard/testdata/source/child/test_8.xml
vendored
Normal file
0
filters/wildcard/testdata/source/child/test_8.xml
vendored
Normal file
0
filters/wildcard/testdata/source/test_1.txt
vendored
Normal file
0
filters/wildcard/testdata/source/test_1.txt
vendored
Normal file
0
filters/wildcard/testdata/source/test_2.md
vendored
Normal file
0
filters/wildcard/testdata/source/test_2.md
vendored
Normal file
0
filters/wildcard/testdata/source/test_3.json
vendored
Normal file
0
filters/wildcard/testdata/source/test_3.json
vendored
Normal file
0
filters/wildcard/testdata/source/test_4.xml
vendored
Normal file
0
filters/wildcard/testdata/source/test_4.xml
vendored
Normal file
47
filters/wildcard/wildcard.go
Normal file
47
filters/wildcard/wildcard.go
Normal file
@ -0,0 +1,47 @@
|
||||
package wildcard
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.foosoft.net/alex/goldsmith"
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
)
|
||||
|
||||
type Wildcard struct {
|
||||
wildcards []string
|
||||
caseSensitive bool
|
||||
}
|
||||
|
||||
func New(wildcards ...string) *Wildcard {
|
||||
return &Wildcard{wildcards: wildcards}
|
||||
}
|
||||
|
||||
func (self *Wildcard) CaseSensitive(caseSensitive bool) *Wildcard {
|
||||
self.caseSensitive = caseSensitive
|
||||
return self
|
||||
}
|
||||
|
||||
func (*Wildcard) Name() string {
|
||||
return "wildcard"
|
||||
}
|
||||
|
||||
func (self *Wildcard) Accept(file *goldsmith.File) bool {
|
||||
filePath := self.adjustCase(file.Path())
|
||||
|
||||
for _, wildcard := range self.wildcards {
|
||||
wildcard = self.adjustCase(wildcard)
|
||||
if matched, _ := doublestar.PathMatch(wildcard, filePath); matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (self *Wildcard) adjustCase(str string) string {
|
||||
if self.caseSensitive {
|
||||
return str
|
||||
}
|
||||
|
||||
return strings.ToLower(str)
|
||||
}
|
17
filters/wildcard/wildcard_test.go
Normal file
17
filters/wildcard/wildcard_test.go
Normal file
@ -0,0 +1,17 @@
|
||||
package wildcard
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.foosoft.net/alex/goldsmith"
|
||||
"git.foosoft.net/alex/goldsmith/harness"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
harness.Validate(
|
||||
t,
|
||||
func(gs *goldsmith.Goldsmith) {
|
||||
gs.FilterPush(New("**/*.txt", "*.md"))
|
||||
},
|
||||
)
|
||||
}
|
24
go.mod
24
go.mod
@ -1,3 +1,25 @@
|
||||
module git.foosoft.net/alex/goldsmith
|
||||
|
||||
go 1.13
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/alecthomas/chroma v0.10.0
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/gorilla/feeds v1.1.2
|
||||
github.com/tdewolff/minify/v2 v2.20.17
|
||||
github.com/yuin/goldmark v1.7.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.7.12 // indirect
|
||||
golang.org/x/image v0.15.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
)
|
||||
|
94
go.sum
Normal file
94
go.sum
Normal file
@ -0,0 +1,94 @@
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gorilla/feeds v1.1.2 h1:pxzZ5PD3RJdhFH2FsJJ4x6PqMqbgFk1+Vez4XWBW8Iw=
|
||||
github.com/gorilla/feeds v1.1.2/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tdewolff/minify/v2 v2.20.17 h1:zGqEDhspr3XjSrQI/56vw9IdAhLAaKTLXWnDBsxNVt8=
|
||||
github.com/tdewolff/minify/v2 v2.20.17/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM=
|
||||
github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ=
|
||||
github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
|
||||
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
|
||||
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
|
||||
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
110
harness/harness.go
Normal file
110
harness/harness.go
Normal file
@ -0,0 +1,110 @@
|
||||
// Package harness provides a simple way to test goldsmith plugins and filters.
|
||||
// It executes a goldsmith chain on provided "source" data and compares the
|
||||
// generated "target" resuts with the known to be good "reference" data.
|
||||
package harness
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.foosoft.net/alex/goldsmith"
|
||||
)
|
||||
|
||||
// Stager callback function is used to set up a goldsmith chain.
|
||||
type Stager func(gs *goldsmith.Goldsmith)
|
||||
|
||||
// Validate enables validation of a single, unnamed case (test data is stored in "testdata").
|
||||
func Validate(t *testing.T, stager Stager) {
|
||||
ValidateCase(t, "", stager)
|
||||
}
|
||||
|
||||
// ValidateCase enables enables of a single, named case (test data is stored in "testdata/caseName").
|
||||
func ValidateCase(t *testing.T, caseName string, stager Stager) {
|
||||
var (
|
||||
caseDir = filepath.Join("testdata", caseName)
|
||||
sourceDir = filepath.Join(caseDir, "source")
|
||||
targetDir = filepath.Join(caseDir, "target")
|
||||
cacheDir = filepath.Join(caseDir, "cache")
|
||||
referenceDir = filepath.Join(caseDir, "reference")
|
||||
)
|
||||
|
||||
if errs := validate(sourceDir, targetDir, cacheDir, referenceDir, stager); len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func validate(sourceDir, targetDir, cacheDir, referenceDir string, stager Stager) []error {
|
||||
if err := os.RemoveAll(targetDir); err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(cacheDir); err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
defer os.RemoveAll(cacheDir)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
if errs := execute(sourceDir, targetDir, cacheDir, stager); errs != nil {
|
||||
return errs
|
||||
}
|
||||
|
||||
if hashDirState(targetDir) != hashDirState(referenceDir) {
|
||||
return []error{errors.New("directory contents do not match")}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func execute(sourceDir, targetDir, cacheDir string, stager Stager) []error {
|
||||
gs := goldsmith.Begin(sourceDir).Cache(cacheDir).Clean(true)
|
||||
stager(gs)
|
||||
return gs.End(targetDir)
|
||||
}
|
||||
|
||||
func hashDirState(dir string) uint32 {
|
||||
hasher := crc32.NewIEEE()
|
||||
|
||||
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(dir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
summary := fmt.Sprintf("%s %t", relPath, info.IsDir())
|
||||
if _, err := hasher.Write([]byte(summary)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
fp, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
if _, err := io.Copy(hasher, fp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return hasher.Sum32()
|
||||
}
|
114
plugins/absolute/absolute.go
Normal file
114
plugins/absolute/absolute.go
Normal file
@ -0,0 +1,114 @@
|
||||
// Package absolute converts relative file references in HTML documents to
|
||||
// absolute paths. This is useful when working with plugins like "layout" and
|
||||
// "collection", which can render a page’s content from the context of a
|
||||
// different directory (imagine an index page showing inline previews of blog
|
||||
// posts). This plugin makes it easy to fix incorrect relative file references
|
||||
// by making sure all paths are absolute before content is featured on other
|
||||
// sections of your site.
|
||||
package absolute
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"git.foosoft.net/alex/goldsmith"
|
||||
"git.foosoft.net/alex/goldsmith/filters/wildcard"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
// Absolute chainable plugin context.
|
||||
type Absolute struct {
|
||||
attributes []string
|
||||
baseUrl *url.URL
|
||||
}
|
||||
|
||||
// New creates absolute new instance of the Absolute plugin.
|
||||
func New() *Absolute {
|
||||
return &Absolute{attributes: []string{"href", "src"}}
|
||||
}
|
||||
|
||||
// Attributes sets the attributes which are scanned for relative URLs (default: "href", "src").
|
||||
func (self *Absolute) Attributes(attributes ...string) *Absolute {
|
||||
self.attributes = attributes
|
||||
return self
|
||||
}
|
||||
|
||||
// BaseUrl sets the base URL which is prepended to absolute-converted relative paths.
|
||||
func (self *Absolute) BaseUrl(baseUrl string) *Absolute {
|
||||
self.baseUrl, _ = url.Parse(baseUrl)
|
||||
return self
|
||||
}
|
||||
|
||||
func (*Absolute) Name() string {
|
||||
return "absolute"
|
||||
}
|
||||
|
||||
func (*Absolute) Initialize(context *goldsmith.Context) error {
|
||||
context.Filter(wildcard.New("**/*.html", "**/*.htm"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Absolute) Process(context *goldsmith.Context, inputFile *goldsmith.File) error {
|
||||
if outputFile := context.RetrieveCachedFile(inputFile.Path(), inputFile); outputFile != nil {
|
||||
outputFile.CopyProps(inputFile)
|
||||
context.DispatchFile(outputFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
fileUrl, err := url.Parse(inputFile.Path())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(inputFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, attribute := range self.attributes {
|
||||
cssPath := fmt.Sprintf("*[%s]", attribute)
|
||||
doc.Find(cssPath).Each(func(index int, selection *goquery.Selection) {
|
||||
value, exists := selection.Attr(attribute)
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
currUrl, err := url.Parse(value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if currUrl.IsAbs() {
|
||||
return
|
||||
}
|
||||
|
||||
currUrl = fileUrl.ResolveReference(currUrl)
|
||||
if self.baseUrl != nil {
|
||||
rebasedUrl := *self.baseUrl
|
||||
rebasedUrl.Path = path.Join(rebasedUrl.Path, currUrl.Path)
|
||||
rebasedUrl.Fragment = currUrl.Fragment
|
||||
rebasedUrl.RawFragment = currUrl.RawFragment
|
||||
rebasedUrl.RawQuery = currUrl.RawQuery
|
||||
currUrl = &rebasedUrl
|
||||
}
|
||||
|
||||
selection.SetAttr(attribute, currUrl.String())
|
||||
})
|
||||
}
|
||||
|
||||
html, err := doc.Html()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputFile, err := context.CreateFileFromReader(inputFile.Path(), bytes.NewReader([]byte(html)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputFile.CopyProps(inputFile)
|
||||
context.DispatchAndCacheFile(outputFile, inputFile)
|
||||
return nil
|
||||
}
|
17
plugins/absolute/absolute_test.go
Normal file
17
plugins/absolute/absolute_test.go
Normal file
@ -0,0 +1,17 @@
|
||||
package absolute
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.foosoft.net/alex/goldsmith"
|
||||
"git.foosoft.net/alex/goldsmith/harness"
|
||||
)
|
||||
|
||||
func Test(self *testing.T) {
|
||||
harness.Validate(
|
||||
self,
|
||||
func(gs *goldsmith.Goldsmith) {
|
||||
gs.Chain(New().BaseUrl("https://foosoft.net"))
|
||||
},
|
||||
)
|
||||
}
|
7
plugins/absolute/testdata/reference/dir/index.html
vendored
Normal file
7
plugins/absolute/testdata/reference/dir/index.html
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<html><head></head><body>
|
||||
<a href="https://foosoft.net/">Relative link</a>
|
||||
<a href="https://foosoft.net">Absolute link</a>
|
||||
<a href="https://www.example.com/dir/index.html">External link</a>
|
||||
|
||||
|
||||
</body></html>
|
7
plugins/absolute/testdata/reference/index.html
vendored
Normal file
7
plugins/absolute/testdata/reference/index.html
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<html><head></head><body>
|
||||
<a href="https://foosoft.net/dir/index.html#anchor">Relative link</a>
|
||||
<a href="https://foosoft.net/index.html?query">Absolute link</a>
|
||||
<a href="https://www.example.com/dir/index.html">External link</a>
|
||||
|
||||
|
||||
</body></html>
|
7
plugins/absolute/testdata/source/dir/index.html
vendored
Normal file
7
plugins/absolute/testdata/source/dir/index.html
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<a href="../">Relative link</a>
|
||||
<a href="https://foosoft.net">Absolute link</a>
|
||||
<a href="https://www.example.com/dir/index.html">External link</a>
|
||||
</body>
|
||||
</html>
|
7
plugins/absolute/testdata/source/index.html
vendored
Normal file
7
plugins/absolute/testdata/source/index.html
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<a href="dir/index.html#anchor">Relative link</a>
|
||||
<a href="/index.html?query">Absolute link</a>
|
||||
<a href="https://www.example.com/dir/index.html">External link</a>
|
||||
</body>
|
||||
</html>
|
131
plugins/breadcrumbs/breadcrumbs.go
Normal file
131
plugins/breadcrumbs/breadcrumbs.go
Normal file
@ -0,0 +1,131 @@
|
||||
// Package breadcrumbs generates metadata required to enable breadcrumb
|
||||
// navigation. This is particularly helpful for sites that have deep
|
||||
// hierarchies which may be otherwise confusing to visitors.
|
||||
package breadcrumbs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"git.foosoft.net/alex/goldsmith"
|
||||
"git.foosoft.net/alex/goldsmith/filters/wildcard"
|
||||
)
|
||||
|
||||
// Crumb provides organizational information about this node and ones before it.
|
||||
type Crumb struct {
|
||||
Ancestors []*Node
|
||||
Node *Node
|
||||
}
|
||||
|
||||
// Node represents information about a specific file in the site's structure.
|
||||
type Node struct {
|
||||
File *goldsmith.File
|
||||
Parent *Node
|
||||
Children []*Node
|
||||
|
||||
parentName string
|
||||
}
|
||||
|
||||
// Breadcrumbs chainable plugin context.
|
||||
type Breadcrumbs struct {
|
||||
nameKey string
|
||||
parentKey string
|
||||
crumbsKey string
|
||||
|
||||
allNodes []*Node
|
||||
namedNodes map[string]*Node
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// New creates a new instance of the Breadcrumbs plugin.
|
||||
func New() *Breadcrumbs {
|
||||
return &Breadcrumbs{
|
||||
nameKey: "CrumbName",
|
||||
parentKey: "CrumbParent",
|
||||
crumbsKey: "Crumbs",
|
||||
namedNodes: make(map[string]*Node),
|
||||
}
|
||||
}
|
||||
|
||||
// NameKey sets the metadata key used to access the crumb name (default: "CrumbName").
|
||||
// Crumb names must be globally unique within any given website.
|
||||
func (self *Breadcrumbs) NameKey(key string) *Breadcrumbs {
|
||||
self.nameKey = key
|
||||
return self
|
||||
}
|
||||
|
||||
// ParentKey sets the metadata key used to access the parent name (default: "CrumbParent").
|
||||
func (self *Breadcrumbs) ParentKey(key string) *Breadcrumbs {
|
||||
self.parentKey = key
|
||||
return self
|
||||
}
|
||||
|
||||
// CrumbsKey sets the metadata key used to store information about crumbs (default: "Crumbs").
|
||||
func (self *Breadcrumbs) CrumbsKey(key string) *Breadcrumbs {
|
||||
self.crumbsKey = key
|
||||
return self
|
||||
}
|
||||
|
||||
func (*Breadcrumbs) Name() string {
|
||||
return "breadcrumbs"
|
||||
}
|
||||
|
||||
func (*Breadcrumbs) Initialize(context *goldsmith.Context) error {
|
||||
context.Filter(wildcard.New("**/*.html", "**/*.htm"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Breadcrumbs) Process(context *goldsmith.Context, inputFile *goldsmith.File) error {
|
||||
var parentNameStr string
|
||||
if parentName, ok := inputFile.Prop(self.parentKey); ok {
|
||||
parentNameStr, _ = parentName.(string)
|
||||
}
|
||||
|
||||
var nodeNameStr string
|
||||
if nodeName, ok := inputFile.Prop(self.nameKey); ok {
|
||||
nodeNameStr, _ = nodeName.(string)
|
||||
}
|
||||
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
node := &Node{File: inputFile, parentName: parentNameStr}
|
||||
self.allNodes = append(self.allNodes, node)
|
||||
|
||||
if len(nodeNameStr) > 0 {
|
||||
if _, ok := self.namedNodes[nodeNameStr]; ok {
|
||||
return fmt.Errorf("duplicate node: %s", nodeNameStr)
|
||||
}
|
||||
|
||||
self.namedNodes[nodeNameStr] = node
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Breadcrumbs) Finalize(context *goldsmith.Context) error {
|
||||
for _, node := range self.allNodes {
|
||||
if len(node.parentName) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if parentNode, ok := self.namedNodes[node.parentName]; ok {
|
||||
parentNode.Children = append(parentNode.Children, node)
|
||||
node.Parent = parentNode
|
||||
} else {
|
||||
return fmt.Errorf("undefined parent: %s", node.parentName)
|
||||
}
|
||||
}
|
||||
|
||||
for _, node := range self.allNodes {
|
||||
var ancestors []*Node
|
||||
for currentNode := node.Parent; currentNode != nil; currentNode = currentNode.Parent {
|
||||
ancestors = append([]*Node{currentNode}, ancestors...)
|
||||
}
|
||||
|
||||
node.File.SetProp(self.crumbsKey, Crumb{ancestors, node})
|
||||
context.DispatchFile(node.File)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
22
plugins/breadcrumbs/breadcrumbs_test.go
Normal file
22
plugins/breadcrumbs/breadcrumbs_test.go
Normal file
@ -0,0 +1,22 @@
|
||||
package breadcrumbs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.foosoft.net/alex/goldsmith"
|
||||
"git.foosoft.net/alex/goldsmith/harness"
|
||||
"git.foosoft.net/alex/goldsmith/plugins/frontmatter"
|
||||
"git.foosoft.net/alex/goldsmith/plugins/layout"
|
||||
)
|
||||
|
||||
func Test(self *testing.T) {
|
||||
harness.Validate(
|
||||
self,
|
||||
func(gs *goldsmith.Goldsmith) {
|
||||
gs.
|
||||
Chain(frontmatter.New()).
|
||||
Chain(New()).
|
||||
Chain(layout.New())
|
||||
},
|
||||
)
|
||||
}
|
26
plugins/breadcrumbs/testdata/reference/child_1.html
vendored
Normal file
26
plugins/breadcrumbs/testdata/reference/child_1.html
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<h1>Child 1</h1>
|
||||
<ul>
|
||||
<li><a href="child_1.html">Child 1</a></li>
|
||||
<li><a href="child_2.html">Child 2</a></li>
|
||||
<li><a href="child_3.html">Child 3</a></li>
|
||||
<li><a href="child_4.html">Child 4</a></li>
|
||||
<li><a href="parent_1.html">Parent 1</a></li>
|
||||
<li><a href="parent_2.html">Parent 2</a></li>
|
||||
<li><a href="root_1.html">Root 1</a></li>
|
||||
<li><a href="root_2.html">Root 2</a></li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
|
||||
<a href="root_1.html" class="breadcrumb-item">Root 1</a> >
|
||||
|
||||
<a href="parent_1.html" class="breadcrumb-item">Parent 1</a> >
|
||||
|
||||
<span class="breadcrumb-item active">Child 1</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
26
plugins/breadcrumbs/testdata/reference/child_2.html
vendored
Normal file
26
plugins/breadcrumbs/testdata/reference/child_2.html
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<h1>Child 2</h1>
|
||||
<ul>
|
||||
<li><a href="child_1.html">Child 1</a></li>
|
||||
<li><a href="child_2.html">Child 2</a></li>
|
||||
<li><a href="child_3.html">Child 3</a></li>
|
||||
<li><a href="child_4.html">Child 4</a></li>
|
||||
<li><a href="parent_1.html">Parent 1</a></li>
|
||||
<li><a href="parent_2.html">Parent 2</a></li>
|
||||
<li><a href="root_1.html">Root 1</a></li>
|
||||
<li><a href="root_2.html">Root 2</a></li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
|
||||
<a href="root_1.html" class="breadcrumb-item">Root 1</a> >
|
||||
|
||||
<a href="parent_1.html" class="breadcrumb-item">Parent 1</a> >
|
||||
|
||||
<span class="breadcrumb-item active">Child 2</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
26
plugins/breadcrumbs/testdata/reference/child_3.html
vendored
Normal file
26
plugins/breadcrumbs/testdata/reference/child_3.html
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<h1>Child 3</h1>
|
||||
<ul>
|
||||
<li><a href="child_1.html">Child 1</a></li>
|
||||
<li><a href="child_2.html">Child 2</a></li>
|
||||
<li><a href="child_3.html">Child 3</a></li>
|
||||
<li><a href="child_4.html">Child 4</a></li>
|
||||
<li><a href="parent_1.html">Parent 1</a></li>
|
||||
<li><a href="parent_2.html">Parent 2</a></li>
|
||||
<li><a href="root_1.html">Root 1</a></li>
|
||||
<li><a href="root_2.html">Root 2</a></li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
|
||||
<a href="root_1.html" class="breadcrumb-item">Root 1</a> >
|
||||
|
||||
<a href="parent_2.html" class="breadcrumb-item">Parent 2</a> >
|
||||
|
||||
<span class="breadcrumb-item active">Child 3</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
26
plugins/breadcrumbs/testdata/reference/child_4.html
vendored
Normal file
26
plugins/breadcrumbs/testdata/reference/child_4.html
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<h1>Child 4</h1>
|
||||
<ul>
|
||||
<li><a href="child_1.html">Child 1</a></li>
|
||||
<li><a href="child_2.html">Child 2</a></li>
|
||||
<li><a href="child_3.html">Child 3</a></li>
|
||||
<li><a href="child_4.html">Child 4</a></li>
|
||||
<li><a href="parent_1.html">Parent 1</a></li>
|
||||
<li><a href="parent_2.html">Parent 2</a></li>
|
||||
<li><a href="root_1.html">Root 1</a></li>
|
||||
<li><a href="root_2.html">Root 2</a></li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
|
||||
<a href="root_1.html" class="breadcrumb-item">Root 1</a> >
|
||||
|
||||
<a href="parent_2.html" class="breadcrumb-item">Parent 2</a> >
|
||||
|
||||
<span class="breadcrumb-item active">Child 4</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
24
plugins/breadcrumbs/testdata/reference/parent_1.html
vendored
Normal file
24
plugins/breadcrumbs/testdata/reference/parent_1.html
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<h1>Parent 1</h1>
|
||||
<ul>
|
||||
<li><a href="child_1.html">Child 1</a></li>
|
||||
<li><a href="child_2.html">Child 2</a></li>
|
||||
<li><a href="child_3.html">Child 3</a></li>
|
||||
<li><a href="child_4.html">Child 4</a></li>
|
||||
<li><a href="parent_1.html">Parent 1</a></li>
|
||||
<li><a href="parent_2.html">Parent 2</a></li>
|
||||
<li><a href="root_1.html">Root 1</a></li>
|
||||
<li><a href="root_2.html">Root 2</a></li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
|
||||
<a href="root_1.html" class="breadcrumb-item">Root 1</a> >
|
||||
|
||||
<span class="breadcrumb-item active">Parent 1</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
24
plugins/breadcrumbs/testdata/reference/parent_2.html
vendored
Normal file
24
plugins/breadcrumbs/testdata/reference/parent_2.html
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<h1>Parent 2</h1>
|
||||
<ul>
|
||||
<li><a href="child_1.html">Child 1</a></li>
|
||||
<li><a href="child_2.html">Child 2</a></li>
|
||||
<li><a href="child_3.html">Child 3</a></li>
|
||||
<li><a href="child_4.html">Child 4</a></li>
|
||||
<li><a href="parent_1.html">Parent 1</a></li>
|
||||
<li><a href="parent_2.html">Parent 2</a></li>
|
||||
<li><a href="root_1.html">Root 1</a></li>
|
||||
<li><a href="root_2.html">Root 2</a></li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
|
||||
<a href="root_1.html" class="breadcrumb-item">Root 1</a> >
|
||||
|
||||
<span class="breadcrumb-item active">Parent 2</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
17
plugins/breadcrumbs/testdata/reference/root_1.html
vendored
Normal file
17
plugins/breadcrumbs/testdata/reference/root_1.html
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<h1>Root 1</h1>
|
||||
<ul>
|
||||
<li><a href="child_1.html">Child 1</a></li>
|
||||
<li><a href="child_2.html">Child 2</a></li>
|
||||
<li><a href="child_3.html">Child 3</a></li>
|
||||
<li><a href="child_4.html">Child 4</a></li>
|
||||
<li><a href="parent_1.html">Parent 1</a></li>
|
||||
<li><a href="parent_2.html">Parent 2</a></li>
|
||||
<li><a href="root_1.html">Root 1</a></li>
|
||||
<li><a href="root_2.html">Root 2</a></li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
17
plugins/breadcrumbs/testdata/reference/root_2.html
vendored
Normal file
17
plugins/breadcrumbs/testdata/reference/root_2.html
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<h1>Root 2</h1>
|
||||
<ul>
|
||||
<li><a href="child_1.html">Child 1</a></li>
|
||||
<li><a href="child_2.html">Child 2</a></li>
|
||||
<li><a href="child_3.html">Child 3</a></li>
|
||||
<li><a href="child_4.html">Child 4</a></li>
|
||||
<li><a href="parent_1.html">Parent 1</a></li>
|
||||
<li><a href="parent_2.html">Parent 2</a></li>
|
||||
<li><a href="root_1.html">Root 1</a></li>
|
||||
<li><a href="root_2.html">Root 2</a></li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|