Compare commits
23 Commits
e4eacea3dd
...
f5cee22324
Author | SHA1 | Date | |
---|---|---|---|
f5cee22324 | |||
8ee5f529d8 | |||
5c547dbf68 | |||
dcd19b19a6 | |||
7acc65170f | |||
cd2e515141 | |||
889397b9c5 | |||
22ffaae954 | |||
e24e892773 | |||
9ba1c7cfeb | |||
05f7ee5280 | |||
ab0c1c53d2 | |||
01fac8cb96 | |||
8aee72f2c8 | |||
771b40e82e | |||
c0c940156f | |||
2f95fdba2d | |||
55ed95ddbb | |||
25d629af10 | |||
ea28b5c8cf | |||
648ab12c5b | |||
972c4d81f5 | |||
f0259bcbe9 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
target
|
144
README.md
144
README.md
@ -14,42 +14,42 @@ to understand, it is often best to learn by example:
|
|||||||
1. Start by copying files from a source directory to a destination directory (the simplest possible use case):
|
1. Start by copying files from a source directory to a destination directory (the simplest possible use case):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
goldsmith.
|
var gs goldsmith.Goldsmith
|
||||||
Begin(srcDir). // read files from srcDir
|
gs.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-components/plugins/markdown) plugin:
|
[Markdown](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/markdown) plugin:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
goldsmith.
|
var gs goldsmith.Goldsmith
|
||||||
Begin(srcDir). // read files from srcDir
|
gs.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
|
3. If we have any [front
|
||||||
[front matter](https://raw.githubusercontent.com/FooSoft/goldsmith-samples/master/basic/content/index.md) in our
|
matter](https://git.foosoft.net/alex/goldsmith-samples/raw/branch/master/basic/content/index.md) in our Markdown
|
||||||
Markdown files, we need to extract it using the,
|
files, we need to extract it using the,
|
||||||
[FrontMatter](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/frontmatter) plugin:
|
[FrontMatter](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/frontmatter) plugin:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
goldsmith.
|
var gs goldsmith.Goldsmith
|
||||||
Begin(srcDir). // read files from srcDir
|
gs.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://raw.githubusercontent.com/FooSoft/goldsmith-samples/master/basic/content/layouts/basic.gohtml) to
|
[template](https://git.foosoft.net/alex/goldsmith-samples/raw/branch/master/basic/content/layouts/basic.gohtml) to
|
||||||
add elements like a header, footer, or a menu; for this we can use the
|
add elements like a header, footer, or a menu; for this we can use the
|
||||||
[Layout](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/frontmatter) plugin:
|
[Layout](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/frontmatter) plugin:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
goldsmith.
|
var gs goldsmith.Goldsmith
|
||||||
Begin(srcDir). // read files from srcDir
|
gs.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-components/plugins/minify) plugin:
|
[Minify](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/minify) plugin:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
goldsmith.
|
var gs goldsmith.Goldsmith
|
||||||
Begin(srcDir). // read files from srcDir
|
gs.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-components/filters/condition) filter to make
|
[Condition](https://godoc.org/git.foosoft.net/alex/goldsmith/filters/condition) filter to make minification occur
|
||||||
minification occur only when we are ready for distribution.
|
only when we are ready for distribution.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
goldsmith.
|
var gs goldsmith.Goldsmith
|
||||||
Begin(srcDir). // read files from srcDir
|
gs.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-components/devserver) to bootstrap a complete
|
[DevServer](https://godoc.org/git.foosoft.net/alex/goldsmith/devserver) to bootstrap a complete development sever
|
||||||
development sever which automatically rebuilds the site whenever source files are updated.
|
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-components/devserver"
|
"git.foosoft.net/alex/goldsmith/devserver"
|
||||||
"git.foosoft.net/alex/goldsmith-components/filters/condition"
|
"git.foosoft.net/alex/goldsmith/filters/condition"
|
||||||
"git.foosoft.net/alex/goldsmith-components/plugins/frontmatter"
|
"git.foosoft.net/alex/goldsmith/plugins/frontmatter"
|
||||||
"git.foosoft.net/alex/goldsmith-components/plugins/layout"
|
"git.foosoft.net/alex/goldsmith/plugins/layout"
|
||||||
"git.foosoft.net/alex/goldsmith-components/plugins/markdown"
|
"git.foosoft.net/alex/goldsmith/plugins/markdown"
|
||||||
"git.foosoft.net/alex/goldsmith-components/plugins/minify"
|
"git.foosoft.net/alex/goldsmith/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) {
|
||||||
errs := goldsmith.
|
var gs goldsmith.Goldsmith
|
||||||
Begin(srcDir). // read files from srcDir
|
errs := gs.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,12 +139,9 @@ to understand, it is often best to learn by example:
|
|||||||
|
|
||||||
Below are some examples of Goldsmith usage which can used to base your site on:
|
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
|
* [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.
|
||||||
is the sample site from the tutorial.
|
* [Bootstrap Sample](https://git.foosoft.net/alex/goldsmith-samples/src/branch/master/bootstrap): a slightly more advanced sample using [Bootstrap](https://getbootstrap.com/).
|
||||||
* [Bootstrap Sample](https://git.foosoft.net/alex/goldsmith-samples/src/branch/master/bootstrap): a slightly more
|
* [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.
|
||||||
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
|
||||||
|
|
||||||
@ -152,55 +149,32 @@ 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-components/plugins/absolute): Convert relative HTML file
|
* [Absolute](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/absolute): Convert relative HTML file references to absolute paths.
|
||||||
references to absolute paths.
|
* [Breadcrumbs](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/breadcrumbs): Generate metadata required to build breadcrumb navigation.
|
||||||
* [Breadcrumbs](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/breadcrumbs): Generate metadata
|
* [Collection](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/collection): Group related pages into named collections.
|
||||||
required to build breadcrumb navigation.
|
* [Document](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/document): Enable simple DOM modification via an API similar to jQuery.
|
||||||
* [Collection](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/collection): Group related pages
|
* [Forward](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/forward): Create simple redirections for pages that have moved to a new URL.
|
||||||
into named collections.
|
* [FrontMatter](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/frontmatter): Extract the JSON, YAML, or TOML metadata stored in your files.
|
||||||
* [Document](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/document): Enable simple DOM
|
* [Index](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/index): Create metadata for directory file listings and generate directory index pages.
|
||||||
modification via an API similar to jQuery.
|
* [Layout](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/layout): Transform your HTML files with Go templates.
|
||||||
* [Forward](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/forward): Create simple redirections
|
* [LiveJs](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/livejs): Inject JavaScript code to automatically reload pages when modified.
|
||||||
for pages that have moved to a new URL.
|
* [Markdown](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/markdown): Render Markdown documents as HTML fragments.
|
||||||
* [FrontMatter](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/frontmatter): Extract the
|
* [Minify](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/minify): Remove superfluous data from a variety of web formats.
|
||||||
JSON, YAML, or TOML metadata stored in your files.
|
* [Pager](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/pager): Split arrays of metadata into standalone pages.
|
||||||
* [Index](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/index): Create metadata for directory
|
* [Rule](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/rule): Update metadata and filter files based on paths.
|
||||||
file listings and generate directory index pages.
|
* [Summary](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/summary): Extract summary and title metadata from HTML files.
|
||||||
* [Layout](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/layout): Transform your HTML files with
|
* [Syndicate](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/syndicate): Generate RSS, Atom, and JSON feeds from existing metadata.
|
||||||
Go templates.
|
* [Syntax](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/syntax): Enable syntax highlighting for pre-formatted code blocks.
|
||||||
* [LiveJs](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/livejs): Inject JavaScript code to
|
* [Tags](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/tags): Generate tag clouds and indices from file metadata.
|
||||||
automatically reload pages when modified.
|
* [Thumbnail](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/thumbnail): Build thumbnails for a variety of common image formats.
|
||||||
* [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-components/filters/condition): Filter files based on a
|
* [Condition](https://godoc.org/git.foosoft.net/alex/goldsmith/filters/condition): Filter files based on a single condition.
|
||||||
single condition.
|
* [Operator](https://godoc.org/git.foosoft.net/alex/goldsmith/filters/operator): Join filters using logical `AND`, `OR`, and `NOT` operators.
|
||||||
* [Operator](https://godoc.org/git.foosoft.net/alex/goldsmith-components/filters/operator): Join filters using
|
* [Wildcard](https://godoc.org/git.foosoft.net/alex/goldsmith/filters/wildcard): Filter files using path wildcards (`*`, `?`, etc.)
|
||||||
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-components/devserver): Simple framework for building,
|
* [DevServer](https://godoc.org/git.foosoft.net/alex/goldsmith/devserver): Simple framework for building, updating, and viewing your site.
|
||||||
updating, and viewing your site.
|
* [Harness](https://godoc.org/git.foosoft.net/alex/goldsmith/harness): Unit test harness for verifying Goldsmith plugins and filters.
|
||||||
* [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,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type cache struct {
|
type cache struct {
|
||||||
@ -15,7 +16,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(context, outputPath, inputFiles)
|
cachePath, err := self.buildCachePath(outputPath, inputFiles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -32,8 +33,8 @@ func (self *cache) retrieveFile(context *Context, outputPath string, inputFiles
|
|||||||
return outputFile, nil
|
return outputFile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *cache) storeFile(context *Context, outputFile *File, inputFiles []*File) error {
|
func (self *cache) storeFile(outputFile *File, inputFiles []*File) error {
|
||||||
cachePath, err := self.buildCachePath(context, outputFile.Path(), inputFiles)
|
cachePath, err := self.buildCachePath(outputFile.Path(), inputFiles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -68,11 +69,13 @@ func (self *cache) storeFile(context *Context, outputFile *File, inputFiles []*F
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *cache) buildCachePath(context *Context, outputPath string, inputFiles []*File) (string, error) {
|
func (self *cache) buildCachePath(outputPath string, inputFiles []*File) (string, error) {
|
||||||
hasher := crc32.NewIEEE()
|
hasher := crc32.NewIEEE()
|
||||||
hasher.Write([]byte(outputPath))
|
hasher.Write([]byte(outputPath))
|
||||||
|
|
||||||
sort.Sort(filesByPath(inputFiles))
|
sort.Slice(inputFiles, func(i, j int) bool {
|
||||||
|
return strings.Compare(inputFiles[i].Path(), inputFiles[j].Path()) < 0
|
||||||
|
})
|
||||||
|
|
||||||
for _, inputFile := range inputFiles {
|
for _, inputFile := range inputFiles {
|
||||||
modTimeBuff := make([]byte, 8)
|
modTimeBuff := make([]byte, 8)
|
||||||
|
32
chain_state.go
Normal file
32
chain_state.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package goldsmith
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type chainState struct {
|
||||||
|
contexts []*Context
|
||||||
|
|
||||||
|
cache *cache
|
||||||
|
filters filterStack
|
||||||
|
clean bool
|
||||||
|
index int
|
||||||
|
|
||||||
|
errors []error
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *chainState) fault(name string, file *File, err error) {
|
||||||
|
self.mutex.Lock()
|
||||||
|
defer self.mutex.Unlock()
|
||||||
|
|
||||||
|
var faultError error
|
||||||
|
if file == nil {
|
||||||
|
faultError = fmt.Errorf("[%s]: %w", name, err)
|
||||||
|
} else {
|
||||||
|
faultError = fmt.Errorf("[%s@%v]: %w", name, file, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.errors = append(self.errors, faultError)
|
||||||
|
}
|
49
context.go
49
context.go
@ -14,8 +14,7 @@ import (
|
|||||||
// Context corresponds to the current link in the chain and provides methods
|
// 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 {
|
||||||
goldsmith *Goldsmith
|
chain *chainState
|
||||||
|
|
||||||
plugin Plugin
|
plugin Plugin
|
||||||
|
|
||||||
filtersExt filterStack
|
filtersExt filterStack
|
||||||
@ -29,15 +28,19 @@ 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(sourcePath string, reader io.Reader) (*File, error) {
|
func (self *Context) CreateFileFromReader(relPath string, reader io.Reader) (*File, error) {
|
||||||
|
if filepath.IsAbs(relPath) {
|
||||||
|
return nil, errors.New("file paths must be relative")
|
||||||
|
}
|
||||||
|
|
||||||
data, err := io.ReadAll(reader)
|
data, err := io.ReadAll(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
file := &File{
|
file := &File{
|
||||||
relPath: sourcePath,
|
relPath: relPath,
|
||||||
props: make(map[string]Prop),
|
props: make(FileProps),
|
||||||
modTime: time.Now(),
|
modTime: time.Now(),
|
||||||
size: int64(len(data)),
|
size: int64(len(data)),
|
||||||
reader: bytes.NewReader(data),
|
reader: bytes.NewReader(data),
|
||||||
@ -48,13 +51,9 @@ func (self *Context) CreateFileFromReader(sourcePath string, reader io.Reader) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateFileFromAsset creates a new file instance from the provided file path.
|
// CreateFileFromAsset creates a new file instance from the provided file path.
|
||||||
func (self *Context) CreateFileFromAsset(sourcePath, dataPath string) (*File, error) {
|
func (self *Context) CreateFileFromAsset(relPath, dataPath string) (*File, error) {
|
||||||
if filepath.IsAbs(sourcePath) {
|
if filepath.IsAbs(relPath) {
|
||||||
return nil, errors.New("source paths must be relative")
|
return nil, errors.New("file 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)
|
||||||
@ -62,12 +61,12 @@ func (self *Context) CreateFileFromAsset(sourcePath, dataPath string) (*File, er
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
return nil, errors.New("assets must be files")
|
return nil, errors.New("file paths cannot be directories")
|
||||||
}
|
}
|
||||||
|
|
||||||
file := &File{
|
file := &File{
|
||||||
relPath: sourcePath,
|
relPath: relPath,
|
||||||
props: make(map[string]Prop),
|
props: make(FileProps),
|
||||||
modTime: info.ModTime(),
|
modTime: info.ModTime(),
|
||||||
size: info.Size(),
|
size: info.Size(),
|
||||||
dataPath: dataPath,
|
dataPath: dataPath,
|
||||||
@ -86,11 +85,11 @@ func (self *Context) DispatchFile(file *File) {
|
|||||||
// dependencies on any input files that are needed to generate it, and then
|
// 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.goldsmith.cache != nil {
|
if self.chain.cache != nil {
|
||||||
self.goldsmith.cache.storeFile(self, outputFile, inputFiles)
|
self.chain.cache.storeFile(outputFile, inputFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.filesOut <- outputFile
|
self.DispatchFile(outputFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RetrieveCachedFile looks up file data (excluding the metadata), given an
|
// RetrieveCachedFile looks up file data (excluding the metadata), given an
|
||||||
@ -98,8 +97,8 @@ func (self *Context) DispatchAndCacheFile(outputFile *File, inputFiles ...*File)
|
|||||||
// will return nil if the desired file is not found in the cache.
|
// 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.goldsmith.cache != nil {
|
if self.chain.cache != nil {
|
||||||
outputFile, _ = self.goldsmith.cache.retrieveFile(self, outputPath, inputFiles)
|
outputFile, _ = self.chain.cache.retrieveFile(self, outputPath, inputFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
return outputFile
|
return outputFile
|
||||||
@ -125,7 +124,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.goldsmith.fault(self.plugin.Name(), nil, err)
|
self.chain.fault(self.plugin.Name(), nil, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -146,13 +145,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.goldsmith.fault("core", inputFile, err)
|
self.chain.fault("core", inputFile, err)
|
||||||
}
|
}
|
||||||
if err := processor.Process(self, inputFile); err != nil {
|
if err := processor.Process(self, inputFile); err != nil {
|
||||||
self.goldsmith.fault(self.plugin.Name(), inputFile, err)
|
self.chain.fault(self.plugin.Name(), inputFile, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.filesOut <- inputFile
|
self.DispatchFile(inputFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -163,7 +162,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.goldsmith.fault(self.plugin.Name(), nil, err)
|
self.chain.fault(self.plugin.Name(), nil, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
115
devserver/devserver.go
Normal file
115
devserver/devserver.go
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
// Package devserver makes it easy to view statically generated websites and
|
||||||
|
// automatically rebuild them when source data changes. When combined with the
|
||||||
|
// "livejs" plugin, it is possible to have a live preview of your site.
|
||||||
|
package devserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Builder interface should be implemented by you to contain the required
|
||||||
|
// goldsmith chain to generate your website.
|
||||||
|
type Builder interface {
|
||||||
|
Build(sourceDir, targetDir, cacheDir string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DevServe should be called to start a web server using the provided builder.
|
||||||
|
// While the source directory will be watched for changes by default, it is
|
||||||
|
// possible to pass in additional directories to watch; modification of these
|
||||||
|
// directories will automatically trigger a site rebuild. This function does
|
||||||
|
// not return and will continue watching for file changes and serving your
|
||||||
|
// website until it is terminated.
|
||||||
|
func DevServe(builder Builder, port int, sourceDir, targetDir, cacheDir string, watchDirs ...string) {
|
||||||
|
dirs := append(watchDirs, sourceDir)
|
||||||
|
build(dirs, func() {
|
||||||
|
builder.Build(sourceDir, targetDir, cacheDir)
|
||||||
|
})
|
||||||
|
|
||||||
|
httpAddr := fmt.Sprintf(":%d", port)
|
||||||
|
httpHandler := http.FileServer(http.Dir(targetDir))
|
||||||
|
|
||||||
|
log.Fatal(http.ListenAndServe(httpAddr, httpHandler))
|
||||||
|
}
|
||||||
|
|
||||||
|
func build(dirs []string, callback func()) {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mutex sync.Mutex
|
||||||
|
timestamp := time.Now()
|
||||||
|
dirty := true
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-watcher.Events:
|
||||||
|
mutex.Lock()
|
||||||
|
timestamp = time.Now()
|
||||||
|
dirty = true
|
||||||
|
mutex.Unlock()
|
||||||
|
|
||||||
|
if event.Op&fsnotify.Create == fsnotify.Create {
|
||||||
|
info, err := os.Stat(event.Name)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
watch(event.Name, watcher)
|
||||||
|
} else {
|
||||||
|
watcher.Add(event.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case err := <-watcher.Errors:
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for range time.Tick(10 * time.Millisecond) {
|
||||||
|
if dirty && time.Now().Sub(timestamp) > 100*time.Millisecond {
|
||||||
|
mutex.Lock()
|
||||||
|
dirty = false
|
||||||
|
mutex.Unlock()
|
||||||
|
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, dir := range dirs {
|
||||||
|
watch(dir, watcher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func watch(dir string, watcher *fsnotify.Watcher) {
|
||||||
|
watcher.Add(dir)
|
||||||
|
|
||||||
|
items, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
fullPath := path.Join(dir, item.Name())
|
||||||
|
if item.IsDir() {
|
||||||
|
watch(fullPath, watcher)
|
||||||
|
} else {
|
||||||
|
watcher.Add(fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
extension.go
Normal file
32
extension.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package goldsmith
|
||||||
|
|
||||||
|
// Plugin contains the minimum set of methods required on plugins. Plugins can
|
||||||
|
// also optionally implement Initializer, Processor, and Finalizer interfaces.
|
||||||
|
type (
|
||||||
|
Plugin interface {
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializer is used to optionally initialize a plugin and to specify a
|
||||||
|
// filter to be used for determining which files will be processed.
|
||||||
|
Initializer interface {
|
||||||
|
Initialize(context *Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processor allows for optional processing of files passing through a plugin.
|
||||||
|
Processor interface {
|
||||||
|
Process(context *Context, file *File) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalizer allows for optional finalization of a plugin after all files
|
||||||
|
// queued in the chain have passed through it.
|
||||||
|
Finalizer interface {
|
||||||
|
Finalize(context *Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter is used to determine which files should continue in the chain.
|
||||||
|
Filter interface {
|
||||||
|
Name() string
|
||||||
|
Accept(file *File) bool
|
||||||
|
}
|
||||||
|
)
|
122
file.go
122
file.go
@ -2,42 +2,38 @@ package goldsmith
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Prop interface{}
|
type (
|
||||||
type PropMap map[string]Prop
|
FileProp any
|
||||||
|
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.
|
||||||
type File struct {
|
File struct {
|
||||||
relPath string
|
relPath string
|
||||||
props map[string]Prop
|
props FileProps
|
||||||
modTime time.Time
|
modTime time.Time
|
||||||
size int64
|
size int64
|
||||||
|
|
||||||
dataPath string
|
dataPath string
|
||||||
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) {
|
func (self *File) Rename(path string) error {
|
||||||
self.relPath = path
|
if filepath.IsAbs(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.reader = bytes.NewReader(data)
|
self.relPath = path
|
||||||
self.modTime = time.Now()
|
|
||||||
self.size = int64(len(data))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,16 +42,16 @@ func (self *File) Path() string {
|
|||||||
return filepath.ToSlash(self.relPath)
|
return filepath.ToSlash(self.relPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name returns the base name of the file.
|
|
||||||
func (self *File) Name() string {
|
|
||||||
return filepath.Base(self.relPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dir returns the containing directory of the file.
|
// Dir returns the containing directory of the file.
|
||||||
func (self *File) Dir() string {
|
func (self *File) Dir() string {
|
||||||
return filepath.ToSlash(filepath.Dir(self.relPath))
|
return filepath.ToSlash(filepath.Dir(self.relPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name returns the base name of the file.
|
||||||
|
func (self *File) Name() string {
|
||||||
|
return filepath.Base(self.relPath)
|
||||||
|
}
|
||||||
|
|
||||||
// Ext returns the extension of the file.
|
// 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)
|
||||||
@ -102,31 +98,29 @@ func (self *File) Seek(offset int64, whence int) (int64, error) {
|
|||||||
return self.reader.Seek(offset, whence)
|
return self.reader.Seek(offset, whence)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns value for string formatting.
|
// GoString returns value for string formatting.
|
||||||
func (self *File) GoString() string {
|
func (self *File) GoString() string {
|
||||||
return self.relPath
|
return self.relPath
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *File) SetProp(name string, value Prop) {
|
// RemoveProp deletes the metadata property for the provided name.
|
||||||
|
func (self *File) RemoveProp(name string) {
|
||||||
|
delete(self.props, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetProp updates the metadata property for the provided name.
|
||||||
|
func (self *File) SetProp(name string, value FileProp) {
|
||||||
self.props[name] = value
|
self.props[name] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *File) CopyProps(file *File) {
|
// Prop returns the metadata property for the provided name.
|
||||||
for key, value := range file.props {
|
func (self *File) Prop(name string) (FileProp, bool) {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *File) Props() PropMap {
|
// PropOrDef returns the metadata property for the provided name or the default.
|
||||||
return self.props
|
func (self *File) PropOrDef(name string, valueDef FileProp) FileProp {
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@ -134,44 +128,16 @@ func (self *File) PropOrDefault(name string, valueDef Prop) Prop {
|
|||||||
return valueDef
|
return valueDef
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *File) export(targetDir string) error {
|
// Props returns all of the metadata properties.
|
||||||
targetPath := filepath.Join(targetDir, self.relPath)
|
func (self *File) Props() FileProps {
|
||||||
|
return self.props
|
||||||
|
}
|
||||||
|
|
||||||
if targetInfo, err := os.Stat(targetPath); err == nil && !targetInfo.ModTime().Before(self.ModTime()) {
|
// CopyProps copies all metadata properties from the provided file.
|
||||||
return nil
|
func (self *File) CopyProps(file *File) {
|
||||||
|
for key, value := range file.props {
|
||||||
|
self.props[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fw, err := os.Create(targetPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer fw.Close()
|
|
||||||
|
|
||||||
if self.reader == nil {
|
|
||||||
fr, err := os.Open(self.dataPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer fr.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(fw, fr); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if _, err := self.Seek(0, io.SeekStart); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := self.WriteTo(fw); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *File) load() error {
|
func (self *File) load() error {
|
||||||
|
90
file_exporter.go
Normal file
90
file_exporter.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package goldsmith
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fileExporter struct {
|
||||||
|
targetDir string
|
||||||
|
clean bool
|
||||||
|
tokens map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*fileExporter) Name() string {
|
||||||
|
return "exporter"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *fileExporter) Initialize(context *Context) error {
|
||||||
|
self.tokens = make(map[string]bool)
|
||||||
|
context.Threads(1)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *fileExporter) Process(context *Context, file *File) error {
|
||||||
|
slicePath := func(path string) string {
|
||||||
|
if filepath.IsAbs(path) {
|
||||||
|
var err error
|
||||||
|
if path, err = filepath.Rel("/", path); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Clean(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
for token := slicePath(file.Path()); token != "."; token = filepath.Dir(token) {
|
||||||
|
self.tokens[token] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(self.targetDir, file.Path())
|
||||||
|
if targetInfo, err := os.Stat(targetPath); err == nil && !targetInfo.ModTime().Before(file.ModTime()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fw, err := os.Create(targetPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fw.Close()
|
||||||
|
|
||||||
|
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := file.WriteTo(fw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *fileExporter) Finalize(context *Context) error {
|
||||||
|
if !self.clean {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Walk(self.targetDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if path == self.targetDir {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
relPath, err := filepath.Rel(self.targetDir, path)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenized, _ := self.tokens[relPath]; !tokenized {
|
||||||
|
if err := os.RemoveAll(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
35
file_importer.go
Normal file
35
file_importer.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package goldsmith
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fileImporter struct {
|
||||||
|
sourceDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*fileImporter) Name() string {
|
||||||
|
return "importer"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *fileImporter) Initialize(context *Context) error {
|
||||||
|
return filepath.Walk(self.sourceDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
relPath, err := filepath.Rel(self.sourceDir, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := context.CreateFileFromAsset(relPath, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
context.DispatchFile(file)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
49
file_util.go
49
file_util.go
@ -1,49 +0,0 @@
|
|||||||
package goldsmith
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type filesByPath []*File
|
|
||||||
|
|
||||||
func (self filesByPath) Len() int {
|
|
||||||
return len(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self filesByPath) Swap(i, j int) {
|
|
||||||
self[i], self[j] = self[j], self[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self filesByPath) Less(i, j int) bool {
|
|
||||||
return strings.Compare(self[i].Path(), self[j].Path()) < 0
|
|
||||||
}
|
|
||||||
|
|
||||||
type fileInfo struct {
|
|
||||||
os.FileInfo
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanPath(path string) string {
|
|
||||||
if filepath.IsAbs(path) {
|
|
||||||
var err error
|
|
||||||
if path, err = filepath.Rel("/", path); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Clean(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func scanDir(rootDir string, infos chan fileInfo) {
|
|
||||||
defer close(infos)
|
|
||||||
|
|
||||||
filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err == nil {
|
|
||||||
infos <- fileInfo{FileInfo: info, path: path}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,11 +1,13 @@
|
|||||||
package goldsmith
|
package goldsmith
|
||||||
|
|
||||||
type filterEntry struct {
|
type (
|
||||||
filter Filter
|
filterEntry struct {
|
||||||
index int
|
filter Filter
|
||||||
}
|
index int
|
||||||
|
}
|
||||||
|
|
||||||
type filterStack []filterEntry
|
filterStack []filterEntry
|
||||||
|
)
|
||||||
|
|
||||||
func (self *filterStack) accept(file *File) bool {
|
func (self *filterStack) accept(file *File) bool {
|
||||||
for _, entry := range *self {
|
for _, entry := range *self {
|
21
filters/condition/condition.go
Normal file
21
filters/condition/condition.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package condition
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.foosoft.net/alex/goldsmith"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Condition struct {
|
||||||
|
accept bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(accept bool) *Condition {
|
||||||
|
return &Condition{accept: accept}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Condition) Name() string {
|
||||||
|
return "condition"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Condition) Accept(file *goldsmith.File) bool {
|
||||||
|
return self.accept
|
||||||
|
}
|
28
filters/condition/condition_test.go
Normal file
28
filters/condition/condition_test.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package condition
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.foosoft.net/alex/goldsmith"
|
||||||
|
"git.foosoft.net/alex/goldsmith/harness"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEnabled(self *testing.T) {
|
||||||
|
harness.ValidateCase(
|
||||||
|
self,
|
||||||
|
"true",
|
||||||
|
func(gs *goldsmith.Goldsmith) {
|
||||||
|
gs.FilterPush(New(true))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisabled(self *testing.T) {
|
||||||
|
harness.ValidateCase(
|
||||||
|
self,
|
||||||
|
"false",
|
||||||
|
func(gs *goldsmith.Goldsmith) {
|
||||||
|
gs.FilterPush(New(false))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
0
filters/condition/testdata/false/source/file_1.txt
vendored
Normal file
0
filters/condition/testdata/false/source/file_1.txt
vendored
Normal file
0
filters/condition/testdata/false/source/file_2.txt
vendored
Normal file
0
filters/condition/testdata/false/source/file_2.txt
vendored
Normal file
0
filters/condition/testdata/true/reference/file_1.txt
vendored
Normal file
0
filters/condition/testdata/true/reference/file_1.txt
vendored
Normal file
0
filters/condition/testdata/true/reference/file_2.txt
vendored
Normal file
0
filters/condition/testdata/true/reference/file_2.txt
vendored
Normal file
0
filters/condition/testdata/true/source/file_1.txt
vendored
Normal file
0
filters/condition/testdata/true/source/file_1.txt
vendored
Normal file
0
filters/condition/testdata/true/source/file_2.txt
vendored
Normal file
0
filters/condition/testdata/true/source/file_2.txt
vendored
Normal file
69
filters/operator/operator.go
Normal file
69
filters/operator/operator.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.foosoft.net/alex/goldsmith"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Operator interface {
|
||||||
|
goldsmith.Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func And(filters ...goldsmith.Filter) Operator {
|
||||||
|
return &operatorAnd{filters}
|
||||||
|
}
|
||||||
|
|
||||||
|
type operatorAnd struct {
|
||||||
|
filters []goldsmith.Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*operatorAnd) Name() string {
|
||||||
|
return "operator"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *operatorAnd) Accept(file *goldsmith.File) bool {
|
||||||
|
for _, filter := range self.filters {
|
||||||
|
if !filter.Accept(file) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func Not(self goldsmith.Filter) Operator {
|
||||||
|
return &operatorNot{self}
|
||||||
|
}
|
||||||
|
|
||||||
|
type operatorNot struct {
|
||||||
|
filter goldsmith.Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*operatorNot) Name() string {
|
||||||
|
return "operator"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *operatorNot) Accept(file *goldsmith.File) bool {
|
||||||
|
return !self.filter.Accept(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Or(self ...goldsmith.Filter) Operator {
|
||||||
|
return &operatorOr{self}
|
||||||
|
}
|
||||||
|
|
||||||
|
type operatorOr struct {
|
||||||
|
filters []goldsmith.Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*operatorOr) Name() string {
|
||||||
|
return "operator"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *operatorOr) Accept(file *goldsmith.File) bool {
|
||||||
|
for _, filter := range self.filters {
|
||||||
|
if filter.Accept(file) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
109
filters/operator/operator_test.go
Normal file
109
filters/operator/operator_test.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.foosoft.net/alex/goldsmith"
|
||||||
|
"git.foosoft.net/alex/goldsmith/filters/condition"
|
||||||
|
"git.foosoft.net/alex/goldsmith/harness"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAndFalse(t *testing.T) {
|
||||||
|
harness.ValidateCase(
|
||||||
|
t,
|
||||||
|
"and_false",
|
||||||
|
func(gs *goldsmith.Goldsmith) {
|
||||||
|
gs.FilterPush(And(condition.New(false)))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAndFalseTrue(t *testing.T) {
|
||||||
|
harness.ValidateCase(
|
||||||
|
t,
|
||||||
|
"and_false_true",
|
||||||
|
func(gs *goldsmith.Goldsmith) {
|
||||||
|
gs.FilterPush(And(condition.New(false), condition.New(true)))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAndTrueFalse(t *testing.T) {
|
||||||
|
harness.ValidateCase(
|
||||||
|
t,
|
||||||
|
"and_true_false",
|
||||||
|
func(gs *goldsmith.Goldsmith) {
|
||||||
|
gs.FilterPush(And(condition.New(true), condition.New(false)))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAndTrue(t *testing.T) {
|
||||||
|
harness.ValidateCase(
|
||||||
|
t,
|
||||||
|
"and_true",
|
||||||
|
func(gs *goldsmith.Goldsmith) {
|
||||||
|
gs.FilterPush(And(condition.New(true)))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOrFalse(t *testing.T) {
|
||||||
|
harness.ValidateCase(
|
||||||
|
t,
|
||||||
|
"or_false",
|
||||||
|
func(gs *goldsmith.Goldsmith) {
|
||||||
|
gs.FilterPush(Or(condition.New(false)))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOrFalseTrue(t *testing.T) {
|
||||||
|
harness.ValidateCase(
|
||||||
|
t,
|
||||||
|
"or_false_true",
|
||||||
|
func(gs *goldsmith.Goldsmith) {
|
||||||
|
gs.FilterPush(Or(condition.New(false), condition.New(true)))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOrTrueFalse(t *testing.T) {
|
||||||
|
harness.ValidateCase(
|
||||||
|
t,
|
||||||
|
"or_true_false",
|
||||||
|
func(gs *goldsmith.Goldsmith) {
|
||||||
|
gs.FilterPush(Or(condition.New(true), condition.New(false)))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOrTrue(t *testing.T) {
|
||||||
|
harness.ValidateCase(
|
||||||
|
t,
|
||||||
|
"or_true",
|
||||||
|
func(gs *goldsmith.Goldsmith) {
|
||||||
|
gs.FilterPush(Or(condition.New(true)))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotFalse(t *testing.T) {
|
||||||
|
harness.ValidateCase(
|
||||||
|
t,
|
||||||
|
"not_false",
|
||||||
|
func(gs *goldsmith.Goldsmith) {
|
||||||
|
gs.FilterPush(Not(condition.New(false)))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotTrue(t *testing.T) {
|
||||||
|
harness.ValidateCase(
|
||||||
|
t,
|
||||||
|
"not_true",
|
||||||
|
func(gs *goldsmith.Goldsmith) {
|
||||||
|
gs.FilterPush(Not(condition.New(true)))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
0
filters/operator/testdata/and_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_false_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_false_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_false_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_false_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_true/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_true/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_true/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_true/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_true_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_true_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/and_true_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/and_true_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/not_false/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/not_false/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/not_false/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/not_false/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/not_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/not_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/not_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/not_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/not_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/not_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/not_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/not_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_false_true/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_false_true/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_false_true/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_false_true/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_false_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_false_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_false_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_false_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_true/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_true/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_true/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_true/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_true/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_true/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_true_false/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_true_false/reference/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_true_false/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_true_false/reference/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_true_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_true_false/source/file_1.txt
vendored
Normal file
0
filters/operator/testdata/or_true_false/source/file_2.txt
vendored
Normal file
0
filters/operator/testdata/or_true_false/source/file_2.txt
vendored
Normal file
0
filters/wildcard/testdata/reference/child/test_5.txt
vendored
Normal file
0
filters/wildcard/testdata/reference/child/test_5.txt
vendored
Normal file
0
filters/wildcard/testdata/reference/test_1.txt
vendored
Normal file
0
filters/wildcard/testdata/reference/test_1.txt
vendored
Normal file
0
filters/wildcard/testdata/reference/test_2.md
vendored
Normal file
0
filters/wildcard/testdata/reference/test_2.md
vendored
Normal file
0
filters/wildcard/testdata/source/child/test_3.json
vendored
Normal file
0
filters/wildcard/testdata/source/child/test_3.json
vendored
Normal file
0
filters/wildcard/testdata/source/child/test_5.txt
vendored
Normal file
0
filters/wildcard/testdata/source/child/test_5.txt
vendored
Normal file
0
filters/wildcard/testdata/source/child/test_7.md
vendored
Normal file
0
filters/wildcard/testdata/source/child/test_7.md
vendored
Normal file
0
filters/wildcard/testdata/source/child/test_8.xml
vendored
Normal file
0
filters/wildcard/testdata/source/child/test_8.xml
vendored
Normal file
0
filters/wildcard/testdata/source/test_1.txt
vendored
Normal file
0
filters/wildcard/testdata/source/test_1.txt
vendored
Normal file
0
filters/wildcard/testdata/source/test_2.md
vendored
Normal file
0
filters/wildcard/testdata/source/test_2.md
vendored
Normal file
0
filters/wildcard/testdata/source/test_3.json
vendored
Normal file
0
filters/wildcard/testdata/source/test_3.json
vendored
Normal file
0
filters/wildcard/testdata/source/test_4.xml
vendored
Normal file
0
filters/wildcard/testdata/source/test_4.xml
vendored
Normal file
47
filters/wildcard/wildcard.go
Normal file
47
filters/wildcard/wildcard.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package wildcard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.foosoft.net/alex/goldsmith"
|
||||||
|
"github.com/bmatcuk/doublestar/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Wildcard struct {
|
||||||
|
wildcards []string
|
||||||
|
caseSensitive bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(wildcards ...string) *Wildcard {
|
||||||
|
return &Wildcard{wildcards: wildcards}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Wildcard) CaseSensitive(caseSensitive bool) *Wildcard {
|
||||||
|
self.caseSensitive = caseSensitive
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Wildcard) Name() string {
|
||||||
|
return "wildcard"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Wildcard) Accept(file *goldsmith.File) bool {
|
||||||
|
filePath := self.adjustCase(file.Path())
|
||||||
|
|
||||||
|
for _, wildcard := range self.wildcards {
|
||||||
|
wildcard = self.adjustCase(wildcard)
|
||||||
|
if matched, _ := doublestar.PathMatch(wildcard, filePath); matched {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Wildcard) adjustCase(str string) string {
|
||||||
|
if self.caseSensitive {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ToLower(str)
|
||||||
|
}
|
17
filters/wildcard/wildcard_test.go
Normal file
17
filters/wildcard/wildcard_test.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package wildcard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.foosoft.net/alex/goldsmith"
|
||||||
|
"git.foosoft.net/alex/goldsmith/harness"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test(t *testing.T) {
|
||||||
|
harness.Validate(
|
||||||
|
t,
|
||||||
|
func(gs *goldsmith.Goldsmith) {
|
||||||
|
gs.FilterPush(New("**/*.txt", "*.md"))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
27
go.mod
27
go.mod
@ -1,3 +1,28 @@
|
|||||||
module git.foosoft.net/alex/goldsmith
|
module git.foosoft.net/alex/goldsmith
|
||||||
|
|
||||||
go 1.13
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.3.2
|
||||||
|
github.com/PuerkitoBio/goquery v1.8.1
|
||||||
|
github.com/alecthomas/chroma v0.10.0
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.6.1
|
||||||
|
github.com/disintegration/imaging v1.6.2
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0
|
||||||
|
github.com/gorilla/feeds v1.1.2
|
||||||
|
github.com/tdewolff/minify/v2 v2.20.17
|
||||||
|
github.com/yuin/goldmark v1.7.0
|
||||||
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||||
|
github.com/stretchr/testify v1.8.4 // indirect
|
||||||
|
github.com/tdewolff/parse/v2 v2.7.12 // indirect
|
||||||
|
golang.org/x/image v0.15.0 // indirect
|
||||||
|
golang.org/x/net v0.21.0 // indirect
|
||||||
|
golang.org/x/sys v0.17.0 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
|
)
|
||||||
|
96
go.sum
Normal file
96
go.sum
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||||
|
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
|
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||||
|
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||||
|
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||||
|
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||||
|
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||||
|
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||||
|
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
|
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
|
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||||
|
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
github.com/gorilla/feeds v1.1.2 h1:pxzZ5PD3RJdhFH2FsJJ4x6PqMqbgFk1+Vez4XWBW8Iw=
|
||||||
|
github.com/gorilla/feeds v1.1.2/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/tdewolff/minify/v2 v2.20.17 h1:zGqEDhspr3XjSrQI/56vw9IdAhLAaKTLXWnDBsxNVt8=
|
||||||
|
github.com/tdewolff/minify/v2 v2.20.17/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM=
|
||||||
|
github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ=
|
||||||
|
github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
|
||||||
|
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||||
|
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
|
||||||
|
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||||
|
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
|
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
79
goldsmith.go
79
goldsmith.go
@ -1,107 +1,78 @@
|
|||||||
// Package goldsmith generates static websites.
|
// Package goldsmith generates static websites.
|
||||||
package goldsmith
|
package goldsmith
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Goldsmith chainable context.
|
// Goldsmith chainable context.
|
||||||
type Goldsmith struct {
|
type Goldsmith struct {
|
||||||
sourceDir string
|
chain *chainState
|
||||||
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 Begin(sourceDir string) *Goldsmith {
|
func (self *Goldsmith) Begin(sourceDir string) *Goldsmith {
|
||||||
goldsmith := &Goldsmith{sourceDir: sourceDir}
|
self.chain = &chainState{}
|
||||||
goldsmith.Chain(&loader{})
|
self.Chain(&fileImporter{sourceDir: sourceDir})
|
||||||
return goldsmith
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.cache = &cache{cacheDir}
|
self.chain.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.clean = clean
|
self.chain.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{
|
||||||
goldsmith: self,
|
chain: self.chain,
|
||||||
plugin: plugin,
|
plugin: plugin,
|
||||||
filtersExt: append(filterStack(nil), self.filters...),
|
filtersExt: append(filterStack(nil), self.chain.filters...),
|
||||||
index: self.index,
|
index: self.chain.index,
|
||||||
filesOut: make(chan *File),
|
filesOut: make(chan *File),
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(self.contexts) > 0 {
|
if len(self.chain.contexts) > 0 {
|
||||||
context.filesIn = self.contexts[len(self.contexts)-1].filesOut
|
context.filesIn = self.chain.contexts[len(self.chain.contexts)-1].filesOut
|
||||||
}
|
}
|
||||||
|
|
||||||
self.contexts = append(self.contexts, context)
|
self.chain.contexts = append(self.chain.contexts, context)
|
||||||
self.index++
|
self.chain.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.filters.push(filter, self.index)
|
self.chain.filters.push(filter, self.chain.index)
|
||||||
self.index++
|
self.chain.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.filters.pop()
|
self.chain.filters.pop()
|
||||||
self.index++
|
self.chain.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.targetDir = targetDir
|
self.Chain(&fileExporter{targetDir: targetDir, clean: self.chain.clean})
|
||||||
|
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.contexts[len(self.contexts)-1]
|
context := self.chain.contexts[len(self.chain.contexts)-1]
|
||||||
for range context.filesOut {
|
for range context.filesOut {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.errors
|
errors := self.chain.errors
|
||||||
}
|
self.chain = nil
|
||||||
|
|
||||||
func (self *Goldsmith) fault(name string, file *File, err error) {
|
return errors
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
111
harness/harness.go
Normal file
111
harness/harness.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
// Package harness provides a simple way to test goldsmith plugins and filters.
|
||||||
|
// It executes a goldsmith chain on provided "source" data and compares the
|
||||||
|
// generated "target" resuts with the known to be good "reference" data.
|
||||||
|
package harness
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"hash/crc32"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.foosoft.net/alex/goldsmith"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StagingCallback callback function is used to set up a goldsmith chain.
|
||||||
|
type StagingCallback func(gs *goldsmith.Goldsmith)
|
||||||
|
|
||||||
|
// Validate enables validation of a single, unnamed case (test data is stored in "testdata").
|
||||||
|
func Validate(t *testing.T, stager StagingCallback) {
|
||||||
|
ValidateCase(t, "", stager)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCase enables enables of a single, named case (test data is stored in "testdata/caseName").
|
||||||
|
func ValidateCase(t *testing.T, caseName string, stager StagingCallback) {
|
||||||
|
var (
|
||||||
|
caseDir = filepath.Join("testdata", caseName)
|
||||||
|
sourceDir = filepath.Join(caseDir, "source")
|
||||||
|
targetDir = filepath.Join(caseDir, "target")
|
||||||
|
cacheDir = filepath.Join(caseDir, "cache")
|
||||||
|
referenceDir = filepath.Join(caseDir, "reference")
|
||||||
|
)
|
||||||
|
|
||||||
|
if errs := validate(sourceDir, targetDir, cacheDir, referenceDir, stager); len(errs) > 0 {
|
||||||
|
for _, err := range errs {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate(sourceDir, targetDir, cacheDir, referenceDir string, stager StagingCallback) []error {
|
||||||
|
if err := os.RemoveAll(targetDir); err != nil {
|
||||||
|
return []error{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.RemoveAll(cacheDir); err != nil {
|
||||||
|
return []error{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
defer os.RemoveAll(cacheDir)
|
||||||
|
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
if errs := execute(sourceDir, targetDir, cacheDir, stager); errs != nil {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
if hashDirState(targetDir) != hashDirState(referenceDir) {
|
||||||
|
return []error{errors.New("directory contents do not match")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(sourceDir, targetDir, cacheDir string, stager StagingCallback) []error {
|
||||||
|
var gs goldsmith.Goldsmith
|
||||||
|
gs.Begin(sourceDir).Cache(cacheDir).Clean(true)
|
||||||
|
stager(&gs)
|
||||||
|
return gs.End(targetDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashDirState(dir string) uint32 {
|
||||||
|
hasher := crc32.NewIEEE()
|
||||||
|
|
||||||
|
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
relPath, err := filepath.Rel(dir, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := fmt.Sprintf("%s %t", relPath, info.IsDir())
|
||||||
|
if _, err := hasher.Write([]byte(summary)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.IsDir() {
|
||||||
|
fp, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(hasher, fp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return hasher.Sum32()
|
||||||
|
}
|
30
interface.go
30
interface.go
@ -1,30 +0,0 @@
|
|||||||
package goldsmith
|
|
||||||
|
|
||||||
// Plugin contains the minimum set of methods required on plugins. Plugins can
|
|
||||||
// also optionally implement Initializer, Processor, and Finalizer interfaces.
|
|
||||||
type Plugin interface {
|
|
||||||
Name() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initializer is used to optionally initialize a plugin and to specify a
|
|
||||||
// filter to be used for determining which files will be processed.
|
|
||||||
type Initializer interface {
|
|
||||||
Initialize(context *Context) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Processor allows for optional processing of files passing through a plugin.
|
|
||||||
type Processor interface {
|
|
||||||
Process(context *Context, file *File) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finalizer allows for optional finalization of a plugin after all files
|
|
||||||
// queued in the chain have passed through it.
|
|
||||||
type Finalizer interface {
|
|
||||||
Finalize(context *Context) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter is used to determine which files should continue in the chain.
|
|
||||||
type Filter interface {
|
|
||||||
Name() string
|
|
||||||
Accept(file *File) bool
|
|
||||||
}
|
|
30
loader.go
30
loader.go
@ -1,30 +0,0 @@
|
|||||||
package goldsmith
|
|
||||||
|
|
||||||
import "path/filepath"
|
|
||||||
|
|
||||||
type loader struct{}
|
|
||||||
|
|
||||||
func (*loader) Name() string {
|
|
||||||
return "loader"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*loader) Initialize(context *Context) error {
|
|
||||||
scannedInfo := make(chan fileInfo)
|
|
||||||
go scanDir(context.goldsmith.sourceDir, scannedInfo)
|
|
||||||
|
|
||||||
for info := range scannedInfo {
|
|
||||||
if info.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
relPath, _ := filepath.Rel(context.goldsmith.sourceDir, info.path)
|
|
||||||
file, err := context.CreateFileFromAsset(relPath, info.path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
context.DispatchFile(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
114
plugins/absolute/absolute.go
Normal file
114
plugins/absolute/absolute.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// Package absolute converts relative file references in HTML documents to
|
||||||
|
// absolute paths. This is useful when working with plugins like "layout" and
|
||||||
|
// "collection", which can render a page’s content from the context of a
|
||||||
|
// different directory (imagine an index page showing inline previews of blog
|
||||||
|
// posts). This plugin makes it easy to fix incorrect relative file references
|
||||||
|
// by making sure all paths are absolute before content is featured on other
|
||||||
|
// sections of your site.
|
||||||
|
package absolute
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"git.foosoft.net/alex/goldsmith"
|
||||||
|
"git.foosoft.net/alex/goldsmith/filters/wildcard"
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Absolute chainable plugin context.
|
||||||
|
type Absolute struct {
|
||||||
|
attributes []string
|
||||||
|
baseUrl *url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates absolute new instance of the Absolute plugin.
|
||||||
|
func New() *Absolute {
|
||||||
|
return &Absolute{attributes: []string{"href", "src"}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attributes sets the attributes which are scanned for relative URLs (default: "href", "src").
|
||||||
|
func (self *Absolute) Attributes(attributes ...string) *Absolute {
|
||||||
|
self.attributes = attributes
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseUrl sets the base URL which is prepended to absolute-converted relative paths.
|
||||||
|
func (self *Absolute) BaseUrl(baseUrl string) *Absolute {
|
||||||
|
self.baseUrl, _ = url.Parse(baseUrl)
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Absolute) Name() string {
|
||||||
|
return "absolute"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Absolute) Initialize(context *goldsmith.Context) error {
|
||||||
|
context.Filter(wildcard.New("**/*.html", "**/*.htm"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Absolute) Process(context *goldsmith.Context, inputFile *goldsmith.File) error {
|
||||||
|
if outputFile := context.RetrieveCachedFile(inputFile.Path(), inputFile); outputFile != nil {
|
||||||
|
outputFile.CopyProps(inputFile)
|
||||||
|
context.DispatchFile(outputFile)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fileUrl, err := url.Parse(inputFile.Path())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, err := goquery.NewDocumentFromReader(inputFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, attribute := range self.attributes {
|
||||||
|
cssPath := fmt.Sprintf("*[%s]", attribute)
|
||||||
|
doc.Find(cssPath).Each(func(index int, selection *goquery.Selection) {
|
||||||
|
value, exists := selection.Attr(attribute)
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currUrl, err := url.Parse(value)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if currUrl.IsAbs() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currUrl = fileUrl.ResolveReference(currUrl)
|
||||||
|
if self.baseUrl != nil {
|
||||||
|
rebasedUrl := *self.baseUrl
|
||||||
|
rebasedUrl.Path = path.Join(rebasedUrl.Path, currUrl.Path)
|
||||||
|
rebasedUrl.Fragment = currUrl.Fragment
|
||||||
|
rebasedUrl.RawFragment = currUrl.RawFragment
|
||||||
|
rebasedUrl.RawQuery = currUrl.RawQuery
|
||||||
|
currUrl = &rebasedUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
selection.SetAttr(attribute, currUrl.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
html, err := doc.Html()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFile, err := context.CreateFileFromReader(inputFile.Path(), bytes.NewReader([]byte(html)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFile.CopyProps(inputFile)
|
||||||
|
context.DispatchAndCacheFile(outputFile, inputFile)
|
||||||
|
return nil
|
||||||
|
}
|
17
plugins/absolute/absolute_test.go
Normal file
17
plugins/absolute/absolute_test.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package absolute
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.foosoft.net/alex/goldsmith"
|
||||||
|
"git.foosoft.net/alex/goldsmith/harness"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test(self *testing.T) {
|
||||||
|
harness.Validate(
|
||||||
|
self,
|
||||||
|
func(gs *goldsmith.Goldsmith) {
|
||||||
|
gs.Chain(New().BaseUrl("https://foosoft.net"))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
7
plugins/absolute/testdata/reference/dir/index.html
vendored
Normal file
7
plugins/absolute/testdata/reference/dir/index.html
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<html><head></head><body>
|
||||||
|
<a href="https://foosoft.net/">Relative link</a>
|
||||||
|
<a href="https://foosoft.net">Absolute link</a>
|
||||||
|
<a href="https://www.example.com/dir/index.html">External link</a>
|
||||||
|
|
||||||
|
|
||||||
|
</body></html>
|
7
plugins/absolute/testdata/reference/index.html
vendored
Normal file
7
plugins/absolute/testdata/reference/index.html
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<html><head></head><body>
|
||||||
|
<a href="https://foosoft.net/dir/index.html#anchor">Relative link</a>
|
||||||
|
<a href="https://foosoft.net/index.html?query">Absolute link</a>
|
||||||
|
<a href="https://www.example.com/dir/index.html">External link</a>
|
||||||
|
|
||||||
|
|
||||||
|
</body></html>
|
7
plugins/absolute/testdata/source/dir/index.html
vendored
Normal file
7
plugins/absolute/testdata/source/dir/index.html
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<a href="../">Relative link</a>
|
||||||
|
<a href="https://foosoft.net">Absolute link</a>
|
||||||
|
<a href="https://www.example.com/dir/index.html">External link</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
7
plugins/absolute/testdata/source/index.html
vendored
Normal file
7
plugins/absolute/testdata/source/index.html
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<a href="dir/index.html#anchor">Relative link</a>
|
||||||
|
<a href="/index.html?query">Absolute link</a>
|
||||||
|
<a href="https://www.example.com/dir/index.html">External link</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
131
plugins/breadcrumbs/breadcrumbs.go
Normal file
131
plugins/breadcrumbs/breadcrumbs.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// Package breadcrumbs generates metadata required to enable breadcrumb
|
||||||
|
// navigation. This is particularly helpful for sites that have deep
|
||||||
|
// hierarchies which may be otherwise confusing to visitors.
|
||||||
|
package breadcrumbs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.foosoft.net/alex/goldsmith"
|
||||||
|
"git.foosoft.net/alex/goldsmith/filters/wildcard"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Crumb provides organizational information about this node and ones before it.
|
||||||
|
type Crumb struct {
|
||||||
|
Ancestors []*Node
|
||||||
|
Node *Node
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node represents information about a specific file in the site's structure.
|
||||||
|
type Node struct {
|
||||||
|
File *goldsmith.File
|
||||||
|
Parent *Node
|
||||||
|
Children []*Node
|
||||||
|
|
||||||
|
parentName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Breadcrumbs chainable plugin context.
|
||||||
|
type Breadcrumbs struct {
|
||||||
|
nameKey string
|
||||||
|
parentKey string
|
||||||
|
crumbsKey string
|
||||||
|
|
||||||
|
allNodes []*Node
|
||||||
|
namedNodes map[string]*Node
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new instance of the Breadcrumbs plugin.
|
||||||
|
func New() *Breadcrumbs {
|
||||||
|
return &Breadcrumbs{
|
||||||
|
nameKey: "CrumbName",
|
||||||
|
parentKey: "CrumbParent",
|
||||||
|
crumbsKey: "Crumbs",
|
||||||
|
namedNodes: make(map[string]*Node),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameKey sets the metadata key used to access the crumb name (default: "CrumbName").
|
||||||
|
// Crumb names must be globally unique within any given website.
|
||||||
|
func (self *Breadcrumbs) NameKey(key string) *Breadcrumbs {
|
||||||
|
self.nameKey = key
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParentKey sets the metadata key used to access the parent name (default: "CrumbParent").
|
||||||
|
func (self *Breadcrumbs) ParentKey(key string) *Breadcrumbs {
|
||||||
|
self.parentKey = key
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
// CrumbsKey sets the metadata key used to store information about crumbs (default: "Crumbs").
|
||||||
|
func (self *Breadcrumbs) CrumbsKey(key string) *Breadcrumbs {
|
||||||
|
self.crumbsKey = key
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Breadcrumbs) Name() string {
|
||||||
|
return "breadcrumbs"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Breadcrumbs) Initialize(context *goldsmith.Context) error {
|
||||||
|
context.Filter(wildcard.New("**/*.html", "**/*.htm"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Breadcrumbs) Process(context *goldsmith.Context, inputFile *goldsmith.File) error {
|
||||||
|
var parentNameStr string
|
||||||
|
if parentName, ok := inputFile.Prop(self.parentKey); ok {
|
||||||
|
parentNameStr, _ = parentName.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodeNameStr string
|
||||||
|
if nodeName, ok := inputFile.Prop(self.nameKey); ok {
|
||||||
|
nodeNameStr, _ = nodeName.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mutex.Lock()
|
||||||
|
defer self.mutex.Unlock()
|
||||||
|
|
||||||
|
node := &Node{File: inputFile, parentName: parentNameStr}
|
||||||
|
self.allNodes = append(self.allNodes, node)
|
||||||
|
|
||||||
|
if len(nodeNameStr) > 0 {
|
||||||
|
if _, ok := self.namedNodes[nodeNameStr]; ok {
|
||||||
|
return fmt.Errorf("duplicate node: %s", nodeNameStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.namedNodes[nodeNameStr] = node
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Breadcrumbs) Finalize(context *goldsmith.Context) error {
|
||||||
|
for _, node := range self.allNodes {
|
||||||
|
if len(node.parentName) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if parentNode, ok := self.namedNodes[node.parentName]; ok {
|
||||||
|
parentNode.Children = append(parentNode.Children, node)
|
||||||
|
node.Parent = parentNode
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("undefined parent: %s", node.parentName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, node := range self.allNodes {
|
||||||
|
var ancestors []*Node
|
||||||
|
for currentNode := node.Parent; currentNode != nil; currentNode = currentNode.Parent {
|
||||||
|
ancestors = append([]*Node{currentNode}, ancestors...)
|
||||||
|
}
|
||||||
|
|
||||||
|
node.File.SetProp(self.crumbsKey, Crumb{ancestors, node})
|
||||||
|
context.DispatchFile(node.File)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
22
plugins/breadcrumbs/breadcrumbs_test.go
Normal file
22
plugins/breadcrumbs/breadcrumbs_test.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package breadcrumbs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.foosoft.net/alex/goldsmith"
|
||||||
|
"git.foosoft.net/alex/goldsmith/harness"
|
||||||
|
"git.foosoft.net/alex/goldsmith/plugins/frontmatter"
|
||||||
|
"git.foosoft.net/alex/goldsmith/plugins/layout"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test(self *testing.T) {
|
||||||
|
harness.Validate(
|
||||||
|
self,
|
||||||
|
func(gs *goldsmith.Goldsmith) {
|
||||||
|
gs.
|
||||||
|
Chain(frontmatter.New()).
|
||||||
|
Chain(New()).
|
||||||
|
Chain(layout.New())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
26
plugins/breadcrumbs/testdata/reference/child_1.html
vendored
Normal file
26
plugins/breadcrumbs/testdata/reference/child_1.html
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Child 1</h1>
|
||||||
|
<ul>
|
||||||
|
<li><a href="child_1.html">Child 1</a></li>
|
||||||
|
<li><a href="child_2.html">Child 2</a></li>
|
||||||
|
<li><a href="child_3.html">Child 3</a></li>
|
||||||
|
<li><a href="child_4.html">Child 4</a></li>
|
||||||
|
<li><a href="parent_1.html">Parent 1</a></li>
|
||||||
|
<li><a href="parent_2.html">Parent 2</a></li>
|
||||||
|
<li><a href="root_1.html">Root 1</a></li>
|
||||||
|
<li><a href="root_2.html">Root 2</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<a href="root_1.html" class="breadcrumb-item">Root 1</a> >
|
||||||
|
|
||||||
|
<a href="parent_1.html" class="breadcrumb-item">Parent 1</a> >
|
||||||
|
|
||||||
|
<span class="breadcrumb-item active">Child 1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
26
plugins/breadcrumbs/testdata/reference/child_2.html
vendored
Normal file
26
plugins/breadcrumbs/testdata/reference/child_2.html
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Child 2</h1>
|
||||||
|
<ul>
|
||||||
|
<li><a href="child_1.html">Child 1</a></li>
|
||||||
|
<li><a href="child_2.html">Child 2</a></li>
|
||||||
|
<li><a href="child_3.html">Child 3</a></li>
|
||||||
|
<li><a href="child_4.html">Child 4</a></li>
|
||||||
|
<li><a href="parent_1.html">Parent 1</a></li>
|
||||||
|
<li><a href="parent_2.html">Parent 2</a></li>
|
||||||
|
<li><a href="root_1.html">Root 1</a></li>
|
||||||
|
<li><a href="root_2.html">Root 2</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<a href="root_1.html" class="breadcrumb-item">Root 1</a> >
|
||||||
|
|
||||||
|
<a href="parent_1.html" class="breadcrumb-item">Parent 1</a> >
|
||||||
|
|
||||||
|
<span class="breadcrumb-item active">Child 2</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
26
plugins/breadcrumbs/testdata/reference/child_3.html
vendored
Normal file
26
plugins/breadcrumbs/testdata/reference/child_3.html
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Child 3</h1>
|
||||||
|
<ul>
|
||||||
|
<li><a href="child_1.html">Child 1</a></li>
|
||||||
|
<li><a href="child_2.html">Child 2</a></li>
|
||||||
|
<li><a href="child_3.html">Child 3</a></li>
|
||||||
|
<li><a href="child_4.html">Child 4</a></li>
|
||||||
|
<li><a href="parent_1.html">Parent 1</a></li>
|
||||||
|
<li><a href="parent_2.html">Parent 2</a></li>
|
||||||
|
<li><a href="root_1.html">Root 1</a></li>
|
||||||
|
<li><a href="root_2.html">Root 2</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<a href="root_1.html" class="breadcrumb-item">Root 1</a> >
|
||||||
|
|
||||||
|
<a href="parent_2.html" class="breadcrumb-item">Parent 2</a> >
|
||||||
|
|
||||||
|
<span class="breadcrumb-item active">Child 3</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
26
plugins/breadcrumbs/testdata/reference/child_4.html
vendored
Normal file
26
plugins/breadcrumbs/testdata/reference/child_4.html
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Child 4</h1>
|
||||||
|
<ul>
|
||||||
|
<li><a href="child_1.html">Child 1</a></li>
|
||||||
|
<li><a href="child_2.html">Child 2</a></li>
|
||||||
|
<li><a href="child_3.html">Child 3</a></li>
|
||||||
|
<li><a href="child_4.html">Child 4</a></li>
|
||||||
|
<li><a href="parent_1.html">Parent 1</a></li>
|
||||||
|
<li><a href="parent_2.html">Parent 2</a></li>
|
||||||
|
<li><a href="root_1.html">Root 1</a></li>
|
||||||
|
<li><a href="root_2.html">Root 2</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<a href="root_1.html" class="breadcrumb-item">Root 1</a> >
|
||||||
|
|
||||||
|
<a href="parent_2.html" class="breadcrumb-item">Parent 2</a> >
|
||||||
|
|
||||||
|
<span class="breadcrumb-item active">Child 4</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
24
plugins/breadcrumbs/testdata/reference/parent_1.html
vendored
Normal file
24
plugins/breadcrumbs/testdata/reference/parent_1.html
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Parent 1</h1>
|
||||||
|
<ul>
|
||||||
|
<li><a href="child_1.html">Child 1</a></li>
|
||||||
|
<li><a href="child_2.html">Child 2</a></li>
|
||||||
|
<li><a href="child_3.html">Child 3</a></li>
|
||||||
|
<li><a href="child_4.html">Child 4</a></li>
|
||||||
|
<li><a href="parent_1.html">Parent 1</a></li>
|
||||||
|
<li><a href="parent_2.html">Parent 2</a></li>
|
||||||
|
<li><a href="root_1.html">Root 1</a></li>
|
||||||
|
<li><a href="root_2.html">Root 2</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<a href="root_1.html" class="breadcrumb-item">Root 1</a> >
|
||||||
|
|
||||||
|
<span class="breadcrumb-item active">Parent 1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
24
plugins/breadcrumbs/testdata/reference/parent_2.html
vendored
Normal file
24
plugins/breadcrumbs/testdata/reference/parent_2.html
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Parent 2</h1>
|
||||||
|
<ul>
|
||||||
|
<li><a href="child_1.html">Child 1</a></li>
|
||||||
|
<li><a href="child_2.html">Child 2</a></li>
|
||||||
|
<li><a href="child_3.html">Child 3</a></li>
|
||||||
|
<li><a href="child_4.html">Child 4</a></li>
|
||||||
|
<li><a href="parent_1.html">Parent 1</a></li>
|
||||||
|
<li><a href="parent_2.html">Parent 2</a></li>
|
||||||
|
<li><a href="root_1.html">Root 1</a></li>
|
||||||
|
<li><a href="root_2.html">Root 2</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<a href="root_1.html" class="breadcrumb-item">Root 1</a> >
|
||||||
|
|
||||||
|
<span class="breadcrumb-item active">Parent 2</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
17
plugins/breadcrumbs/testdata/reference/root_1.html
vendored
Normal file
17
plugins/breadcrumbs/testdata/reference/root_1.html
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Root 1</h1>
|
||||||
|
<ul>
|
||||||
|
<li><a href="child_1.html">Child 1</a></li>
|
||||||
|
<li><a href="child_2.html">Child 2</a></li>
|
||||||
|
<li><a href="child_3.html">Child 3</a></li>
|
||||||
|
<li><a href="child_4.html">Child 4</a></li>
|
||||||
|
<li><a href="parent_1.html">Parent 1</a></li>
|
||||||
|
<li><a href="parent_2.html">Parent 2</a></li>
|
||||||
|
<li><a href="root_1.html">Root 1</a></li>
|
||||||
|
<li><a href="root_2.html">Root 2</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
17
plugins/breadcrumbs/testdata/reference/root_2.html
vendored
Normal file
17
plugins/breadcrumbs/testdata/reference/root_2.html
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Root 2</h1>
|
||||||
|
<ul>
|
||||||
|
<li><a href="child_1.html">Child 1</a></li>
|
||||||
|
<li><a href="child_2.html">Child 2</a></li>
|
||||||
|
<li><a href="child_3.html">Child 3</a></li>
|
||||||
|
<li><a href="child_4.html">Child 4</a></li>
|
||||||
|
<li><a href="parent_1.html">Parent 1</a></li>
|
||||||
|
<li><a href="parent_2.html">Parent 2</a></li>
|
||||||
|
<li><a href="root_1.html">Root 1</a></li>
|
||||||
|
<li><a href="root_2.html">Root 2</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
5
plugins/breadcrumbs/testdata/source/child_1.html
vendored
Normal file
5
plugins/breadcrumbs/testdata/source/child_1.html
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
+++
|
||||||
|
Layout = "page"
|
||||||
|
CrumbName = "Child 1"
|
||||||
|
CrumbParent = "Parent 1"
|
||||||
|
+++
|
5
plugins/breadcrumbs/testdata/source/child_2.html
vendored
Normal file
5
plugins/breadcrumbs/testdata/source/child_2.html
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
+++
|
||||||
|
Layout = "page"
|
||||||
|
CrumbName = "Child 2"
|
||||||
|
CrumbParent = "Parent 1"
|
||||||
|
+++
|
5
plugins/breadcrumbs/testdata/source/child_3.html
vendored
Normal file
5
plugins/breadcrumbs/testdata/source/child_3.html
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
+++
|
||||||
|
Layout = "page"
|
||||||
|
CrumbName = "Child 3"
|
||||||
|
CrumbParent = "Parent 2"
|
||||||
|
+++
|
5
plugins/breadcrumbs/testdata/source/child_4.html
vendored
Normal file
5
plugins/breadcrumbs/testdata/source/child_4.html
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
+++
|
||||||
|
Layout = "page"
|
||||||
|
CrumbName = "Child 4"
|
||||||
|
CrumbParent = "Parent 2"
|
||||||
|
+++
|
5
plugins/breadcrumbs/testdata/source/parent_1.html
vendored
Normal file
5
plugins/breadcrumbs/testdata/source/parent_1.html
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
+++
|
||||||
|
Layout = "page"
|
||||||
|
CrumbName = "Parent 1"
|
||||||
|
CrumbParent = "Root 1"
|
||||||
|
+++
|
5
plugins/breadcrumbs/testdata/source/parent_2.html
vendored
Normal file
5
plugins/breadcrumbs/testdata/source/parent_2.html
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
+++
|
||||||
|
Layout = "page"
|
||||||
|
CrumbName = "Parent 2"
|
||||||
|
CrumbParent = "Root 1"
|
||||||
|
+++
|
4
plugins/breadcrumbs/testdata/source/root_1.html
vendored
Normal file
4
plugins/breadcrumbs/testdata/source/root_1.html
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
+++
|
||||||
|
Layout = "page"
|
||||||
|
CrumbName = "Root 1"
|
||||||
|
+++
|
4
plugins/breadcrumbs/testdata/source/root_2.html
vendored
Normal file
4
plugins/breadcrumbs/testdata/source/root_2.html
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
+++
|
||||||
|
Layout = "page"
|
||||||
|
CrumbName = "Root 2"
|
||||||
|
+++
|
26
plugins/breadcrumbs/testdata/source/template.gohtml
vendored
Normal file
26
plugins/breadcrumbs/testdata/source/template.gohtml
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{{define "page"}}
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>{{.Props.CrumbName}}</h1>
|
||||||
|
<ul>
|
||||||
|
<li><a href="child_1.html">Child 1</a></li>
|
||||||
|
<li><a href="child_2.html">Child 2</a></li>
|
||||||
|
<li><a href="child_3.html">Child 3</a></li>
|
||||||
|
<li><a href="child_4.html">Child 4</a></li>
|
||||||
|
<li><a href="parent_1.html">Parent 1</a></li>
|
||||||
|
<li><a href="parent_2.html">Parent 2</a></li>
|
||||||
|
<li><a href="root_1.html">Root 1</a></li>
|
||||||
|
<li><a href="root_2.html">Root 2</a></li>
|
||||||
|
</ul>
|
||||||
|
{{if .Props.CrumbParent}}
|
||||||
|
<div>
|
||||||
|
{{range .Props.Crumbs.Ancestors}}
|
||||||
|
<a href="{{.File.Path}}" class="breadcrumb-item">{{.File.Props.CrumbName}}</a> >
|
||||||
|
{{end}}
|
||||||
|
<span class="breadcrumb-item active">{{.Props.CrumbName}}</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
||||||
|
|
131
plugins/collection/collection.go
Normal file
131
plugins/collection/collection.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// Package collection groups related pages into named collections. This can be
|
||||||
|
// useful for presenting blog posts on your front page, and displaying summary
|
||||||
|
// information about other types of content on your website. It can be used in
|
||||||
|
// conjunction with the "pager" plugin to create large collections which are
|
||||||
|
// split across several pages.
|
||||||
|
package collection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.foosoft.net/alex/goldsmith"
|
||||||
|
"git.foosoft.net/alex/goldsmith/filters/wildcard"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Comparer callback function is used to sort files within a collection group.
|
||||||
|
type Comparer func(i, j *goldsmith.File) (less bool)
|
||||||
|
|
||||||
|
// Collection chainable plugin context.
|
||||||
|
type Collection struct {
|
||||||
|
collectionKey string
|
||||||
|
groupsKey string
|
||||||
|
comparer Comparer
|
||||||
|
|
||||||
|
groups map[string][]*goldsmith.File
|
||||||
|
files []*goldsmith.File
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new instance of the Collection plugin.
|
||||||
|
func New() *Collection {
|
||||||
|
return &Collection{
|
||||||
|
collectionKey: "Collection",
|
||||||
|
groupsKey: "Groups",
|
||||||
|
groups: make(map[string][]*goldsmith.File),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectionKey sets the metadata key used to access the collection name (default: "Collection").
|
||||||
|
// The metadata associated with this key can be either a single string or an array of strings.
|
||||||
|
func (self *Collection) CollectionKey(collectionKey string) *Collection {
|
||||||
|
self.collectionKey = collectionKey
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupsKey sets the metadata key used to store information about collection groups (default: "Groups").
|
||||||
|
// This information is stored as a mapping of group names to contained files.
|
||||||
|
func (self *Collection) GroupsKey(groupsKey string) *Collection {
|
||||||
|
self.groupsKey = groupsKey
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comparer sets the function used to sort files in collection groups (default: sort by filenames).
|
||||||
|
func (plugin *Collection) Comparer(comparer Comparer) *Collection {
|
||||||
|
plugin.comparer = comparer
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Collection) Name() string {
|
||||||
|
return "collection"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Collection) Initialize(context *goldsmith.Context) error {
|
||||||
|
context.Filter(wildcard.New("**/*.html", "**/*.htm"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Collection) Process(context *goldsmith.Context, inputFile *goldsmith.File) error {
|
||||||
|
self.mutex.Lock()
|
||||||
|
defer func() {
|
||||||
|
inputFile.SetProp(self.groupsKey, self.groups)
|
||||||
|
self.files = append(self.files, inputFile)
|
||||||
|
self.mutex.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
collectionRaw, ok := inputFile.Prop(self.collectionKey)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var collectionNames []string
|
||||||
|
switch t := collectionRaw.(type) {
|
||||||
|
case string:
|
||||||
|
collectionNames = append(collectionNames, t)
|
||||||
|
case []string:
|
||||||
|
collectionNames = append(collectionNames, t...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, collectionName := range collectionNames {
|
||||||
|
files, _ := self.groups[collectionName]
|
||||||
|
files = append(files, inputFile)
|
||||||
|
self.groups[collectionName] = files
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Collection) Finalize(context *goldsmith.Context) error {
|
||||||
|
for _, files := range self.groups {
|
||||||
|
fg := &fileSorter{files, self.comparer}
|
||||||
|
sort.Sort(fg)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range self.files {
|
||||||
|
context.DispatchFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileSorter struct {
|
||||||
|
files []*goldsmith.File
|
||||||
|
comparer Comparer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self fileSorter) Len() int {
|
||||||
|
return len(self.files)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self fileSorter) Swap(i, j int) {
|
||||||
|
self.files[i], self.files[j] = self.files[j], self.files[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self fileSorter) Less(i, j int) bool {
|
||||||
|
if self.comparer == nil {
|
||||||
|
return strings.Compare(self.files[i].Path(), self.files[j].Path()) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.comparer(self.files[i], self.files[j])
|
||||||
|
}
|
22
plugins/collection/collection_test.go
Normal file
22
plugins/collection/collection_test.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package collection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.foosoft.net/alex/goldsmith"
|
||||||
|
"git.foosoft.net/alex/goldsmith/harness"
|
||||||
|
"git.foosoft.net/alex/goldsmith/plugins/frontmatter"
|
||||||
|
"git.foosoft.net/alex/goldsmith/plugins/layout"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test(self *testing.T) {
|
||||||
|
harness.Validate(
|
||||||
|
self,
|
||||||
|
func(gs *goldsmith.Goldsmith) {
|
||||||
|
gs.
|
||||||
|
Chain(frontmatter.New()).
|
||||||
|
Chain(New()).
|
||||||
|
Chain(layout.New())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
46
plugins/collection/testdata/reference/index.html
vendored
Normal file
46
plugins/collection/testdata/reference/index.html
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<h1>Group 1</h1>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="page_1.html">Page 1</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="page_2.html">Page 2</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="page_3.html">Page 3</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1>Group 2</h1>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="page_4.html">Page 4</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="page_5.html">Page 5</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="page_6.html">Page 6</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
6
plugins/collection/testdata/reference/page_1.html
vendored
Normal file
6
plugins/collection/testdata/reference/page_1.html
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Page 1</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user