Compare commits
No commits in common. "f5cee223248961f0f9a469e6a6b08e18ae9c48da" and "e4eacea3dd449db2b46cd6fd4ce2cd98e0d82367" have entirely different histories.
f5cee22324
...
e4eacea3dd
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
target
|
|
142
README.md
142
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):
|
1. Start by copying files from a source directory to a destination directory (the simplest possible use case):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var gs goldsmith.Goldsmith
|
goldsmith.
|
||||||
gs.Begin(srcDir). // read files from srcDir
|
Begin(srcDir). // read files from srcDir
|
||||||
End(dstDir) // write files to dstDir
|
End(dstDir) // write files to dstDir
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Now let's convert any Markdown files to HTML fragments (while still copying the rest), using the
|
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/plugins/markdown) plugin:
|
[Markdown](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/markdown) plugin:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var gs goldsmith.Goldsmith
|
goldsmith.
|
||||||
gs.Begin(srcDir). // read files from srcDir
|
Begin(srcDir). // read files from srcDir
|
||||||
Chain(markdown.New()). // convert *.md files to *.html files
|
Chain(markdown.New()). // convert *.md files to *.html files
|
||||||
End(dstDir) // write files to dstDir
|
End(dstDir) // write files to dstDir
|
||||||
```
|
```
|
||||||
|
|
||||||
3. If we have any [front
|
3. If we have any
|
||||||
matter](https://git.foosoft.net/alex/goldsmith-samples/raw/branch/master/basic/content/index.md) in our Markdown
|
[front matter](https://raw.githubusercontent.com/FooSoft/goldsmith-samples/master/basic/content/index.md) in our
|
||||||
files, we need to extract it using the,
|
Markdown files, we need to extract it using the,
|
||||||
[FrontMatter](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/frontmatter) plugin:
|
[FrontMatter](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/frontmatter) plugin:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var gs goldsmith.Goldsmith
|
goldsmith.
|
||||||
gs.Begin(srcDir). // read files from srcDir
|
Begin(srcDir). // read files from srcDir
|
||||||
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
|
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
|
||||||
Chain(markdown.New()). // convert *.md files to *.html files
|
Chain(markdown.New()). // convert *.md files to *.html files
|
||||||
End(dstDir) // write files to dstDir
|
End(dstDir) // write files to dstDir
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Next, we should run our barebones HTML through a
|
4. Next, we should run our barebones HTML through a
|
||||||
[template](https://git.foosoft.net/alex/goldsmith-samples/raw/branch/master/basic/content/layouts/basic.gohtml) to
|
[template](https://raw.githubusercontent.com/FooSoft/goldsmith-samples/master/basic/content/layouts/basic.gohtml) to
|
||||||
add elements like a header, footer, or a menu; for this we can use the
|
add elements like a header, footer, or a menu; for this we can use the
|
||||||
[Layout](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/frontmatter) plugin:
|
[Layout](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/frontmatter) plugin:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var gs goldsmith.Goldsmith
|
goldsmith.
|
||||||
gs.Begin(srcDir). // read files from srcDir
|
Begin(srcDir). // read files from srcDir
|
||||||
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
|
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
|
||||||
Chain(markdown.New()). // convert *.md files to *.html files
|
Chain(markdown.New()). // convert *.md files to *.html files
|
||||||
Chain(layout.New()). // apply *.gohtml templates 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
|
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
|
load times for our site's visitors using the
|
||||||
[Minify](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/minify) plugin:
|
[Minify](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/minify) plugin:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var gs goldsmith.Goldsmith
|
goldsmith.
|
||||||
gs.Begin(srcDir). // read files from srcDir
|
Begin(srcDir). // read files from srcDir
|
||||||
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
|
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
|
||||||
Chain(markdown.New()). // convert *.md files to *.html files
|
Chain(markdown.New()). // convert *.md files to *.html files
|
||||||
Chain(layout.New()). // apply *.gohtml templates 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
|
6. Debugging problems in minified code can be tricky, so let's use the
|
||||||
[Condition](https://godoc.org/git.foosoft.net/alex/goldsmith/filters/condition) filter to make minification occur
|
[Condition](https://godoc.org/git.foosoft.net/alex/goldsmith-components/filters/condition) filter to make
|
||||||
only when we are ready for distribution.
|
minification occur only when we are ready for distribution.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var gs goldsmith.Goldsmith
|
goldsmith.
|
||||||
gs.Begin(srcDir). // read files from srcDir
|
Begin(srcDir). // read files from srcDir
|
||||||
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
|
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
|
||||||
Chain(markdown.New()). // convert *.md files to *.html files
|
Chain(markdown.New()). // convert *.md files to *.html files
|
||||||
Chain(layout.New()). // apply *.gohtml templates 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
|
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/devserver) to bootstrap a complete development sever
|
[DevServer](https://godoc.org/git.foosoft.net/alex/goldsmith-components/devserver) to bootstrap a complete
|
||||||
which automatically rebuilds the site whenever source files are updated.
|
development sever which automatically rebuilds the site whenever source files are updated.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
@ -98,12 +98,12 @@ to understand, it is often best to learn by example:
|
|||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.foosoft.net/alex/goldsmith"
|
"git.foosoft.net/alex/goldsmith"
|
||||||
"git.foosoft.net/alex/goldsmith/devserver"
|
"git.foosoft.net/alex/goldsmith-components/devserver"
|
||||||
"git.foosoft.net/alex/goldsmith/filters/condition"
|
"git.foosoft.net/alex/goldsmith-components/filters/condition"
|
||||||
"git.foosoft.net/alex/goldsmith/plugins/frontmatter"
|
"git.foosoft.net/alex/goldsmith-components/plugins/frontmatter"
|
||||||
"git.foosoft.net/alex/goldsmith/plugins/layout"
|
"git.foosoft.net/alex/goldsmith-components/plugins/layout"
|
||||||
"git.foosoft.net/alex/goldsmith/plugins/markdown"
|
"git.foosoft.net/alex/goldsmith-components/plugins/markdown"
|
||||||
"git.foosoft.net/alex/goldsmith/plugins/minify"
|
"git.foosoft.net/alex/goldsmith-components/plugins/minify"
|
||||||
)
|
)
|
||||||
|
|
||||||
type builder struct {
|
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) {
|
func (b *builder) Build(srcDir, dstDir, cacheDir string) {
|
||||||
var gs goldsmith.Goldsmith
|
errs := goldsmith.
|
||||||
errs := gs.Begin(srcDir). // read files from srcDir
|
Begin(srcDir). // read files from srcDir
|
||||||
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
|
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
|
||||||
Chain(markdown.New()). // convert *.md files to *.html files
|
Chain(markdown.New()). // convert *.md files to *.html files
|
||||||
Chain(layout.New()). // apply *.gohtml templates to *.html files
|
Chain(layout.New()). // apply *.gohtml templates to *.html files
|
||||||
@ -139,9 +139,12 @@ 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:
|
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.
|
* [Basic Sample](https://git.foosoft.net/alex/goldsmith-samples/src/branch/master/basic): a great starting point, this
|
||||||
* [Bootstrap Sample](https://git.foosoft.net/alex/goldsmith-samples/src/branch/master/bootstrap): a slightly more advanced sample using [Bootstrap](https://getbootstrap.com/).
|
is the sample site from the tutorial.
|
||||||
* [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.
|
* [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
|
## Components
|
||||||
|
|
||||||
@ -149,32 +152,55 @@ A growing set of plugins, filters, and other tools are provided to make it easie
|
|||||||
|
|
||||||
### Plugins
|
### Plugins
|
||||||
|
|
||||||
* [Absolute](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/absolute): Convert relative HTML file references to absolute paths.
|
* [Absolute](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/absolute): Convert relative HTML file
|
||||||
* [Breadcrumbs](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/breadcrumbs): Generate metadata required to build breadcrumb navigation.
|
references to absolute paths.
|
||||||
* [Collection](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/collection): Group related pages into named collections.
|
* [Breadcrumbs](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/breadcrumbs): Generate metadata
|
||||||
* [Document](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/document): Enable simple DOM modification via an API similar to jQuery.
|
required to build breadcrumb navigation.
|
||||||
* [Forward](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/forward): Create simple redirections for pages that have moved to a new URL.
|
* [Collection](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/collection): Group related pages
|
||||||
* [FrontMatter](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/frontmatter): Extract the JSON, YAML, or TOML metadata stored in your files.
|
into named collections.
|
||||||
* [Index](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/index): Create metadata for directory file listings and generate directory index pages.
|
* [Document](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/document): Enable simple DOM
|
||||||
* [Layout](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/layout): Transform your HTML files with Go templates.
|
modification via an API similar to jQuery.
|
||||||
* [LiveJs](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/livejs): Inject JavaScript code to automatically reload pages when modified.
|
* [Forward](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/forward): Create simple redirections
|
||||||
* [Markdown](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/markdown): Render Markdown documents as HTML fragments.
|
for pages that have moved to a new URL.
|
||||||
* [Minify](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/minify): Remove superfluous data from a variety of web formats.
|
* [FrontMatter](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/frontmatter): Extract the
|
||||||
* [Pager](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/pager): Split arrays of metadata into standalone pages.
|
JSON, YAML, or TOML metadata stored in your files.
|
||||||
* [Rule](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/rule): Update metadata and filter files based on paths.
|
* [Index](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/index): Create metadata for directory
|
||||||
* [Summary](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/summary): Extract summary and title metadata from HTML files.
|
file listings and generate directory index pages.
|
||||||
* [Syndicate](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/syndicate): Generate RSS, Atom, and JSON feeds from existing metadata.
|
* [Layout](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/layout): Transform your HTML files with
|
||||||
* [Syntax](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/syntax): Enable syntax highlighting for pre-formatted code blocks.
|
Go templates.
|
||||||
* [Tags](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/tags): Generate tag clouds and indices from file metadata.
|
* [LiveJs](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/livejs): Inject JavaScript code to
|
||||||
* [Thumbnail](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/thumbnail): Build thumbnails for a variety of common image formats.
|
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.
|
||||||
|
|
||||||
### Filters
|
### Filters
|
||||||
|
|
||||||
* [Condition](https://godoc.org/git.foosoft.net/alex/goldsmith/filters/condition): Filter files based on a single condition.
|
* [Condition](https://godoc.org/git.foosoft.net/alex/goldsmith-components/filters/condition): Filter files based on a
|
||||||
* [Operator](https://godoc.org/git.foosoft.net/alex/goldsmith/filters/operator): Join filters using logical `AND`, `OR`, and `NOT` operators.
|
single condition.
|
||||||
* [Wildcard](https://godoc.org/git.foosoft.net/alex/goldsmith/filters/wildcard): Filter files using path wildcards (`*`, `?`, etc.)
|
* [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.)
|
||||||
|
|
||||||
### Other
|
### Other
|
||||||
|
|
||||||
* [DevServer](https://godoc.org/git.foosoft.net/alex/goldsmith/devserver): Simple framework for building, updating, and viewing your site.
|
* [DevServer](https://godoc.org/git.foosoft.net/alex/goldsmith-components/devserver): Simple framework for building,
|
||||||
* [Harness](https://godoc.org/git.foosoft.net/alex/goldsmith/harness): Unit test harness for verifying Goldsmith plugins and filters.
|
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.
|
||||||
|
13
cache.go
13
cache.go
@ -8,7 +8,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type cache struct {
|
type cache struct {
|
||||||
@ -16,7 +15,7 @@ type cache struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (self *cache) retrieveFile(context *Context, outputPath string, inputFiles []*File) (*File, error) {
|
func (self *cache) retrieveFile(context *Context, outputPath string, inputFiles []*File) (*File, error) {
|
||||||
cachePath, err := self.buildCachePath(outputPath, inputFiles)
|
cachePath, err := self.buildCachePath(context, outputPath, inputFiles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -33,8 +32,8 @@ func (self *cache) retrieveFile(context *Context, outputPath string, inputFiles
|
|||||||
return outputFile, nil
|
return outputFile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *cache) storeFile(outputFile *File, inputFiles []*File) error {
|
func (self *cache) storeFile(context *Context, outputFile *File, inputFiles []*File) error {
|
||||||
cachePath, err := self.buildCachePath(outputFile.Path(), inputFiles)
|
cachePath, err := self.buildCachePath(context, outputFile.Path(), inputFiles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -69,13 +68,11 @@ func (self *cache) storeFile(outputFile *File, inputFiles []*File) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *cache) buildCachePath(outputPath string, inputFiles []*File) (string, error) {
|
func (self *cache) buildCachePath(context *Context, outputPath string, inputFiles []*File) (string, error) {
|
||||||
hasher := crc32.NewIEEE()
|
hasher := crc32.NewIEEE()
|
||||||
hasher.Write([]byte(outputPath))
|
hasher.Write([]byte(outputPath))
|
||||||
|
|
||||||
sort.Slice(inputFiles, func(i, j int) bool {
|
sort.Sort(filesByPath(inputFiles))
|
||||||
return strings.Compare(inputFiles[i].Path(), inputFiles[j].Path()) < 0
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, inputFile := range inputFiles {
|
for _, inputFile := range inputFiles {
|
||||||
modTimeBuff := make([]byte, 8)
|
modTimeBuff := make([]byte, 8)
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
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,7 +14,8 @@ import (
|
|||||||
// Context corresponds to the current link in the chain and provides methods
|
// Context corresponds to the current link in the chain and provides methods
|
||||||
// that enable plugins to inject new files into the chain.
|
// that enable plugins to inject new files into the chain.
|
||||||
type Context struct {
|
type Context struct {
|
||||||
chain *chainState
|
goldsmith *Goldsmith
|
||||||
|
|
||||||
plugin Plugin
|
plugin Plugin
|
||||||
|
|
||||||
filtersExt filterStack
|
filtersExt filterStack
|
||||||
@ -28,19 +29,15 @@ type Context struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateFileFrom data creates a new file instance from the provided data buffer.
|
// CreateFileFrom data creates a new file instance from the provided data buffer.
|
||||||
func (self *Context) CreateFileFromReader(relPath string, reader io.Reader) (*File, error) {
|
func (self *Context) CreateFileFromReader(sourcePath string, reader io.Reader) (*File, error) {
|
||||||
if filepath.IsAbs(relPath) {
|
|
||||||
return nil, errors.New("file paths must be relative")
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := io.ReadAll(reader)
|
data, err := io.ReadAll(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
file := &File{
|
file := &File{
|
||||||
relPath: relPath,
|
relPath: sourcePath,
|
||||||
props: make(FileProps),
|
props: make(map[string]Prop),
|
||||||
modTime: time.Now(),
|
modTime: time.Now(),
|
||||||
size: int64(len(data)),
|
size: int64(len(data)),
|
||||||
reader: bytes.NewReader(data),
|
reader: bytes.NewReader(data),
|
||||||
@ -51,9 +48,13 @@ func (self *Context) CreateFileFromReader(relPath string, reader io.Reader) (*Fi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateFileFromAsset creates a new file instance from the provided file path.
|
// CreateFileFromAsset creates a new file instance from the provided file path.
|
||||||
func (self *Context) CreateFileFromAsset(relPath, dataPath string) (*File, error) {
|
func (self *Context) CreateFileFromAsset(sourcePath, dataPath string) (*File, error) {
|
||||||
if filepath.IsAbs(relPath) {
|
if filepath.IsAbs(sourcePath) {
|
||||||
return nil, errors.New("file paths must be relative")
|
return nil, errors.New("source paths must be relative")
|
||||||
|
}
|
||||||
|
|
||||||
|
if filepath.IsAbs(dataPath) {
|
||||||
|
return nil, errors.New("data paths must be relative")
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := os.Stat(dataPath)
|
info, err := os.Stat(dataPath)
|
||||||
@ -61,12 +62,12 @@ func (self *Context) CreateFileFromAsset(relPath, dataPath string) (*File, error
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
return nil, errors.New("file paths cannot be directories")
|
return nil, errors.New("assets must be files")
|
||||||
}
|
}
|
||||||
|
|
||||||
file := &File{
|
file := &File{
|
||||||
relPath: relPath,
|
relPath: sourcePath,
|
||||||
props: make(FileProps),
|
props: make(map[string]Prop),
|
||||||
modTime: info.ModTime(),
|
modTime: info.ModTime(),
|
||||||
size: info.Size(),
|
size: info.Size(),
|
||||||
dataPath: dataPath,
|
dataPath: dataPath,
|
||||||
@ -85,11 +86,11 @@ func (self *Context) DispatchFile(file *File) {
|
|||||||
// dependencies on any input files that are needed to generate it, and then
|
// dependencies on any input files that are needed to generate it, and then
|
||||||
// passes it to the next link in the chain.
|
// passes it to the next link in the chain.
|
||||||
func (self *Context) DispatchAndCacheFile(outputFile *File, inputFiles ...*File) {
|
func (self *Context) DispatchAndCacheFile(outputFile *File, inputFiles ...*File) {
|
||||||
if self.chain.cache != nil {
|
if self.goldsmith.cache != nil {
|
||||||
self.chain.cache.storeFile(outputFile, inputFiles)
|
self.goldsmith.cache.storeFile(self, outputFile, inputFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.DispatchFile(outputFile)
|
self.filesOut <- outputFile
|
||||||
}
|
}
|
||||||
|
|
||||||
// RetrieveCachedFile looks up file data (excluding the metadata), given an
|
// RetrieveCachedFile looks up file data (excluding the metadata), given an
|
||||||
@ -97,8 +98,8 @@ func (self *Context) DispatchAndCacheFile(outputFile *File, inputFiles ...*File)
|
|||||||
// will return nil if the desired file is not found in the cache.
|
// will return nil if the desired file is not found in the cache.
|
||||||
func (self *Context) RetrieveCachedFile(outputPath string, inputFiles ...*File) *File {
|
func (self *Context) RetrieveCachedFile(outputPath string, inputFiles ...*File) *File {
|
||||||
var outputFile *File
|
var outputFile *File
|
||||||
if self.chain.cache != nil {
|
if self.goldsmith.cache != nil {
|
||||||
outputFile, _ = self.chain.cache.retrieveFile(self, outputPath, inputFiles)
|
outputFile, _ = self.goldsmith.cache.retrieveFile(self, outputPath, inputFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
return outputFile
|
return outputFile
|
||||||
@ -124,7 +125,7 @@ func (self *Context) step() {
|
|||||||
|
|
||||||
if initializer, ok := self.plugin.(Initializer); ok {
|
if initializer, ok := self.plugin.(Initializer); ok {
|
||||||
if err := initializer.Initialize(self); err != nil {
|
if err := initializer.Initialize(self); err != nil {
|
||||||
self.chain.fault(self.plugin.Name(), nil, err)
|
self.goldsmith.fault(self.plugin.Name(), nil, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,13 +146,13 @@ func (self *Context) step() {
|
|||||||
for inputFile := range self.filesIn {
|
for inputFile := range self.filesIn {
|
||||||
if processor != nil && self.filtersInt.accept(inputFile) && self.filtersExt.accept(inputFile) {
|
if processor != nil && self.filtersInt.accept(inputFile) && self.filtersExt.accept(inputFile) {
|
||||||
if _, err := inputFile.Seek(0, io.SeekStart); err != nil {
|
if _, err := inputFile.Seek(0, io.SeekStart); err != nil {
|
||||||
self.chain.fault("core", inputFile, err)
|
self.goldsmith.fault("core", inputFile, err)
|
||||||
}
|
}
|
||||||
if err := processor.Process(self, inputFile); err != nil {
|
if err := processor.Process(self, inputFile); err != nil {
|
||||||
self.chain.fault(self.plugin.Name(), inputFile, err)
|
self.goldsmith.fault(self.plugin.Name(), inputFile, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.DispatchFile(inputFile)
|
self.filesOut <- inputFile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -162,7 +163,7 @@ func (self *Context) step() {
|
|||||||
|
|
||||||
if finalizer, ok := self.plugin.(Finalizer); ok {
|
if finalizer, ok := self.plugin.(Finalizer); ok {
|
||||||
if err := finalizer.Finalize(self); err != nil {
|
if err := finalizer.Finalize(self); err != nil {
|
||||||
self.chain.fault(self.plugin.Name(), nil, err)
|
self.goldsmith.fault(self.plugin.Name(), nil, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,115 +0,0 @@
|
|||||||
// 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
32
extension.go
@ -1,32 +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.
|
|
||||||
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
|
|
||||||
}
|
|
||||||
)
|
|
110
file.go
110
file.go
@ -2,21 +2,19 @@ package goldsmith
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type Prop interface{}
|
||||||
FileProp any
|
type PropMap map[string]Prop
|
||||||
FileProps map[string]FileProp
|
|
||||||
|
|
||||||
// File represents in-memory or on-disk files in a chain.
|
// File represents in-memory or on-disk files in a chain.
|
||||||
File struct {
|
type File struct {
|
||||||
relPath string
|
relPath string
|
||||||
props FileProps
|
props map[string]Prop
|
||||||
modTime time.Time
|
modTime time.Time
|
||||||
size int64
|
size int64
|
||||||
|
|
||||||
@ -24,16 +22,22 @@ type (
|
|||||||
reader *bytes.Reader
|
reader *bytes.Reader
|
||||||
|
|
||||||
index int
|
index int
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
// Rename modifies the file path relative to the source directory.
|
// Rename modifies the file path relative to the source directory.
|
||||||
func (self *File) Rename(path string) error {
|
func (self *File) Rename(path string) {
|
||||||
if filepath.IsAbs(path) {
|
self.relPath = path
|
||||||
return fmt.Errorf("unexpected absolute path: %s", path)
|
}
|
||||||
|
|
||||||
|
func (self *File) Rewrite(reader io.Reader) error {
|
||||||
|
data, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
self.relPath = path
|
self.reader = bytes.NewReader(data)
|
||||||
|
self.modTime = time.Now()
|
||||||
|
self.size = int64(len(data))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,16 +46,16 @@ func (self *File) Path() string {
|
|||||||
return filepath.ToSlash(self.relPath)
|
return filepath.ToSlash(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.
|
// Name returns the base name of the file.
|
||||||
func (self *File) Name() string {
|
func (self *File) Name() string {
|
||||||
return filepath.Base(self.relPath)
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
// Ext returns the extension of the file.
|
// Ext returns the extension of the file.
|
||||||
func (self *File) Ext() string {
|
func (self *File) Ext() string {
|
||||||
return filepath.Ext(self.relPath)
|
return filepath.Ext(self.relPath)
|
||||||
@ -98,29 +102,31 @@ func (self *File) Seek(offset int64, whence int) (int64, error) {
|
|||||||
return self.reader.Seek(offset, whence)
|
return self.reader.Seek(offset, whence)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GoString returns value for string formatting.
|
// Returns value for string formatting.
|
||||||
func (self *File) GoString() string {
|
func (self *File) GoString() string {
|
||||||
return self.relPath
|
return self.relPath
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveProp deletes the metadata property for the provided name.
|
func (self *File) SetProp(name string, value Prop) {
|
||||||
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
|
self.props[name] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prop returns the metadata property for the provided name.
|
func (self *File) CopyProps(file *File) {
|
||||||
func (self *File) Prop(name string) (FileProp, bool) {
|
for key, value := range file.props {
|
||||||
|
self.props[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *File) Prop(name string) (Prop, bool) {
|
||||||
value, ok := self.props[name]
|
value, ok := self.props[name]
|
||||||
return value, ok
|
return value, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// PropOrDef returns the metadata property for the provided name or the default.
|
func (self *File) Props() PropMap {
|
||||||
func (self *File) PropOrDef(name string, valueDef FileProp) FileProp {
|
return self.props
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *File) PropOrDefault(name string, valueDef Prop) Prop {
|
||||||
if value, ok := self.Prop(name); ok {
|
if value, ok := self.Prop(name); ok {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
@ -128,16 +134,44 @@ func (self *File) PropOrDef(name string, valueDef FileProp) FileProp {
|
|||||||
return valueDef
|
return valueDef
|
||||||
}
|
}
|
||||||
|
|
||||||
// Props returns all of the metadata properties.
|
func (self *File) export(targetDir string) error {
|
||||||
func (self *File) Props() FileProps {
|
targetPath := filepath.Join(targetDir, self.relPath)
|
||||||
return self.props
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyProps copies all metadata properties from the provided file.
|
if targetInfo, err := os.Stat(targetPath); err == nil && !targetInfo.ModTime().Before(self.ModTime()) {
|
||||||
func (self *File) CopyProps(file *File) {
|
return nil
|
||||||
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 {
|
func (self *File) load() error {
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
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
Normal file
49
file_util.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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,13 +1,11 @@
|
|||||||
package goldsmith
|
package goldsmith
|
||||||
|
|
||||||
type (
|
type filterEntry struct {
|
||||||
filterEntry struct {
|
|
||||||
filter Filter
|
filter Filter
|
||||||
index int
|
index int
|
||||||
}
|
}
|
||||||
|
|
||||||
filterStack []filterEntry
|
type filterStack []filterEntry
|
||||||
)
|
|
||||||
|
|
||||||
func (self *filterStack) accept(file *File) bool {
|
func (self *filterStack) accept(file *File) bool {
|
||||||
for _, entry := range *self {
|
for _, entry := range *self {
|
@ -1,21 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
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))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,109 +0,0 @@
|
|||||||
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)))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
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,28 +1,3 @@
|
|||||||
module git.foosoft.net/alex/goldsmith
|
module git.foosoft.net/alex/goldsmith
|
||||||
|
|
||||||
go 1.20
|
go 1.13
|
||||||
|
|
||||||
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
96
go.sum
@ -1,96 +0,0 @@
|
|||||||
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,78 +1,107 @@
|
|||||||
// Package goldsmith generates static websites.
|
// Package goldsmith generates static websites.
|
||||||
package goldsmith
|
package goldsmith
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
// Goldsmith chainable context.
|
// Goldsmith chainable context.
|
||||||
type Goldsmith struct {
|
type Goldsmith struct {
|
||||||
chain *chainState
|
sourceDir string
|
||||||
|
targetDir string
|
||||||
|
|
||||||
|
contexts []*Context
|
||||||
|
|
||||||
|
cache *cache
|
||||||
|
filters filterStack
|
||||||
|
clean bool
|
||||||
|
index int
|
||||||
|
|
||||||
|
errors []error
|
||||||
|
mutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin starts a chain, reading the files located in the source directory as input.
|
// Begin starts a chain, reading the files located in the source directory as input.
|
||||||
func (self *Goldsmith) Begin(sourceDir string) *Goldsmith {
|
func Begin(sourceDir string) *Goldsmith {
|
||||||
self.chain = &chainState{}
|
goldsmith := &Goldsmith{sourceDir: sourceDir}
|
||||||
self.Chain(&fileImporter{sourceDir: sourceDir})
|
goldsmith.Chain(&loader{})
|
||||||
return self
|
return goldsmith
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache enables caching in cacheDir for the remainder of the chain.
|
// Cache enables caching in cacheDir for the remainder of the chain.
|
||||||
func (self *Goldsmith) Cache(cacheDir string) *Goldsmith {
|
func (self *Goldsmith) Cache(cacheDir string) *Goldsmith {
|
||||||
self.chain.cache = &cache{cacheDir}
|
self.cache = &cache{cacheDir}
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean enables or disables removal of leftover files in the target directory.
|
// Clean enables or disables removal of leftover files in the target directory.
|
||||||
func (self *Goldsmith) Clean(clean bool) *Goldsmith {
|
func (self *Goldsmith) Clean(clean bool) *Goldsmith {
|
||||||
self.chain.clean = clean
|
self.clean = clean
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chain links a plugin instance into the chain.
|
// Chain links a plugin instance into the chain.
|
||||||
func (self *Goldsmith) Chain(plugin Plugin) *Goldsmith {
|
func (self *Goldsmith) Chain(plugin Plugin) *Goldsmith {
|
||||||
context := &Context{
|
context := &Context{
|
||||||
chain: self.chain,
|
goldsmith: self,
|
||||||
plugin: plugin,
|
plugin: plugin,
|
||||||
filtersExt: append(filterStack(nil), self.chain.filters...),
|
filtersExt: append(filterStack(nil), self.filters...),
|
||||||
index: self.chain.index,
|
index: self.index,
|
||||||
filesOut: make(chan *File),
|
filesOut: make(chan *File),
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(self.chain.contexts) > 0 {
|
if len(self.contexts) > 0 {
|
||||||
context.filesIn = self.chain.contexts[len(self.chain.contexts)-1].filesOut
|
context.filesIn = self.contexts[len(self.contexts)-1].filesOut
|
||||||
}
|
}
|
||||||
|
|
||||||
self.chain.contexts = append(self.chain.contexts, context)
|
self.contexts = append(self.contexts, context)
|
||||||
self.chain.index++
|
self.index++
|
||||||
|
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterPush pushes a filter instance on the chain's filter stack.
|
// FilterPush pushes a filter instance on the chain's filter stack.
|
||||||
func (self *Goldsmith) FilterPush(filter Filter) *Goldsmith {
|
func (self *Goldsmith) FilterPush(filter Filter) *Goldsmith {
|
||||||
self.chain.filters.push(filter, self.chain.index)
|
self.filters.push(filter, self.index)
|
||||||
self.chain.index++
|
self.index++
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterPop pops a filter instance from the chain's filter stack.
|
// FilterPop pops a filter instance from the chain's filter stack.
|
||||||
func (self *Goldsmith) FilterPop() *Goldsmith {
|
func (self *Goldsmith) FilterPop() *Goldsmith {
|
||||||
self.chain.filters.pop()
|
self.filters.pop()
|
||||||
self.chain.index++
|
self.index++
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
// End stops a chain, writing all recieved files to targetDir as output.
|
// End stops a chain, writing all recieved files to targetDir as output.
|
||||||
func (self *Goldsmith) End(targetDir string) []error {
|
func (self *Goldsmith) End(targetDir string) []error {
|
||||||
self.Chain(&fileExporter{targetDir: targetDir, clean: self.chain.clean})
|
self.targetDir = targetDir
|
||||||
for _, context := range self.chain.contexts {
|
|
||||||
|
self.Chain(&saver{clean: self.clean})
|
||||||
|
for _, context := range self.contexts {
|
||||||
go context.step()
|
go context.step()
|
||||||
}
|
}
|
||||||
|
|
||||||
context := self.chain.contexts[len(self.chain.contexts)-1]
|
context := self.contexts[len(self.contexts)-1]
|
||||||
for range context.filesOut {
|
for range context.filesOut {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
errors := self.chain.errors
|
return self.errors
|
||||||
self.chain = nil
|
}
|
||||||
|
|
||||||
return 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)
|
||||||
}
|
}
|
||||||
|
@ -1,111 +0,0 @@
|
|||||||
// 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
Normal file
30
interface.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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
Normal file
30
loader.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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
|
||||||
|
}
|
@ -1,114 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
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"))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
<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>
|
|
@ -1,7 +0,0 @@
|
|||||||
<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>
|
|
@ -1,7 +0,0 @@
|
|||||||
<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
7
plugins/absolute/testdata/source/index.html
vendored
@ -1,7 +0,0 @@
|
|||||||
<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>
|
|
@ -1,131 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
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())
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
|
|
||||||
<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>
|
|
@ -1,26 +0,0 @@
|
|||||||
|
|
||||||
<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>
|
|
@ -1,26 +0,0 @@
|
|||||||
|
|
||||||
<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>
|
|
@ -1,26 +0,0 @@
|
|||||||
|
|
||||||
<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>
|
|
@ -1,24 +0,0 @@
|
|||||||
|
|
||||||
<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>
|
|
@ -1,24 +0,0 @@
|
|||||||
|
|
||||||
<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>
|
|
@ -1,17 +0,0 @@
|
|||||||
|
|
||||||
<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>
|
|
@ -1,17 +0,0 @@
|
|||||||
|
|
||||||
<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>
|
|
@ -1,5 +0,0 @@
|
|||||||
+++
|
|
||||||
Layout = "page"
|
|
||||||
CrumbName = "Child 1"
|
|
||||||
CrumbParent = "Parent 1"
|
|
||||||
+++
|
|
@ -1,5 +0,0 @@
|
|||||||
+++
|
|
||||||
Layout = "page"
|
|
||||||
CrumbName = "Child 2"
|
|
||||||
CrumbParent = "Parent 1"
|
|
||||||
+++
|
|
@ -1,5 +0,0 @@
|
|||||||
+++
|
|
||||||
Layout = "page"
|
|
||||||
CrumbName = "Child 3"
|
|
||||||
CrumbParent = "Parent 2"
|
|
||||||
+++
|
|
@ -1,5 +0,0 @@
|
|||||||
+++
|
|
||||||
Layout = "page"
|
|
||||||
CrumbName = "Child 4"
|
|
||||||
CrumbParent = "Parent 2"
|
|
||||||
+++
|
|
@ -1,5 +0,0 @@
|
|||||||
+++
|
|
||||||
Layout = "page"
|
|
||||||
CrumbName = "Parent 1"
|
|
||||||
CrumbParent = "Root 1"
|
|
||||||
+++
|
|
@ -1,5 +0,0 @@
|
|||||||
+++
|
|
||||||
Layout = "page"
|
|
||||||
CrumbName = "Parent 2"
|
|
||||||
CrumbParent = "Root 1"
|
|
||||||
+++
|
|
@ -1,4 +0,0 @@
|
|||||||
+++
|
|
||||||
Layout = "page"
|
|
||||||
CrumbName = "Root 1"
|
|
||||||
+++
|
|
@ -1,4 +0,0 @@
|
|||||||
+++
|
|
||||||
Layout = "page"
|
|
||||||
CrumbName = "Root 2"
|
|
||||||
+++
|
|
@ -1,26 +0,0 @@
|
|||||||
{{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}}
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
|||||||
// 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])
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
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
46
plugins/collection/testdata/reference/index.html
vendored
@ -1,46 +0,0 @@
|
|||||||
|
|
||||||
<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>
|
|
@ -1,6 +0,0 @@
|
|||||||
|
|
||||||
<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