Compare commits
23 Commits
e4eacea3dd
...
f5cee22324
Author | SHA1 | Date | |
---|---|---|---|
f5cee22324 | |||
8ee5f529d8 | |||
5c547dbf68 | |||
dcd19b19a6 | |||
7acc65170f | |||
cd2e515141 | |||
889397b9c5 | |||
22ffaae954 | |||
e24e892773 | |||
9ba1c7cfeb | |||
05f7ee5280 | |||
ab0c1c53d2 | |||
01fac8cb96 | |||
8aee72f2c8 | |||
771b40e82e | |||
c0c940156f | |||
2f95fdba2d | |||
55ed95ddbb | |||
25d629af10 | |||
ea28b5c8cf | |||
648ab12c5b | |||
972c4d81f5 | |||
f0259bcbe9 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
target
|
144
README.md
144
README.md
@ -14,42 +14,42 @@ to understand, it is often best to learn by example:
|
||||
1. Start by copying files from a source directory to a destination directory (the simplest possible use case):
|
||||
|
||||
```go
|
||||
goldsmith.
|
||||
Begin(srcDir). // read files from srcDir
|
||||
End(dstDir) // write files to dstDir
|
||||
var gs goldsmith.Goldsmith
|
||||
gs.Begin(srcDir). // read files from srcDir
|
||||
End(dstDir) // write files to dstDir
|
||||
```
|
||||
|
||||
2. Now let's convert any Markdown files to HTML fragments (while still copying the rest), using the
|
||||
[Markdown](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/markdown) plugin:
|
||||
[Markdown](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/markdown) plugin:
|
||||
|
||||
```go
|
||||
goldsmith.
|
||||
Begin(srcDir). // read files from srcDir
|
||||
var gs goldsmith.Goldsmith
|
||||
gs.Begin(srcDir). // read files from srcDir
|
||||
Chain(markdown.New()). // convert *.md files to *.html files
|
||||
End(dstDir) // write files to dstDir
|
||||
```
|
||||
|
||||
3. If we have any
|
||||
[front matter](https://raw.githubusercontent.com/FooSoft/goldsmith-samples/master/basic/content/index.md) in our
|
||||
Markdown files, we need to extract it using the,
|
||||
[FrontMatter](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/frontmatter) plugin:
|
||||
3. If we have any [front
|
||||
matter](https://git.foosoft.net/alex/goldsmith-samples/raw/branch/master/basic/content/index.md) in our Markdown
|
||||
files, we need to extract it using the,
|
||||
[FrontMatter](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/frontmatter) plugin:
|
||||
|
||||
```go
|
||||
goldsmith.
|
||||
Begin(srcDir). // read files from srcDir
|
||||
var gs goldsmith.Goldsmith
|
||||
gs.Begin(srcDir). // read files from srcDir
|
||||
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
|
||||
Chain(markdown.New()). // convert *.md files to *.html files
|
||||
End(dstDir) // write files to dstDir
|
||||
```
|
||||
|
||||
4. Next, we should run our barebones HTML through a
|
||||
[template](https://raw.githubusercontent.com/FooSoft/goldsmith-samples/master/basic/content/layouts/basic.gohtml) to
|
||||
[template](https://git.foosoft.net/alex/goldsmith-samples/raw/branch/master/basic/content/layouts/basic.gohtml) to
|
||||
add elements like a header, footer, or a menu; for this we can use the
|
||||
[Layout](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/frontmatter) plugin:
|
||||
[Layout](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/frontmatter) plugin:
|
||||
|
||||
```go
|
||||
goldsmith.
|
||||
Begin(srcDir). // read files from srcDir
|
||||
var gs goldsmith.Goldsmith
|
||||
gs.Begin(srcDir). // read files from srcDir
|
||||
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
|
||||
Chain(markdown.New()). // convert *.md files to *.html files
|
||||
Chain(layout.New()). // apply *.gohtml templates to *.html files
|
||||
@ -58,11 +58,11 @@ to understand, it is often best to learn by example:
|
||||
|
||||
5. Now, let's [minify](https://en.wikipedia.org/wiki/Minification_(programming)) our files to reduce data transfer and
|
||||
load times for our site's visitors using the
|
||||
[Minify](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/minify) plugin:
|
||||
[Minify](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/minify) plugin:
|
||||
|
||||
```go
|
||||
goldsmith.
|
||||
Begin(srcDir). // read files from srcDir
|
||||
var gs goldsmith.Goldsmith
|
||||
gs.Begin(srcDir). // read files from srcDir
|
||||
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
|
||||
Chain(markdown.New()). // convert *.md files to *.html files
|
||||
Chain(layout.New()). // apply *.gohtml templates to *.html files
|
||||
@ -71,12 +71,12 @@ to understand, it is often best to learn by example:
|
||||
```
|
||||
|
||||
6. Debugging problems in minified code can be tricky, so let's use the
|
||||
[Condition](https://godoc.org/git.foosoft.net/alex/goldsmith-components/filters/condition) filter to make
|
||||
minification occur only when we are ready for distribution.
|
||||
[Condition](https://godoc.org/git.foosoft.net/alex/goldsmith/filters/condition) filter to make minification occur
|
||||
only when we are ready for distribution.
|
||||
|
||||
```go
|
||||
goldsmith.
|
||||
Begin(srcDir). // read files from srcDir
|
||||
var gs goldsmith.Goldsmith
|
||||
gs.Begin(srcDir). // read files from srcDir
|
||||
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
|
||||
Chain(markdown.New()). // convert *.md files to *.html files
|
||||
Chain(layout.New()). // apply *.gohtml templates to *.html files
|
||||
@ -87,8 +87,8 @@ to understand, it is often best to learn by example:
|
||||
```
|
||||
|
||||
7. Now that we have all of our plugins chained up, let's look at a complete example which uses
|
||||
[DevServer](https://godoc.org/git.foosoft.net/alex/goldsmith-components/devserver) to bootstrap a complete
|
||||
development sever which automatically rebuilds the site whenever source files are updated.
|
||||
[DevServer](https://godoc.org/git.foosoft.net/alex/goldsmith/devserver) to bootstrap a complete development sever
|
||||
which automatically rebuilds the site whenever source files are updated.
|
||||
|
||||
```go
|
||||
package main
|
||||
@ -98,12 +98,12 @@ to understand, it is often best to learn by example:
|
||||
"log"
|
||||
|
||||
"git.foosoft.net/alex/goldsmith"
|
||||
"git.foosoft.net/alex/goldsmith-components/devserver"
|
||||
"git.foosoft.net/alex/goldsmith-components/filters/condition"
|
||||
"git.foosoft.net/alex/goldsmith-components/plugins/frontmatter"
|
||||
"git.foosoft.net/alex/goldsmith-components/plugins/layout"
|
||||
"git.foosoft.net/alex/goldsmith-components/plugins/markdown"
|
||||
"git.foosoft.net/alex/goldsmith-components/plugins/minify"
|
||||
"git.foosoft.net/alex/goldsmith/devserver"
|
||||
"git.foosoft.net/alex/goldsmith/filters/condition"
|
||||
"git.foosoft.net/alex/goldsmith/plugins/frontmatter"
|
||||
"git.foosoft.net/alex/goldsmith/plugins/layout"
|
||||
"git.foosoft.net/alex/goldsmith/plugins/markdown"
|
||||
"git.foosoft.net/alex/goldsmith/plugins/minify"
|
||||
)
|
||||
|
||||
type builder struct {
|
||||
@ -111,8 +111,8 @@ to understand, it is often best to learn by example:
|
||||
}
|
||||
|
||||
func (b *builder) Build(srcDir, dstDir, cacheDir string) {
|
||||
errs := goldsmith.
|
||||
Begin(srcDir). // read files from srcDir
|
||||
var gs goldsmith.Goldsmith
|
||||
errs := gs.Begin(srcDir). // read files from srcDir
|
||||
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
|
||||
Chain(markdown.New()). // convert *.md files to *.html files
|
||||
Chain(layout.New()). // apply *.gohtml templates to *.html files
|
||||
@ -139,12 +139,9 @@ to understand, it is often best to learn by example:
|
||||
|
||||
Below are some examples of Goldsmith usage which can used to base your site on:
|
||||
|
||||
* [Basic Sample](https://git.foosoft.net/alex/goldsmith-samples/src/branch/master/basic): a great starting point, this
|
||||
is the sample site from the tutorial.
|
||||
* [Bootstrap Sample](https://git.foosoft.net/alex/goldsmith-samples/src/branch/master/bootstrap): a slightly more
|
||||
advanced sample using [Bootstrap](https://getbootstrap.com/).
|
||||
* [FooSoft.net](https://git.foosoft.net/alex/goldsmith): I've been "dogfooding" Goldsmith by using it to [generate my
|
||||
homepage](/posts/generating-the-foosoft.net-homepage) for nearly a decade.
|
||||
* [Basic Sample](https://git.foosoft.net/alex/goldsmith-samples/src/branch/master/basic): a great starting point, this is the sample site from the tutorial.
|
||||
* [Bootstrap Sample](https://git.foosoft.net/alex/goldsmith-samples/src/branch/master/bootstrap): a slightly more advanced sample using [Bootstrap](https://getbootstrap.com/).
|
||||
* [FooSoft.net](https://git.foosoft.net/alex/goldsmith): I've been "dogfooding" Goldsmith by using it to [generate my homepage](/posts/generating-the-foosoft.net-homepage) for nearly a decade.
|
||||
|
||||
## Components
|
||||
|
||||
@ -152,55 +149,32 @@ A growing set of plugins, filters, and other tools are provided to make it easie
|
||||
|
||||
### Plugins
|
||||
|
||||
* [Absolute](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/absolute): Convert relative HTML file
|
||||
references to absolute paths.
|
||||
* [Breadcrumbs](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/breadcrumbs): Generate metadata
|
||||
required to build breadcrumb navigation.
|
||||
* [Collection](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/collection): Group related pages
|
||||
into named collections.
|
||||
* [Document](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/document): Enable simple DOM
|
||||
modification via an API similar to jQuery.
|
||||
* [Forward](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/forward): Create simple redirections
|
||||
for pages that have moved to a new URL.
|
||||
* [FrontMatter](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/frontmatter): Extract the
|
||||
JSON, YAML, or TOML metadata stored in your files.
|
||||
* [Index](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/index): Create metadata for directory
|
||||
file listings and generate directory index pages.
|
||||
* [Layout](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/layout): Transform your HTML files with
|
||||
Go templates.
|
||||
* [LiveJs](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/livejs): Inject JavaScript code to
|
||||
automatically reload pages when modified.
|
||||
* [Markdown](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/markdown): Render Markdown documents
|
||||
as HTML fragments.
|
||||
* [Minify](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/minify): Remove superfluous data from a
|
||||
variety of web formats.
|
||||
* [Pager](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/pager): Split arrays of metadata into
|
||||
standalone pages.
|
||||
* [Rule](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/rule): Update metadata and filter files
|
||||
based on paths.
|
||||
* [Summary](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/summary): Extract summary and title
|
||||
metadata from HTML files.
|
||||
* [Syndicate](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/syndicate): Generate RSS, Atom, and
|
||||
JSON feeds from existing metadata.
|
||||
* [Syntax](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/syntax): Enable syntax highlighting for
|
||||
pre-formatted code blocks.
|
||||
* [Tags](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/tags): Generate tag clouds and indices
|
||||
from file metadata.
|
||||
* [Thumbnail](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/thumbnail): Build thumbnails for a
|
||||
variety of common image formats.
|
||||
* [Absolute](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/absolute): Convert relative HTML file references to absolute paths.
|
||||
* [Breadcrumbs](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/breadcrumbs): Generate metadata required to build breadcrumb navigation.
|
||||
* [Collection](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/collection): Group related pages into named collections.
|
||||
* [Document](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/document): Enable simple DOM modification via an API similar to jQuery.
|
||||
* [Forward](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/forward): Create simple redirections for pages that have moved to a new URL.
|
||||
* [FrontMatter](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/frontmatter): Extract the JSON, YAML, or TOML metadata stored in your files.
|
||||
* [Index](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/index): Create metadata for directory file listings and generate directory index pages.
|
||||
* [Layout](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/layout): Transform your HTML files with Go templates.
|
||||
* [LiveJs](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/livejs): Inject JavaScript code to automatically reload pages when modified.
|
||||
* [Markdown](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/markdown): Render Markdown documents as HTML fragments.
|
||||
* [Minify](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/minify): Remove superfluous data from a variety of web formats.
|
||||
* [Pager](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/pager): Split arrays of metadata into standalone pages.
|
||||
* [Rule](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/rule): Update metadata and filter files based on paths.
|
||||
* [Summary](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/summary): Extract summary and title metadata from HTML files.
|
||||
* [Syndicate](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/syndicate): Generate RSS, Atom, and JSON feeds from existing metadata.
|
||||
* [Syntax](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/syntax): Enable syntax highlighting for pre-formatted code blocks.
|
||||
* [Tags](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/tags): Generate tag clouds and indices from file metadata.
|
||||
* [Thumbnail](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/thumbnail): Build thumbnails for a variety of common image formats.
|
||||
|
||||
### Filters
|
||||
|
||||
* [Condition](https://godoc.org/git.foosoft.net/alex/goldsmith-components/filters/condition): Filter files based on a
|
||||
single condition.
|
||||
* [Operator](https://godoc.org/git.foosoft.net/alex/goldsmith-components/filters/operator): Join filters using
|
||||
logical `AND`, `OR`, and `NOT` operators.
|
||||
* [Wildcard](https://godoc.org/git.foosoft.net/alex/goldsmith-components/filters/wildcard): Filter files using path
|
||||
wildcards (`*`, `?`, etc.)
|
||||
* [Condition](https://godoc.org/git.foosoft.net/alex/goldsmith/filters/condition): Filter files based on a single condition.
|
||||
* [Operator](https://godoc.org/git.foosoft.net/alex/goldsmith/filters/operator): Join filters using logical `AND`, `OR`, and `NOT` operators.
|
||||
* [Wildcard](https://godoc.org/git.foosoft.net/alex/goldsmith/filters/wildcard): Filter files using path wildcards (`*`, `?`, etc.)
|
||||
|
||||
### Other
|
||||
|
||||
* [DevServer](https://godoc.org/git.foosoft.net/alex/goldsmith-components/devserver): Simple framework for building,
|
||||
updating, and viewing your site.
|
||||
* [Harness](https://godoc.org/git.foosoft.net/alex/goldsmith-components/harness): Unit test harness for verifying
|
||||
Goldsmith plugins and filters.
|
||||
* [DevServer](https://godoc.org/git.foosoft.net/alex/goldsmith/devserver): Simple framework for building, updating, and viewing your site.
|
||||
* [Harness](https://godoc.org/git.foosoft.net/alex/goldsmith/harness): Unit test harness for verifying Goldsmith plugins and filters.
|
||||
|
13
cache.go
13
cache.go
@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type cache struct {
|
||||
@ -15,7 +16,7 @@ type cache struct {
|
||||
}
|
||||
|
||||
func (self *cache) retrieveFile(context *Context, outputPath string, inputFiles []*File) (*File, error) {
|
||||
cachePath, err := self.buildCachePath(context, outputPath, inputFiles)
|
||||
cachePath, err := self.buildCachePath(outputPath, inputFiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -32,8 +33,8 @@ func (self *cache) retrieveFile(context *Context, outputPath string, inputFiles
|
||||
return outputFile, nil
|
||||
}
|
||||
|
||||
func (self *cache) storeFile(context *Context, outputFile *File, inputFiles []*File) error {
|
||||
cachePath, err := self.buildCachePath(context, outputFile.Path(), inputFiles)
|
||||
func (self *cache) storeFile(outputFile *File, inputFiles []*File) error {
|
||||
cachePath, err := self.buildCachePath(outputFile.Path(), inputFiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -68,11 +69,13 @@ func (self *cache) storeFile(context *Context, outputFile *File, inputFiles []*F
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *cache) buildCachePath(context *Context, outputPath string, inputFiles []*File) (string, error) {
|
||||
func (self *cache) buildCachePath(outputPath string, inputFiles []*File) (string, error) {
|
||||
hasher := crc32.NewIEEE()
|
||||
hasher.Write([]byte(outputPath))
|
||||
|
||||
sort.Sort(filesByPath(inputFiles))
|
||||
sort.Slice(inputFiles, func(i, j int) bool {
|
||||
return strings.Compare(inputFiles[i].Path(), inputFiles[j].Path()) < 0
|
||||
})
|
||||
|
||||
for _, inputFile := range inputFiles {
|
||||
modTimeBuff := make([]byte, 8)
|
||||
|
32
chain_state.go
Normal file
32
chain_state.go
Normal file
@ -0,0 +1,32 @@
|
||||
package goldsmith
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type chainState struct {
|
||||
contexts []*Context
|
||||
|
||||
cache *cache
|
||||
filters filterStack
|
||||
clean bool
|
||||
index int
|
||||
|
||||
errors []error
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func (self *chainState) fault(name string, file *File, err error) {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
var faultError error
|
||||
if file == nil {
|
||||
faultError = fmt.Errorf("[%s]: %w", name, err)
|
||||
} else {
|
||||
faultError = fmt.Errorf("[%s@%v]: %w", name, file, err)
|
||||
}
|
||||
|
||||
self.errors = append(self.errors, faultError)
|
||||
}
|
49
context.go
49
context.go
@ -14,8 +14,7 @@ import (
|
||||
// Context corresponds to the current link in the chain and provides methods
|
||||
// that enable plugins to inject new files into the chain.
|
||||
type Context struct {
|
||||
goldsmith *Goldsmith
|
||||
|
||||
chain *chainState
|
||||
plugin Plugin
|
||||
|
||||
filtersExt filterStack
|
||||
@ -29,15 +28,19 @@ type Context struct {
|
||||
}
|
||||
|
||||
// CreateFileFrom data creates a new file instance from the provided data buffer.
|
||||
func (self *Context) CreateFileFromReader(sourcePath string, reader io.Reader) (*File, error) {
|
||||
func (self *Context) CreateFileFromReader(relPath string, reader io.Reader) (*File, error) {
|
||||
if filepath.IsAbs(relPath) {
|
||||
return nil, errors.New("file paths must be relative")
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file := &File{
|
||||
relPath: sourcePath,
|
||||
props: make(map[string]Prop),
|
||||
relPath: relPath,
|
||||
props: make(FileProps),
|
||||
modTime: time.Now(),
|
||||
size: int64(len(data)),
|
||||
reader: bytes.NewReader(data),
|
||||
@ -48,13 +51,9 @@ func (self *Context) CreateFileFromReader(sourcePath string, reader io.Reader) (
|
||||
}
|
||||
|
||||
// CreateFileFromAsset creates a new file instance from the provided file path.
|
||||
func (self *Context) CreateFileFromAsset(sourcePath, dataPath string) (*File, error) {
|
||||
if filepath.IsAbs(sourcePath) {
|
||||
return nil, errors.New("source paths must be relative")
|
||||
}
|
||||
|
||||
if filepath.IsAbs(dataPath) {
|
||||
return nil, errors.New("data paths must be relative")
|
||||
func (self *Context) CreateFileFromAsset(relPath, dataPath string) (*File, error) {
|
||||
if filepath.IsAbs(relPath) {
|
||||
return nil, errors.New("file paths must be relative")
|
||||
}
|
||||
|
||||
info, err := os.Stat(dataPath)
|
||||
@ -62,12 +61,12 @@ func (self *Context) CreateFileFromAsset(sourcePath, dataPath string) (*File, er
|
||||
return nil, err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil, errors.New("assets must be files")
|
||||
return nil, errors.New("file paths cannot be directories")
|
||||
}
|
||||
|
||||
file := &File{
|
||||
relPath: sourcePath,
|
||||
props: make(map[string]Prop),
|
||||
relPath: relPath,
|
||||
props: make(FileProps),
|
||||
modTime: info.ModTime(),
|
||||
size: info.Size(),
|
||||
dataPath: dataPath,
|
||||
@ -86,11 +85,11 @@ func (self *Context) DispatchFile(file *File) {
|
||||
// dependencies on any input files that are needed to generate it, and then
|
||||
// passes it to the next link in the chain.
|
||||
func (self *Context) DispatchAndCacheFile(outputFile *File, inputFiles ...*File) {
|
||||
if self.goldsmith.cache != nil {
|
||||
self.goldsmith.cache.storeFile(self, outputFile, inputFiles)
|
||||
if self.chain.cache != nil {
|
||||
self.chain.cache.storeFile(outputFile, inputFiles)
|
||||
}
|
||||
|
||||
self.filesOut <- outputFile
|
||||
self.DispatchFile(outputFile)
|
||||
}
|
||||
|
||||
// RetrieveCachedFile looks up file data (excluding the metadata), given an
|
||||
@ -98,8 +97,8 @@ func (self *Context) DispatchAndCacheFile(outputFile *File, inputFiles ...*File)
|
||||
// will return nil if the desired file is not found in the cache.
|
||||
func (self *Context) RetrieveCachedFile(outputPath string, inputFiles ...*File) *File {
|
||||
var outputFile *File
|
||||
if self.goldsmith.cache != nil {
|
||||
outputFile, _ = self.goldsmith.cache.retrieveFile(self, outputPath, inputFiles)
|
||||
if self.chain.cache != nil {
|
||||
outputFile, _ = self.chain.cache.retrieveFile(self, outputPath, inputFiles)
|
||||
}
|
||||
|
||||
return outputFile
|
||||
@ -125,7 +124,7 @@ func (self *Context) step() {
|
||||
|
||||
if initializer, ok := self.plugin.(Initializer); ok {
|
||||
if err := initializer.Initialize(self); err != nil {
|
||||
self.goldsmith.fault(self.plugin.Name(), nil, err)
|
||||
self.chain.fault(self.plugin.Name(), nil, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -146,13 +145,13 @@ func (self *Context) step() {
|
||||
for inputFile := range self.filesIn {
|
||||
if processor != nil && self.filtersInt.accept(inputFile) && self.filtersExt.accept(inputFile) {
|
||||
if _, err := inputFile.Seek(0, io.SeekStart); err != nil {
|
||||
self.goldsmith.fault("core", inputFile, err)
|
||||
self.chain.fault("core", inputFile, err)
|
||||
}
|
||||
if err := processor.Process(self, inputFile); err != nil {
|
||||
self.goldsmith.fault(self.plugin.Name(), inputFile, err)
|
||||
self.chain.fault(self.plugin.Name(), inputFile, err)
|
||||
}
|
||||
} else {
|
||||
self.filesOut <- inputFile
|
||||
self.DispatchFile(inputFile)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@ -163,7 +162,7 @@ func (self *Context) step() {
|
||||
|
||||
if finalizer, ok := self.plugin.(Finalizer); ok {
|
||||
if err := finalizer.Finalize(self); err != nil {
|
||||
self.goldsmith.fault(self.plugin.Name(), nil, err)
|
||||
self.chain.fault(self.plugin.Name(), nil, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
115
devserver/devserver.go
Normal file
115
devserver/devserver.go
Normal file
@ -0,0 +1,115 @@
|
||||
// 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"
|
||||
"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 := os.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)
|
||||
}
|
||||
}
|
||||
}
|
32
extension.go
Normal file
32
extension.go
Normal file
@ -0,0 +1,32 @@
|
||||
package goldsmith
|
||||
|
||||
// Plugin contains the minimum set of methods required on plugins. Plugins can
|
||||
// also optionally implement Initializer, Processor, and Finalizer interfaces.
|
||||
type (
|
||||
Plugin interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
// Initializer is used to optionally initialize a plugin and to specify a
|
||||
// filter to be used for determining which files will be processed.
|
||||
Initializer interface {
|
||||
Initialize(context *Context) error
|
||||
}
|
||||
|
||||
// Processor allows for optional processing of files passing through a plugin.
|
||||
Processor interface {
|
||||
Process(context *Context, file *File) error
|
||||
}
|
||||
|
||||
// Finalizer allows for optional finalization of a plugin after all files
|
||||
// queued in the chain have passed through it.
|
||||
Finalizer interface {
|
||||
Finalize(context *Context) error
|
||||
}
|
||||
|
||||
// Filter is used to determine which files should continue in the chain.
|
||||
Filter interface {
|
||||
Name() string
|
||||
Accept(file *File) bool
|
||||
}
|
||||
)
|
122
file.go
122
file.go
@ -2,42 +2,38 @@ package goldsmith
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Prop interface{}
|
||||
type PropMap map[string]Prop
|
||||
type (
|
||||
FileProp any
|
||||
FileProps map[string]FileProp
|
||||
|
||||
// File represents in-memory or on-disk files in a chain.
|
||||
type File struct {
|
||||
relPath string
|
||||
props map[string]Prop
|
||||
modTime time.Time
|
||||
size int64
|
||||
// File represents in-memory or on-disk files in a chain.
|
||||
File struct {
|
||||
relPath string
|
||||
props FileProps
|
||||
modTime time.Time
|
||||
size int64
|
||||
|
||||
dataPath string
|
||||
reader *bytes.Reader
|
||||
dataPath string
|
||||
reader *bytes.Reader
|
||||
|
||||
index int
|
||||
}
|
||||
index int
|
||||
}
|
||||
)
|
||||
|
||||
// Rename modifies the file path relative to the source directory.
|
||||
func (self *File) Rename(path string) {
|
||||
self.relPath = path
|
||||
}
|
||||
|
||||
func (self *File) Rewrite(reader io.Reader) error {
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
func (self *File) Rename(path string) error {
|
||||
if filepath.IsAbs(path) {
|
||||
return fmt.Errorf("unexpected absolute path: %s", path)
|
||||
}
|
||||
|
||||
self.reader = bytes.NewReader(data)
|
||||
self.modTime = time.Now()
|
||||
self.size = int64(len(data))
|
||||
self.relPath = path
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -46,16 +42,16 @@ func (self *File) Path() string {
|
||||
return filepath.ToSlash(self.relPath)
|
||||
}
|
||||
|
||||
// Name returns the base name of the file.
|
||||
func (self *File) Name() string {
|
||||
return filepath.Base(self.relPath)
|
||||
}
|
||||
|
||||
// Dir returns the containing directory of the file.
|
||||
func (self *File) Dir() string {
|
||||
return filepath.ToSlash(filepath.Dir(self.relPath))
|
||||
}
|
||||
|
||||
// Name returns the base name of the file.
|
||||
func (self *File) Name() string {
|
||||
return filepath.Base(self.relPath)
|
||||
}
|
||||
|
||||
// Ext returns the extension of the file.
|
||||
func (self *File) Ext() string {
|
||||
return filepath.Ext(self.relPath)
|
||||
@ -102,31 +98,29 @@ func (self *File) Seek(offset int64, whence int) (int64, error) {
|
||||
return self.reader.Seek(offset, whence)
|
||||
}
|
||||
|
||||
// Returns value for string formatting.
|
||||
// GoString returns value for string formatting.
|
||||
func (self *File) GoString() string {
|
||||
return self.relPath
|
||||
}
|
||||
|
||||
func (self *File) SetProp(name string, value Prop) {
|
||||
// RemoveProp deletes the metadata property for the provided name.
|
||||
func (self *File) RemoveProp(name string) {
|
||||
delete(self.props, name)
|
||||
}
|
||||
|
||||
// SetProp updates the metadata property for the provided name.
|
||||
func (self *File) SetProp(name string, value FileProp) {
|
||||
self.props[name] = value
|
||||
}
|
||||
|
||||
func (self *File) CopyProps(file *File) {
|
||||
for key, value := range file.props {
|
||||
self.props[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func (self *File) Prop(name string) (Prop, bool) {
|
||||
// Prop returns the metadata property for the provided name.
|
||||
func (self *File) Prop(name string) (FileProp, bool) {
|
||||
value, ok := self.props[name]
|
||||
return value, ok
|
||||
}
|
||||
|
||||
func (self *File) Props() PropMap {
|
||||
return self.props
|
||||
}
|
||||
|
||||
func (self *File) PropOrDefault(name string, valueDef Prop) Prop {
|
||||
// PropOrDef returns the metadata property for the provided name or the default.
|
||||
func (self *File) PropOrDef(name string, valueDef FileProp) FileProp {
|
||||
if value, ok := self.Prop(name); ok {
|
||||
return value
|
||||
}
|
||||
@ -134,44 +128,16 @@ func (self *File) PropOrDefault(name string, valueDef Prop) Prop {
|
||||
return valueDef
|
||||
}
|
||||
|
||||
func (self *File) export(targetDir string) error {
|
||||
targetPath := filepath.Join(targetDir, self.relPath)
|
||||
// Props returns all of the metadata properties.
|
||||
func (self *File) Props() FileProps {
|
||||
return self.props
|
||||
}
|
||||
|
||||
if targetInfo, err := os.Stat(targetPath); err == nil && !targetInfo.ModTime().Before(self.ModTime()) {
|
||||
return nil
|
||||
// CopyProps copies all metadata properties from the provided file.
|
||||
func (self *File) CopyProps(file *File) {
|
||||
for key, value := range file.props {
|
||||
self.props[key] = value
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fw, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fw.Close()
|
||||
|
||||
if self.reader == nil {
|
||||
fr, err := os.Open(self.dataPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fr.Close()
|
||||
|
||||
if _, err := io.Copy(fw, fr); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := self.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := self.WriteTo(fw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *File) load() error {
|
||||
|
90
file_exporter.go
Normal file
90
file_exporter.go
Normal file
@ -0,0 +1,90 @@
|
||||
package goldsmith
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type fileExporter struct {
|
||||
targetDir string
|
||||
clean bool
|
||||
tokens map[string]bool
|
||||
}
|
||||
|
||||
func (*fileExporter) Name() string {
|
||||
return "exporter"
|
||||
}
|
||||
|
||||
func (self *fileExporter) Initialize(context *Context) error {
|
||||
self.tokens = make(map[string]bool)
|
||||
context.Threads(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *fileExporter) Process(context *Context, file *File) error {
|
||||
slicePath := func(path string) string {
|
||||
if filepath.IsAbs(path) {
|
||||
var err error
|
||||
if path, err = filepath.Rel("/", path); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return filepath.Clean(path)
|
||||
}
|
||||
|
||||
for token := slicePath(file.Path()); token != "."; token = filepath.Dir(token) {
|
||||
self.tokens[token] = true
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(self.targetDir, file.Path())
|
||||
if targetInfo, err := os.Stat(targetPath); err == nil && !targetInfo.ModTime().Before(file.ModTime()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fw, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fw.Close()
|
||||
|
||||
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := file.WriteTo(fw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *fileExporter) Finalize(context *Context) error {
|
||||
if !self.clean {
|
||||
return nil
|
||||
}
|
||||
|
||||
return filepath.Walk(self.targetDir, func(path string, info os.FileInfo, err error) error {
|
||||
if path == self.targetDir {
|
||||
return nil
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(self.targetDir, path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if tokenized, _ := self.tokens[relPath]; !tokenized {
|
||||
if err := os.RemoveAll(path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
35
file_importer.go
Normal file
35
file_importer.go
Normal file
@ -0,0 +1,35 @@
|
||||
package goldsmith
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type fileImporter struct {
|
||||
sourceDir string
|
||||
}
|
||||
|
||||
func (*fileImporter) Name() string {
|
||||
return "importer"
|
||||
}
|
||||
|
||||
func (self *fileImporter) Initialize(context *Context) error {
|
||||
return filepath.Walk(self.sourceDir, func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(self.sourceDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := context.CreateFileFromAsset(relPath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
context.DispatchFile(file)
|
||||
return nil
|
||||
})
|
||||
}
|
49
file_util.go
49
file_util.go
@ -1,49 +0,0 @@
|
||||
package goldsmith
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type filesByPath []*File
|
||||
|
||||
func (self filesByPath) Len() int {
|
||||
return len(self)
|
||||
}
|
||||
|
||||
func (self filesByPath) Swap(i, j int) {
|
||||
self[i], self[j] = self[j], self[i]
|
||||
}
|
||||
|
||||
func (self filesByPath) Less(i, j int) bool {
|
||||
return strings.Compare(self[i].Path(), self[j].Path()) < 0
|
||||
}
|
||||
|
||||
type fileInfo struct {
|
||||
os.FileInfo
|
||||
path string
|
||||
}
|
||||
|
||||
func cleanPath(path string) string {
|
||||
if filepath.IsAbs(path) {
|
||||
var err error
|
||||
if path, err = filepath.Rel("/", path); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return filepath.Clean(path)
|
||||
}
|
||||
|
||||
func scanDir(rootDir string, infos chan fileInfo) {
|
||||
defer close(infos)
|
||||
|
||||
filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err == nil {
|
||||
infos <- fileInfo{FileInfo: info, path: path}
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
package goldsmith
|
||||
|
||||
type filterEntry struct {
|
||||
filter Filter
|
||||
index int
|
||||
}
|
||||
type (
|
||||
filterEntry struct {
|
||||
filter Filter
|
||||
index int
|
||||
}
|
||||
|
||||
type filterStack []filterEntry
|
||||
filterStack []filterEntry
|
||||
)
|
||||
|
||||
func (self *filterStack) accept(file *File) bool {
|
||||
for _, entry := range *self {
|
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"))
|
||||
},
|
||||
)
|
||||
}
|
27
go.mod
27
go.mod
@ -1,3 +1,28 @@
|
||||
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/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // 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
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
)
|
||||
|
96
go.sum
Normal file
96
go.sum
Normal file
@ -0,0 +1,96 @@
|
||||
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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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/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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
79
goldsmith.go
79
goldsmith.go
@ -1,107 +1,78 @@
|
||||
// Package goldsmith generates static websites.
|
||||
package goldsmith
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Goldsmith chainable context.
|
||||
type Goldsmith struct {
|
||||
sourceDir string
|
||||
targetDir string
|
||||
|
||||
contexts []*Context
|
||||
|
||||
cache *cache
|
||||
filters filterStack
|
||||
clean bool
|
||||
index int
|
||||
|
||||
errors []error
|
||||
mutex sync.Mutex
|
||||
chain *chainState
|
||||
}
|
||||
|
||||
// Begin starts a chain, reading the files located in the source directory as input.
|
||||
func Begin(sourceDir string) *Goldsmith {
|
||||
goldsmith := &Goldsmith{sourceDir: sourceDir}
|
||||
goldsmith.Chain(&loader{})
|
||||
return goldsmith
|
||||
func (self *Goldsmith) Begin(sourceDir string) *Goldsmith {
|
||||
self.chain = &chainState{}
|
||||
self.Chain(&fileImporter{sourceDir: sourceDir})
|
||||
return self
|
||||
}
|
||||
|
||||
// Cache enables caching in cacheDir for the remainder of the chain.
|
||||
func (self *Goldsmith) Cache(cacheDir string) *Goldsmith {
|
||||
self.cache = &cache{cacheDir}
|
||||
self.chain.cache = &cache{cacheDir}
|
||||
return self
|
||||
}
|
||||
|
||||
// Clean enables or disables removal of leftover files in the target directory.
|
||||
func (self *Goldsmith) Clean(clean bool) *Goldsmith {
|
||||
self.clean = clean
|
||||
self.chain.clean = clean
|
||||
return self
|
||||
}
|
||||
|
||||
// Chain links a plugin instance into the chain.
|
||||
func (self *Goldsmith) Chain(plugin Plugin) *Goldsmith {
|
||||
context := &Context{
|
||||
goldsmith: self,
|
||||
chain: self.chain,
|
||||
plugin: plugin,
|
||||
filtersExt: append(filterStack(nil), self.filters...),
|
||||
index: self.index,
|
||||
filtersExt: append(filterStack(nil), self.chain.filters...),
|
||||
index: self.chain.index,
|
||||
filesOut: make(chan *File),
|
||||
}
|
||||
|
||||
if len(self.contexts) > 0 {
|
||||
context.filesIn = self.contexts[len(self.contexts)-1].filesOut
|
||||
if len(self.chain.contexts) > 0 {
|
||||
context.filesIn = self.chain.contexts[len(self.chain.contexts)-1].filesOut
|
||||
}
|
||||
|
||||
self.contexts = append(self.contexts, context)
|
||||
self.index++
|
||||
self.chain.contexts = append(self.chain.contexts, context)
|
||||
self.chain.index++
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
// FilterPush pushes a filter instance on the chain's filter stack.
|
||||
func (self *Goldsmith) FilterPush(filter Filter) *Goldsmith {
|
||||
self.filters.push(filter, self.index)
|
||||
self.index++
|
||||
self.chain.filters.push(filter, self.chain.index)
|
||||
self.chain.index++
|
||||
return self
|
||||
}
|
||||
|
||||
// FilterPop pops a filter instance from the chain's filter stack.
|
||||
func (self *Goldsmith) FilterPop() *Goldsmith {
|
||||
self.filters.pop()
|
||||
self.index++
|
||||
self.chain.filters.pop()
|
||||
self.chain.index++
|
||||
return self
|
||||
}
|
||||
|
||||
// End stops a chain, writing all recieved files to targetDir as output.
|
||||
func (self *Goldsmith) End(targetDir string) []error {
|
||||
self.targetDir = targetDir
|
||||
|
||||
self.Chain(&saver{clean: self.clean})
|
||||
for _, context := range self.contexts {
|
||||
self.Chain(&fileExporter{targetDir: targetDir, clean: self.chain.clean})
|
||||
for _, context := range self.chain.contexts {
|
||||
go context.step()
|
||||
}
|
||||
|
||||
context := self.contexts[len(self.contexts)-1]
|
||||
context := self.chain.contexts[len(self.chain.contexts)-1]
|
||||
for range context.filesOut {
|
||||
|
||||
}
|
||||
|
||||
return self.errors
|
||||
}
|
||||
|
||||
func (self *Goldsmith) fault(name string, file *File, err error) {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
var faultError error
|
||||
if file == nil {
|
||||
faultError = fmt.Errorf("[%s]: %w", name, err)
|
||||
} else {
|
||||
faultError = fmt.Errorf("[%s@%v]: %w", name, file, err)
|
||||
}
|
||||
|
||||
self.errors = append(self.errors, faultError)
|
||||
errors := self.chain.errors
|
||||
self.chain = nil
|
||||
|
||||
return errors
|
||||
}
|
||||
|
111
harness/harness.go
Normal file
111
harness/harness.go
Normal file
@ -0,0 +1,111 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// StagingCallback callback function is used to set up a goldsmith chain.
|
||||
type StagingCallback func(gs *goldsmith.Goldsmith)
|
||||
|
||||
// Validate enables validation of a single, unnamed case (test data is stored in "testdata").
|
||||
func Validate(t *testing.T, stager StagingCallback) {
|
||||
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 StagingCallback) {
|
||||
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 StagingCallback) []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 StagingCallback) []error {
|
||||
var gs goldsmith.Goldsmith
|
||||
gs.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()
|
||||
}
|
30
interface.go
30
interface.go
@ -1,30 +0,0 @@
|
||||
package goldsmith
|
||||
|
||||
// Plugin contains the minimum set of methods required on plugins. Plugins can
|
||||
// also optionally implement Initializer, Processor, and Finalizer interfaces.
|
||||
type Plugin interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
// Initializer is used to optionally initialize a plugin and to specify a
|
||||
// filter to be used for determining which files will be processed.
|
||||
type Initializer interface {
|
||||
Initialize(context *Context) error
|
||||
}
|
||||
|
||||
// Processor allows for optional processing of files passing through a plugin.
|
||||
type Processor interface {
|
||||
Process(context *Context, file *File) error
|
||||
}
|
||||
|
||||
// Finalizer allows for optional finalization of a plugin after all files
|
||||
// queued in the chain have passed through it.
|
||||
type Finalizer interface {
|
||||
Finalize(context *Context) error
|
||||
}
|
||||
|
||||
// Filter is used to determine which files should continue in the chain.
|
||||
type Filter interface {
|
||||
Name() string
|
||||
Accept(file *File) bool
|
||||
}
|
30
loader.go
30
loader.go
@ -1,30 +0,0 @@
|
||||
package goldsmith
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
type loader struct{}
|
||||
|
||||
func (*loader) Name() string {
|
||||
return "loader"
|
||||
}
|
||||
|
||||
func (*loader) Initialize(context *Context) error {
|
||||
scannedInfo := make(chan fileInfo)
|
||||
go scanDir(context.goldsmith.sourceDir, scannedInfo)
|
||||
|
||||
for info := range scannedInfo {
|
||||
if info.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
relPath, _ := filepath.Rel(context.goldsmith.sourceDir, info.path)
|
||||
file, err := context.CreateFileFromAsset(relPath, info.path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
context.DispatchFile(file)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
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>
|
5
plugins/breadcrumbs/testdata/source/child_1.html
vendored
Normal file
5
plugins/breadcrumbs/testdata/source/child_1.html
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
+++
|
||||
Layout = "page"
|
||||
CrumbName = "Child 1"
|
||||
CrumbParent = "Parent 1"
|
||||
+++
|
5
plugins/breadcrumbs/testdata/source/child_2.html
vendored
Normal file
5
plugins/breadcrumbs/testdata/source/child_2.html
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
+++
|
||||
Layout = "page"
|
||||
CrumbName = "Child 2"
|
||||
CrumbParent = "Parent 1"
|
||||
+++
|
5
plugins/breadcrumbs/testdata/source/child_3.html
vendored
Normal file
5
plugins/breadcrumbs/testdata/source/child_3.html
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
+++
|
||||
Layout = "page"
|
||||
CrumbName = "Child 3"
|
||||
CrumbParent = "Parent 2"
|
||||
+++
|
5
plugins/breadcrumbs/testdata/source/child_4.html
vendored
Normal file
5
plugins/breadcrumbs/testdata/source/child_4.html
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
+++
|
||||
Layout = "page"
|
||||
CrumbName = "Child 4"
|
||||
CrumbParent = "Parent 2"
|
||||
+++
|
5
plugins/breadcrumbs/testdata/source/parent_1.html
vendored
Normal file
5
plugins/breadcrumbs/testdata/source/parent_1.html
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
+++
|
||||
Layout = "page"
|
||||
CrumbName = "Parent 1"
|
||||
CrumbParent = "Root 1"
|
||||
+++
|
5
plugins/breadcrumbs/testdata/source/parent_2.html
vendored
Normal file
5
plugins/breadcrumbs/testdata/source/parent_2.html
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
+++
|
||||
Layout = "page"
|
||||
CrumbName = "Parent 2"
|
||||
CrumbParent = "Root 1"
|
||||
+++
|
4
plugins/breadcrumbs/testdata/source/root_1.html
vendored
Normal file
4
plugins/breadcrumbs/testdata/source/root_1.html
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
+++
|
||||
Layout = "page"
|
||||
CrumbName = "Root 1"
|
||||
+++
|
4
plugins/breadcrumbs/testdata/source/root_2.html
vendored
Normal file
4
plugins/breadcrumbs/testdata/source/root_2.html
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
+++
|
||||
Layout = "page"
|
||||
CrumbName = "Root 2"
|
||||
+++
|
26
plugins/breadcrumbs/testdata/source/template.gohtml
vendored
Normal file
26
plugins/breadcrumbs/testdata/source/template.gohtml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
{{define "page"}}
|
||||
<html>
|
||||
<body>
|
||||
<h1>{{.Props.CrumbName}}</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>
|
||||
{{if .Props.CrumbParent}}
|
||||
<div>
|
||||
{{range .Props.Crumbs.Ancestors}}
|
||||
<a href="{{.File.Path}}" class="breadcrumb-item">{{.File.Props.CrumbName}}</a> >
|
||||
{{end}}
|
||||
<span class="breadcrumb-item active">{{.Props.CrumbName}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
131
plugins/collection/collection.go
Normal file
131
plugins/collection/collection.go
Normal file
@ -0,0 +1,131 @@
|
||||
// Package collection groups related pages into named collections. This can be
|
||||
// useful for presenting blog posts on your front page, and displaying summary
|
||||
// information about other types of content on your website. It can be used in
|
||||
// conjunction with the "pager" plugin to create large collections which are
|
||||
// split across several pages.
|
||||
package collection
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.foosoft.net/alex/goldsmith"
|
||||
"git.foosoft.net/alex/goldsmith/filters/wildcard"
|
||||
)
|
||||
|
||||
// A Comparer callback function is used to sort files within a collection group.
|
||||
type Comparer func(i, j *goldsmith.File) (less bool)
|
||||
|
||||
// Collection chainable plugin context.
|
||||
type Collection struct {
|
||||
collectionKey string
|
||||
groupsKey string
|
||||
comparer Comparer
|
||||
|
||||
groups map[string][]*goldsmith.File
|
||||
files []*goldsmith.File
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// New creates a new instance of the Collection plugin.
|
||||
func New() *Collection {
|
||||
return &Collection{
|
||||
collectionKey: "Collection",
|
||||
groupsKey: "Groups",
|
||||
groups: make(map[string][]*goldsmith.File),
|
||||
}
|
||||
}
|
||||
|
||||
// CollectionKey sets the metadata key used to access the collection name (default: "Collection").
|
||||
// The metadata associated with this key can be either a single string or an array of strings.
|
||||
func (self *Collection) CollectionKey(collectionKey string) *Collection {
|
||||
self.collectionKey = collectionKey
|
||||
return self
|
||||
}
|
||||
|
||||
// GroupsKey sets the metadata key used to store information about collection groups (default: "Groups").
|
||||
// This information is stored as a mapping of group names to contained files.
|
||||
func (self *Collection) GroupsKey(groupsKey string) *Collection {
|
||||
self.groupsKey = groupsKey
|
||||
return self
|
||||
}
|
||||
|
||||
// Comparer sets the function used to sort files in collection groups (default: sort by filenames).
|
||||
func (plugin *Collection) Comparer(comparer Comparer) *Collection {
|
||||
plugin.comparer = comparer
|
||||
return plugin
|
||||
}
|
||||
|
||||
func (*Collection) Name() string {
|
||||
return "collection"
|
||||
}
|
||||
|
||||
func (*Collection) Initialize(context *goldsmith.Context) error {
|
||||
context.Filter(wildcard.New("**/*.html", "**/*.htm"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Collection) Process(context *goldsmith.Context, inputFile *goldsmith.File) error {
|
||||
self.mutex.Lock()
|
||||
defer func() {
|
||||
inputFile.SetProp(self.groupsKey, self.groups)
|
||||
self.files = append(self.files, inputFile)
|
||||
self.mutex.Unlock()
|
||||
}()
|
||||
|
||||
collectionRaw, ok := inputFile.Prop(self.collectionKey)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var collectionNames []string
|
||||
switch t := collectionRaw.(type) {
|
||||
case string:
|
||||
collectionNames = append(collectionNames, t)
|
||||
case []string:
|
||||
collectionNames = append(collectionNames, t...)
|
||||
}
|
||||
|
||||
for _, collectionName := range collectionNames {
|
||||
files, _ := self.groups[collectionName]
|
||||
files = append(files, inputFile)
|
||||
self.groups[collectionName] = files
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Collection) Finalize(context *goldsmith.Context) error {
|
||||
for _, files := range self.groups {
|
||||
fg := &fileSorter{files, self.comparer}
|
||||
sort.Sort(fg)
|
||||
}
|
||||
|
||||
for _, file := range self.files {
|
||||
context.DispatchFile(file)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type fileSorter struct {
|
||||
files []*goldsmith.File
|
||||
comparer Comparer
|
||||
}
|
||||
|
||||
func (self fileSorter) Len() int {
|
||||
return len(self.files)
|
||||
}
|
||||
|
||||
func (self fileSorter) Swap(i, j int) {
|
||||
self.files[i], self.files[j] = self.files[j], self.files[i]
|
||||
}
|
||||
|
||||
func (self fileSorter) Less(i, j int) bool {
|
||||
if self.comparer == nil {
|
||||
return strings.Compare(self.files[i].Path(), self.files[j].Path()) < 0
|
||||
}
|
||||
|
||||
return self.comparer(self.files[i], self.files[j])
|
||||
}
|
22
plugins/collection/collection_test.go
Normal file
22
plugins/collection/collection_test.go
Normal file
@ -0,0 +1,22 @@
|
||||
package collection
|
||||
|
||||
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())
|
||||
},
|
||||
)
|
||||
}
|
46
plugins/collection/testdata/reference/index.html
vendored
Normal file
46
plugins/collection/testdata/reference/index.html
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<div>
|
||||
<h1>Group 1</h1>
|
||||
|
||||
<ul>
|
||||
|
||||
<li>
|
||||
<a href="page_1.html">Page 1</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="page_2.html">Page 2</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="page_3.html">Page 3</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1>Group 2</h1>
|
||||
|
||||
<ul>
|
||||
|
||||
<li>
|
||||
<a href="page_4.html">Page 4</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="page_5.html">Page 5</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="page_6.html">Page 6</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
6
plugins/collection/testdata/reference/page_1.html
vendored
Normal file
6
plugins/collection/testdata/reference/page_1.html
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
<html>
|
||||
<body>
|
||||
<h1>Page 1</h1>
|
||||
</body>
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user