Add components

This commit is contained in:
Alex Yatskov 2024-02-16 22:35:49 -08:00
parent 972c4d81f5
commit 648ab12c5b
258 changed files with 6295 additions and 1 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target

114
devserver/devserver.go Normal file
View 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)
}
}
}

View 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
}

View 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))
},
)
}

View File

View File

View File

View File

View File

View File

View 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
}

View 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)))
},
)
}

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View 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)
}

View 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
View File

@ -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
View 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
View 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()
}

View 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 pages 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
}

View 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"))
},
)
}

View 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>

View 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>

View 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>

View 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>

View 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
}

View 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())
},
)
}

View 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> &gt;
<a href="parent_1.html" class="breadcrumb-item">Parent 1</a> &gt;
<span class="breadcrumb-item active">Child 1</span>
</div>
</body>
</html>

View 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> &gt;
<a href="parent_1.html" class="breadcrumb-item">Parent 1</a> &gt;
<span class="breadcrumb-item active">Child 2</span>
</div>
</body>
</html>

View 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> &gt;
<a href="parent_2.html" class="breadcrumb-item">Parent 2</a> &gt;
<span class="breadcrumb-item active">Child 3</span>
</div>
</body>
</html>

View 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> &gt;
<a href="parent_2.html" class="breadcrumb-item">Parent 2</a> &gt;
<span class="breadcrumb-item active">Child 4</span>
</div>
</body>
</html>

View 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> &gt;
<span class="breadcrumb-item active">Parent 1</span>
</div>
</body>
</html>

View 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> &gt;
<span class="breadcrumb-item active">Parent 2</span>
</div>
</body>
</html>

View 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>

View 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>