Compare commits

..

23 Commits

Author SHA1 Message Date
f5cee22324 Update README 2024-03-09 21:51:45 -05:00
8ee5f529d8 Update README 2024-03-04 20:22:21 -08:00
5c547dbf68 Cleanup 2024-03-04 20:17:25 -08:00
dcd19b19a6 Fix github links 2024-03-04 19:08:10 -08:00
7acc65170f Cleanup 2024-03-03 21:42:41 -08:00
cd2e515141 Cleanup 2024-03-03 21:39:08 -08:00
889397b9c5 Cleanup 2024-03-03 21:30:32 -08:00
22ffaae954 Cleanup 2024-03-03 18:13:42 -08:00
e24e892773 Revert "Change File to interface"
This reverts commit 2f95fdba2d.
2024-03-03 18:09:21 -08:00
9ba1c7cfeb Revert "Make Context be an interface"
This reverts commit ab0c1c53d2.
2024-03-03 17:46:38 -08:00
05f7ee5280 Cleanup 2024-03-03 16:50:03 -08:00
ab0c1c53d2 Make Context be an interface 2024-03-03 11:14:50 -08:00
01fac8cb96 Cleanup 2024-02-21 18:59:53 -08:00
8aee72f2c8 cleanup 2024-02-21 18:53:17 -08:00
771b40e82e Cleanup 2024-02-19 22:23:21 -08:00
c0c940156f Add contextFile 2024-02-19 22:20:10 -08:00
2f95fdba2d Change File to interface 2024-02-19 16:26:41 -08:00
55ed95ddbb udpate plugins 2024-02-19 11:27:23 -08:00
25d629af10 Cleanup 2024-02-19 10:39:05 -08:00
ea28b5c8cf Add test script 2024-02-19 10:06:00 -08:00
648ab12c5b Add components 2024-02-16 22:35:49 -08:00
972c4d81f5 Cleanup 2024-02-16 22:27:10 -08:00
f0259bcbe9 Cleanup 2024-02-16 22:27:10 -08:00
273 changed files with 6660 additions and 411 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target

144
README.md
View File

@ -14,42 +14,42 @@ to understand, it is often best to learn by example:
1. Start by copying files from a source directory to a destination directory (the simplest possible use case):
```go
goldsmith.
Begin(srcDir). // read files from srcDir
End(dstDir) // write files to dstDir
var gs goldsmith.Goldsmith
gs.Begin(srcDir). // read files from srcDir
End(dstDir) // write files to dstDir
```
2. Now let's convert any Markdown files to HTML fragments (while still copying the rest), using the
[Markdown](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/markdown) plugin:
[Markdown](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/markdown) plugin:
```go
goldsmith.
Begin(srcDir). // read files from srcDir
var gs goldsmith.Goldsmith
gs.Begin(srcDir). // read files from srcDir
Chain(markdown.New()). // convert *.md files to *.html files
End(dstDir) // write files to dstDir
```
3. If we have any
[front matter](https://raw.githubusercontent.com/FooSoft/goldsmith-samples/master/basic/content/index.md) in our
Markdown files, we need to extract it using the,
[FrontMatter](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/frontmatter) plugin:
3. If we have any [front
matter](https://git.foosoft.net/alex/goldsmith-samples/raw/branch/master/basic/content/index.md) in our Markdown
files, we need to extract it using the,
[FrontMatter](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/frontmatter) plugin:
```go
goldsmith.
Begin(srcDir). // read files from srcDir
var gs goldsmith.Goldsmith
gs.Begin(srcDir). // read files from srcDir
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
Chain(markdown.New()). // convert *.md files to *.html files
End(dstDir) // write files to dstDir
```
4. Next, we should run our barebones HTML through a
[template](https://raw.githubusercontent.com/FooSoft/goldsmith-samples/master/basic/content/layouts/basic.gohtml) to
[template](https://git.foosoft.net/alex/goldsmith-samples/raw/branch/master/basic/content/layouts/basic.gohtml) to
add elements like a header, footer, or a menu; for this we can use the
[Layout](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/frontmatter) plugin:
[Layout](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/frontmatter) plugin:
```go
goldsmith.
Begin(srcDir). // read files from srcDir
var gs goldsmith.Goldsmith
gs.Begin(srcDir). // read files from srcDir
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
Chain(markdown.New()). // convert *.md files to *.html files
Chain(layout.New()). // apply *.gohtml templates to *.html files
@ -58,11 +58,11 @@ to understand, it is often best to learn by example:
5. Now, let's [minify](https://en.wikipedia.org/wiki/Minification_(programming)) our files to reduce data transfer and
load times for our site's visitors using the
[Minify](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/minify) plugin:
[Minify](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/minify) plugin:
```go
goldsmith.
Begin(srcDir). // read files from srcDir
var gs goldsmith.Goldsmith
gs.Begin(srcDir). // read files from srcDir
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
Chain(markdown.New()). // convert *.md files to *.html files
Chain(layout.New()). // apply *.gohtml templates to *.html files
@ -71,12 +71,12 @@ to understand, it is often best to learn by example:
```
6. Debugging problems in minified code can be tricky, so let's use the
[Condition](https://godoc.org/git.foosoft.net/alex/goldsmith-components/filters/condition) filter to make
minification occur only when we are ready for distribution.
[Condition](https://godoc.org/git.foosoft.net/alex/goldsmith/filters/condition) filter to make minification occur
only when we are ready for distribution.
```go
goldsmith.
Begin(srcDir). // read files from srcDir
var gs goldsmith.Goldsmith
gs.Begin(srcDir). // read files from srcDir
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
Chain(markdown.New()). // convert *.md files to *.html files
Chain(layout.New()). // apply *.gohtml templates to *.html files
@ -87,8 +87,8 @@ to understand, it is often best to learn by example:
```
7. Now that we have all of our plugins chained up, let's look at a complete example which uses
[DevServer](https://godoc.org/git.foosoft.net/alex/goldsmith-components/devserver) to bootstrap a complete
development sever which automatically rebuilds the site whenever source files are updated.
[DevServer](https://godoc.org/git.foosoft.net/alex/goldsmith/devserver) to bootstrap a complete development sever
which automatically rebuilds the site whenever source files are updated.
```go
package main
@ -98,12 +98,12 @@ to understand, it is often best to learn by example:
"log"
"git.foosoft.net/alex/goldsmith"
"git.foosoft.net/alex/goldsmith-components/devserver"
"git.foosoft.net/alex/goldsmith-components/filters/condition"
"git.foosoft.net/alex/goldsmith-components/plugins/frontmatter"
"git.foosoft.net/alex/goldsmith-components/plugins/layout"
"git.foosoft.net/alex/goldsmith-components/plugins/markdown"
"git.foosoft.net/alex/goldsmith-components/plugins/minify"
"git.foosoft.net/alex/goldsmith/devserver"
"git.foosoft.net/alex/goldsmith/filters/condition"
"git.foosoft.net/alex/goldsmith/plugins/frontmatter"
"git.foosoft.net/alex/goldsmith/plugins/layout"
"git.foosoft.net/alex/goldsmith/plugins/markdown"
"git.foosoft.net/alex/goldsmith/plugins/minify"
)
type builder struct {
@ -111,8 +111,8 @@ to understand, it is often best to learn by example:
}
func (b *builder) Build(srcDir, dstDir, cacheDir string) {
errs := goldsmith.
Begin(srcDir). // read files from srcDir
var gs goldsmith.Goldsmith
errs := gs.Begin(srcDir). // read files from srcDir
Chain(frontmatter.New()). // extract frontmatter and store it as metadata
Chain(markdown.New()). // convert *.md files to *.html files
Chain(layout.New()). // apply *.gohtml templates to *.html files
@ -139,12 +139,9 @@ to understand, it is often best to learn by example:
Below are some examples of Goldsmith usage which can used to base your site on:
* [Basic Sample](https://git.foosoft.net/alex/goldsmith-samples/src/branch/master/basic): a great starting point, this
is the sample site from the tutorial.
* [Bootstrap Sample](https://git.foosoft.net/alex/goldsmith-samples/src/branch/master/bootstrap): a slightly more
advanced sample using [Bootstrap](https://getbootstrap.com/).
* [FooSoft.net](https://git.foosoft.net/alex/goldsmith): I've been "dogfooding" Goldsmith by using it to [generate my
homepage](/posts/generating-the-foosoft.net-homepage) for nearly a decade.
* [Basic Sample](https://git.foosoft.net/alex/goldsmith-samples/src/branch/master/basic): a great starting point, this is the sample site from the tutorial.
* [Bootstrap Sample](https://git.foosoft.net/alex/goldsmith-samples/src/branch/master/bootstrap): a slightly more advanced sample using [Bootstrap](https://getbootstrap.com/).
* [FooSoft.net](https://git.foosoft.net/alex/goldsmith): I've been "dogfooding" Goldsmith by using it to [generate my homepage](/posts/generating-the-foosoft.net-homepage) for nearly a decade.
## Components
@ -152,55 +149,32 @@ A growing set of plugins, filters, and other tools are provided to make it easie
### Plugins
* [Absolute](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/absolute): Convert relative HTML file
references to absolute paths.
* [Breadcrumbs](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/breadcrumbs): Generate metadata
required to build breadcrumb navigation.
* [Collection](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/collection): Group related pages
into named collections.
* [Document](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/document): Enable simple DOM
modification via an API similar to jQuery.
* [Forward](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/forward): Create simple redirections
for pages that have moved to a new URL.
* [FrontMatter](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/frontmatter): Extract the
JSON, YAML, or TOML metadata stored in your files.
* [Index](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/index): Create metadata for directory
file listings and generate directory index pages.
* [Layout](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/layout): Transform your HTML files with
Go templates.
* [LiveJs](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/livejs): Inject JavaScript code to
automatically reload pages when modified.
* [Markdown](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/markdown): Render Markdown documents
as HTML fragments.
* [Minify](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/minify): Remove superfluous data from a
variety of web formats.
* [Pager](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/pager): Split arrays of metadata into
standalone pages.
* [Rule](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/rule): Update metadata and filter files
based on paths.
* [Summary](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/summary): Extract summary and title
metadata from HTML files.
* [Syndicate](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/syndicate): Generate RSS, Atom, and
JSON feeds from existing metadata.
* [Syntax](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/syntax): Enable syntax highlighting for
pre-formatted code blocks.
* [Tags](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/tags): Generate tag clouds and indices
from file metadata.
* [Thumbnail](https://godoc.org/git.foosoft.net/alex/goldsmith-components/plugins/thumbnail): Build thumbnails for a
variety of common image formats.
* [Absolute](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/absolute): Convert relative HTML file references to absolute paths.
* [Breadcrumbs](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/breadcrumbs): Generate metadata required to build breadcrumb navigation.
* [Collection](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/collection): Group related pages into named collections.
* [Document](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/document): Enable simple DOM modification via an API similar to jQuery.
* [Forward](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/forward): Create simple redirections for pages that have moved to a new URL.
* [FrontMatter](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/frontmatter): Extract the JSON, YAML, or TOML metadata stored in your files.
* [Index](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/index): Create metadata for directory file listings and generate directory index pages.
* [Layout](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/layout): Transform your HTML files with Go templates.
* [LiveJs](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/livejs): Inject JavaScript code to automatically reload pages when modified.
* [Markdown](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/markdown): Render Markdown documents as HTML fragments.
* [Minify](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/minify): Remove superfluous data from a variety of web formats.
* [Pager](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/pager): Split arrays of metadata into standalone pages.
* [Rule](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/rule): Update metadata and filter files based on paths.
* [Summary](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/summary): Extract summary and title metadata from HTML files.
* [Syndicate](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/syndicate): Generate RSS, Atom, and JSON feeds from existing metadata.
* [Syntax](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/syntax): Enable syntax highlighting for pre-formatted code blocks.
* [Tags](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/tags): Generate tag clouds and indices from file metadata.
* [Thumbnail](https://godoc.org/git.foosoft.net/alex/goldsmith/plugins/thumbnail): Build thumbnails for a variety of common image formats.
### Filters
* [Condition](https://godoc.org/git.foosoft.net/alex/goldsmith-components/filters/condition): Filter files based on a
single condition.
* [Operator](https://godoc.org/git.foosoft.net/alex/goldsmith-components/filters/operator): Join filters using
logical `AND`, `OR`, and `NOT` operators.
* [Wildcard](https://godoc.org/git.foosoft.net/alex/goldsmith-components/filters/wildcard): Filter files using path
wildcards (`*`, `?`, etc.)
* [Condition](https://godoc.org/git.foosoft.net/alex/goldsmith/filters/condition): Filter files based on a single condition.
* [Operator](https://godoc.org/git.foosoft.net/alex/goldsmith/filters/operator): Join filters using logical `AND`, `OR`, and `NOT` operators.
* [Wildcard](https://godoc.org/git.foosoft.net/alex/goldsmith/filters/wildcard): Filter files using path wildcards (`*`, `?`, etc.)
### Other
* [DevServer](https://godoc.org/git.foosoft.net/alex/goldsmith-components/devserver): Simple framework for building,
updating, and viewing your site.
* [Harness](https://godoc.org/git.foosoft.net/alex/goldsmith-components/harness): Unit test harness for verifying
Goldsmith plugins and filters.
* [DevServer](https://godoc.org/git.foosoft.net/alex/goldsmith/devserver): Simple framework for building, updating, and viewing your site.
* [Harness](https://godoc.org/git.foosoft.net/alex/goldsmith/harness): Unit test harness for verifying Goldsmith plugins and filters.

View File

@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"sort"
"strings"
)
type cache struct {
@ -15,7 +16,7 @@ type cache struct {
}
func (self *cache) retrieveFile(context *Context, outputPath string, inputFiles []*File) (*File, error) {
cachePath, err := self.buildCachePath(context, outputPath, inputFiles)
cachePath, err := self.buildCachePath(outputPath, inputFiles)
if err != nil {
return nil, err
}
@ -32,8 +33,8 @@ func (self *cache) retrieveFile(context *Context, outputPath string, inputFiles
return outputFile, nil
}
func (self *cache) storeFile(context *Context, outputFile *File, inputFiles []*File) error {
cachePath, err := self.buildCachePath(context, outputFile.Path(), inputFiles)
func (self *cache) storeFile(outputFile *File, inputFiles []*File) error {
cachePath, err := self.buildCachePath(outputFile.Path(), inputFiles)
if err != nil {
return err
}
@ -68,11 +69,13 @@ func (self *cache) storeFile(context *Context, outputFile *File, inputFiles []*F
return nil
}
func (self *cache) buildCachePath(context *Context, outputPath string, inputFiles []*File) (string, error) {
func (self *cache) buildCachePath(outputPath string, inputFiles []*File) (string, error) {
hasher := crc32.NewIEEE()
hasher.Write([]byte(outputPath))
sort.Sort(filesByPath(inputFiles))
sort.Slice(inputFiles, func(i, j int) bool {
return strings.Compare(inputFiles[i].Path(), inputFiles[j].Path()) < 0
})
for _, inputFile := range inputFiles {
modTimeBuff := make([]byte, 8)

32
chain_state.go Normal file
View 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)
}

View File

@ -14,8 +14,7 @@ import (
// Context corresponds to the current link in the chain and provides methods
// that enable plugins to inject new files into the chain.
type Context struct {
goldsmith *Goldsmith
chain *chainState
plugin Plugin
filtersExt filterStack
@ -29,15 +28,19 @@ type Context struct {
}
// CreateFileFrom data creates a new file instance from the provided data buffer.
func (self *Context) CreateFileFromReader(sourcePath string, reader io.Reader) (*File, error) {
func (self *Context) CreateFileFromReader(relPath string, reader io.Reader) (*File, error) {
if filepath.IsAbs(relPath) {
return nil, errors.New("file paths must be relative")
}
data, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
file := &File{
relPath: sourcePath,
props: make(map[string]Prop),
relPath: relPath,
props: make(FileProps),
modTime: time.Now(),
size: int64(len(data)),
reader: bytes.NewReader(data),
@ -48,13 +51,9 @@ func (self *Context) CreateFileFromReader(sourcePath string, reader io.Reader) (
}
// CreateFileFromAsset creates a new file instance from the provided file path.
func (self *Context) CreateFileFromAsset(sourcePath, dataPath string) (*File, error) {
if filepath.IsAbs(sourcePath) {
return nil, errors.New("source paths must be relative")
}
if filepath.IsAbs(dataPath) {
return nil, errors.New("data paths must be relative")
func (self *Context) CreateFileFromAsset(relPath, dataPath string) (*File, error) {
if filepath.IsAbs(relPath) {
return nil, errors.New("file paths must be relative")
}
info, err := os.Stat(dataPath)
@ -62,12 +61,12 @@ func (self *Context) CreateFileFromAsset(sourcePath, dataPath string) (*File, er
return nil, err
}
if info.IsDir() {
return nil, errors.New("assets must be files")
return nil, errors.New("file paths cannot be directories")
}
file := &File{
relPath: sourcePath,
props: make(map[string]Prop),
relPath: relPath,
props: make(FileProps),
modTime: info.ModTime(),
size: info.Size(),
dataPath: dataPath,
@ -86,11 +85,11 @@ func (self *Context) DispatchFile(file *File) {
// dependencies on any input files that are needed to generate it, and then
// passes it to the next link in the chain.
func (self *Context) DispatchAndCacheFile(outputFile *File, inputFiles ...*File) {
if self.goldsmith.cache != nil {
self.goldsmith.cache.storeFile(self, outputFile, inputFiles)
if self.chain.cache != nil {
self.chain.cache.storeFile(outputFile, inputFiles)
}
self.filesOut <- outputFile
self.DispatchFile(outputFile)
}
// RetrieveCachedFile looks up file data (excluding the metadata), given an
@ -98,8 +97,8 @@ func (self *Context) DispatchAndCacheFile(outputFile *File, inputFiles ...*File)
// will return nil if the desired file is not found in the cache.
func (self *Context) RetrieveCachedFile(outputPath string, inputFiles ...*File) *File {
var outputFile *File
if self.goldsmith.cache != nil {
outputFile, _ = self.goldsmith.cache.retrieveFile(self, outputPath, inputFiles)
if self.chain.cache != nil {
outputFile, _ = self.chain.cache.retrieveFile(self, outputPath, inputFiles)
}
return outputFile
@ -125,7 +124,7 @@ func (self *Context) step() {
if initializer, ok := self.plugin.(Initializer); ok {
if err := initializer.Initialize(self); err != nil {
self.goldsmith.fault(self.plugin.Name(), nil, err)
self.chain.fault(self.plugin.Name(), nil, err)
return
}
}
@ -146,13 +145,13 @@ func (self *Context) step() {
for inputFile := range self.filesIn {
if processor != nil && self.filtersInt.accept(inputFile) && self.filtersExt.accept(inputFile) {
if _, err := inputFile.Seek(0, io.SeekStart); err != nil {
self.goldsmith.fault("core", inputFile, err)
self.chain.fault("core", inputFile, err)
}
if err := processor.Process(self, inputFile); err != nil {
self.goldsmith.fault(self.plugin.Name(), inputFile, err)
self.chain.fault(self.plugin.Name(), inputFile, err)
}
} else {
self.filesOut <- inputFile
self.DispatchFile(inputFile)
}
}
}()
@ -163,7 +162,7 @@ func (self *Context) step() {
if finalizer, ok := self.plugin.(Finalizer); ok {
if err := finalizer.Finalize(self); err != nil {
self.goldsmith.fault(self.plugin.Name(), nil, err)
self.chain.fault(self.plugin.Name(), nil, err)
}
}
}

115
devserver/devserver.go Normal file
View 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
View 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
View File

@ -2,42 +2,38 @@ package goldsmith
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"time"
)
type Prop interface{}
type PropMap map[string]Prop
type (
FileProp any
FileProps map[string]FileProp
// File represents in-memory or on-disk files in a chain.
type File struct {
relPath string
props map[string]Prop
modTime time.Time
size int64
// File represents in-memory or on-disk files in a chain.
File struct {
relPath string
props FileProps
modTime time.Time
size int64
dataPath string
reader *bytes.Reader
dataPath string
reader *bytes.Reader
index int
}
index int
}
)
// Rename modifies the file path relative to the source directory.
func (self *File) Rename(path string) {
self.relPath = path
}
func (self *File) Rewrite(reader io.Reader) error {
data, err := io.ReadAll(reader)
if err != nil {
return err
func (self *File) Rename(path string) error {
if filepath.IsAbs(path) {
return fmt.Errorf("unexpected absolute path: %s", path)
}
self.reader = bytes.NewReader(data)
self.modTime = time.Now()
self.size = int64(len(data))
self.relPath = path
return nil
}
@ -46,16 +42,16 @@ func (self *File) Path() string {
return filepath.ToSlash(self.relPath)
}
// Name returns the base name of the file.
func (self *File) Name() string {
return filepath.Base(self.relPath)
}
// Dir returns the containing directory of the file.
func (self *File) Dir() string {
return filepath.ToSlash(filepath.Dir(self.relPath))
}
// Name returns the base name of the file.
func (self *File) Name() string {
return filepath.Base(self.relPath)
}
// Ext returns the extension of the file.
func (self *File) Ext() string {
return filepath.Ext(self.relPath)
@ -102,31 +98,29 @@ func (self *File) Seek(offset int64, whence int) (int64, error) {
return self.reader.Seek(offset, whence)
}
// Returns value for string formatting.
// GoString returns value for string formatting.
func (self *File) GoString() string {
return self.relPath
}
func (self *File) SetProp(name string, value Prop) {
// RemoveProp deletes the metadata property for the provided name.
func (self *File) RemoveProp(name string) {
delete(self.props, name)
}
// SetProp updates the metadata property for the provided name.
func (self *File) SetProp(name string, value FileProp) {
self.props[name] = value
}
func (self *File) CopyProps(file *File) {
for key, value := range file.props {
self.props[key] = value
}
}
func (self *File) Prop(name string) (Prop, bool) {
// Prop returns the metadata property for the provided name.
func (self *File) Prop(name string) (FileProp, bool) {
value, ok := self.props[name]
return value, ok
}
func (self *File) Props() PropMap {
return self.props
}
func (self *File) PropOrDefault(name string, valueDef Prop) Prop {
// PropOrDef returns the metadata property for the provided name or the default.
func (self *File) PropOrDef(name string, valueDef FileProp) FileProp {
if value, ok := self.Prop(name); ok {
return value
}
@ -134,44 +128,16 @@ func (self *File) PropOrDefault(name string, valueDef Prop) Prop {
return valueDef
}
func (self *File) export(targetDir string) error {
targetPath := filepath.Join(targetDir, self.relPath)
// Props returns all of the metadata properties.
func (self *File) Props() FileProps {
return self.props
}
if targetInfo, err := os.Stat(targetPath); err == nil && !targetInfo.ModTime().Before(self.ModTime()) {
return nil
// CopyProps copies all metadata properties from the provided file.
func (self *File) CopyProps(file *File) {
for key, value := range file.props {
self.props[key] = value
}
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return err
}
fw, err := os.Create(targetPath)
if err != nil {
return err
}
defer fw.Close()
if self.reader == nil {
fr, err := os.Open(self.dataPath)
if err != nil {
return err
}
defer fr.Close()
if _, err := io.Copy(fw, fr); err != nil {
return err
}
} else {
if _, err := self.Seek(0, io.SeekStart); err != nil {
return err
}
if _, err := self.WriteTo(fw); err != nil {
return err
}
}
return nil
}
func (self *File) load() error {

90
file_exporter.go Normal file
View 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
View 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
})
}

View File

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

View File

@ -1,11 +1,13 @@
package goldsmith
type filterEntry struct {
filter Filter
index int
}
type (
filterEntry struct {
filter Filter
index int
}
type filterStack []filterEntry
filterStack []filterEntry
)
func (self *filterStack) accept(file *File) bool {
for _, entry := range *self {

View File

@ -0,0 +1,21 @@
package condition
import (
"git.foosoft.net/alex/goldsmith"
)
type Condition struct {
accept bool
}
func New(accept bool) *Condition {
return &Condition{accept: accept}
}
func (*Condition) Name() string {
return "condition"
}
func (self *Condition) Accept(file *goldsmith.File) bool {
return self.accept
}

View File

@ -0,0 +1,28 @@
package condition
import (
"testing"
"git.foosoft.net/alex/goldsmith"
"git.foosoft.net/alex/goldsmith/harness"
)
func TestEnabled(self *testing.T) {
harness.ValidateCase(
self,
"true",
func(gs *goldsmith.Goldsmith) {
gs.FilterPush(New(true))
},
)
}
func TestDisabled(self *testing.T) {
harness.ValidateCase(
self,
"false",
func(gs *goldsmith.Goldsmith) {
gs.FilterPush(New(false))
},
)
}

View File

View File

View File

View File

View File

View File

View File

@ -0,0 +1,69 @@
package operator
import (
"git.foosoft.net/alex/goldsmith"
)
type Operator interface {
goldsmith.Filter
}
func And(filters ...goldsmith.Filter) Operator {
return &operatorAnd{filters}
}
type operatorAnd struct {
filters []goldsmith.Filter
}
func (*operatorAnd) Name() string {
return "operator"
}
func (self *operatorAnd) Accept(file *goldsmith.File) bool {
for _, filter := range self.filters {
if !filter.Accept(file) {
return false
}
}
return true
}
func Not(self goldsmith.Filter) Operator {
return &operatorNot{self}
}
type operatorNot struct {
filter goldsmith.Filter
}
func (*operatorNot) Name() string {
return "operator"
}
func (self *operatorNot) Accept(file *goldsmith.File) bool {
return !self.filter.Accept(file)
}
func Or(self ...goldsmith.Filter) Operator {
return &operatorOr{self}
}
type operatorOr struct {
filters []goldsmith.Filter
}
func (*operatorOr) Name() string {
return "operator"
}
func (self *operatorOr) Accept(file *goldsmith.File) bool {
for _, filter := range self.filters {
if filter.Accept(file) {
return true
}
}
return false
}

View File

@ -0,0 +1,109 @@
package operator
import (
"testing"
"git.foosoft.net/alex/goldsmith"
"git.foosoft.net/alex/goldsmith/filters/condition"
"git.foosoft.net/alex/goldsmith/harness"
)
func TestAndFalse(t *testing.T) {
harness.ValidateCase(
t,
"and_false",
func(gs *goldsmith.Goldsmith) {
gs.FilterPush(And(condition.New(false)))
},
)
}
func TestAndFalseTrue(t *testing.T) {
harness.ValidateCase(
t,
"and_false_true",
func(gs *goldsmith.Goldsmith) {
gs.FilterPush(And(condition.New(false), condition.New(true)))
},
)
}
func TestAndTrueFalse(t *testing.T) {
harness.ValidateCase(
t,
"and_true_false",
func(gs *goldsmith.Goldsmith) {
gs.FilterPush(And(condition.New(true), condition.New(false)))
},
)
}
func TestAndTrue(t *testing.T) {
harness.ValidateCase(
t,
"and_true",
func(gs *goldsmith.Goldsmith) {
gs.FilterPush(And(condition.New(true)))
},
)
}
func TestOrFalse(t *testing.T) {
harness.ValidateCase(
t,
"or_false",
func(gs *goldsmith.Goldsmith) {
gs.FilterPush(Or(condition.New(false)))
},
)
}
func TestOrFalseTrue(t *testing.T) {
harness.ValidateCase(
t,
"or_false_true",
func(gs *goldsmith.Goldsmith) {
gs.FilterPush(Or(condition.New(false), condition.New(true)))
},
)
}
func TestOrTrueFalse(t *testing.T) {
harness.ValidateCase(
t,
"or_true_false",
func(gs *goldsmith.Goldsmith) {
gs.FilterPush(Or(condition.New(true), condition.New(false)))
},
)
}
func TestOrTrue(t *testing.T) {
harness.ValidateCase(
t,
"or_true",
func(gs *goldsmith.Goldsmith) {
gs.FilterPush(Or(condition.New(true)))
},
)
}
func TestNotFalse(t *testing.T) {
harness.ValidateCase(
t,
"not_false",
func(gs *goldsmith.Goldsmith) {
gs.FilterPush(Not(condition.New(false)))
},
)
}
func TestNotTrue(t *testing.T) {
harness.ValidateCase(
t,
"not_true",
func(gs *goldsmith.Goldsmith) {
gs.FilterPush(Not(condition.New(true)))
},
)
}

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

@ -0,0 +1,47 @@
package wildcard
import (
"strings"
"git.foosoft.net/alex/goldsmith"
"github.com/bmatcuk/doublestar/v4"
)
type Wildcard struct {
wildcards []string
caseSensitive bool
}
func New(wildcards ...string) *Wildcard {
return &Wildcard{wildcards: wildcards}
}
func (self *Wildcard) CaseSensitive(caseSensitive bool) *Wildcard {
self.caseSensitive = caseSensitive
return self
}
func (*Wildcard) Name() string {
return "wildcard"
}
func (self *Wildcard) Accept(file *goldsmith.File) bool {
filePath := self.adjustCase(file.Path())
for _, wildcard := range self.wildcards {
wildcard = self.adjustCase(wildcard)
if matched, _ := doublestar.PathMatch(wildcard, filePath); matched {
return true
}
}
return false
}
func (self *Wildcard) adjustCase(str string) string {
if self.caseSensitive {
return str
}
return strings.ToLower(str)
}

View File

@ -0,0 +1,17 @@
package wildcard
import (
"testing"
"git.foosoft.net/alex/goldsmith"
"git.foosoft.net/alex/goldsmith/harness"
)
func Test(t *testing.T) {
harness.Validate(
t,
func(gs *goldsmith.Goldsmith) {
gs.FilterPush(New("**/*.txt", "*.md"))
},
)
}

27
go.mod
View File

@ -1,3 +1,28 @@
module git.foosoft.net/alex/goldsmith
go 1.13
go 1.20
require (
github.com/BurntSushi/toml v1.3.2
github.com/PuerkitoBio/goquery v1.8.1
github.com/alecthomas/chroma v0.10.0
github.com/bmatcuk/doublestar/v4 v4.6.1
github.com/disintegration/imaging v1.6.2
github.com/fsnotify/fsnotify v1.7.0
github.com/gorilla/feeds v1.1.2
github.com/tdewolff/minify/v2 v2.20.17
github.com/yuin/goldmark v1.7.0
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/tdewolff/parse/v2 v2.7.12 // indirect
golang.org/x/image v0.15.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.17.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)

96
go.sum Normal file
View 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=

View File

@ -1,107 +1,78 @@
// Package goldsmith generates static websites.
package goldsmith
import (
"fmt"
"sync"
)
// Goldsmith chainable context.
type Goldsmith struct {
sourceDir string
targetDir string
contexts []*Context
cache *cache
filters filterStack
clean bool
index int
errors []error
mutex sync.Mutex
chain *chainState
}
// Begin starts a chain, reading the files located in the source directory as input.
func Begin(sourceDir string) *Goldsmith {
goldsmith := &Goldsmith{sourceDir: sourceDir}
goldsmith.Chain(&loader{})
return goldsmith
func (self *Goldsmith) Begin(sourceDir string) *Goldsmith {
self.chain = &chainState{}
self.Chain(&fileImporter{sourceDir: sourceDir})
return self
}
// Cache enables caching in cacheDir for the remainder of the chain.
func (self *Goldsmith) Cache(cacheDir string) *Goldsmith {
self.cache = &cache{cacheDir}
self.chain.cache = &cache{cacheDir}
return self
}
// Clean enables or disables removal of leftover files in the target directory.
func (self *Goldsmith) Clean(clean bool) *Goldsmith {
self.clean = clean
self.chain.clean = clean
return self
}
// Chain links a plugin instance into the chain.
func (self *Goldsmith) Chain(plugin Plugin) *Goldsmith {
context := &Context{
goldsmith: self,
chain: self.chain,
plugin: plugin,
filtersExt: append(filterStack(nil), self.filters...),
index: self.index,
filtersExt: append(filterStack(nil), self.chain.filters...),
index: self.chain.index,
filesOut: make(chan *File),
}
if len(self.contexts) > 0 {
context.filesIn = self.contexts[len(self.contexts)-1].filesOut
if len(self.chain.contexts) > 0 {
context.filesIn = self.chain.contexts[len(self.chain.contexts)-1].filesOut
}
self.contexts = append(self.contexts, context)
self.index++
self.chain.contexts = append(self.chain.contexts, context)
self.chain.index++
return self
}
// FilterPush pushes a filter instance on the chain's filter stack.
func (self *Goldsmith) FilterPush(filter Filter) *Goldsmith {
self.filters.push(filter, self.index)
self.index++
self.chain.filters.push(filter, self.chain.index)
self.chain.index++
return self
}
// FilterPop pops a filter instance from the chain's filter stack.
func (self *Goldsmith) FilterPop() *Goldsmith {
self.filters.pop()
self.index++
self.chain.filters.pop()
self.chain.index++
return self
}
// End stops a chain, writing all recieved files to targetDir as output.
func (self *Goldsmith) End(targetDir string) []error {
self.targetDir = targetDir
self.Chain(&saver{clean: self.clean})
for _, context := range self.contexts {
self.Chain(&fileExporter{targetDir: targetDir, clean: self.chain.clean})
for _, context := range self.chain.contexts {
go context.step()
}
context := self.contexts[len(self.contexts)-1]
context := self.chain.contexts[len(self.chain.contexts)-1]
for range context.filesOut {
}
return self.errors
}
func (self *Goldsmith) fault(name string, file *File, err error) {
self.mutex.Lock()
defer self.mutex.Unlock()
var faultError error
if file == nil {
faultError = fmt.Errorf("[%s]: %w", name, err)
} else {
faultError = fmt.Errorf("[%s@%v]: %w", name, file, err)
}
self.errors = append(self.errors, faultError)
errors := self.chain.errors
self.chain = nil
return errors
}

111
harness/harness.go Normal file
View 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()
}

View File

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

View File

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

View File

@ -0,0 +1,114 @@
// Package absolute converts relative file references in HTML documents to
// absolute paths. This is useful when working with plugins like "layout" and
// "collection", which can render a pages content from the context of a
// different directory (imagine an index page showing inline previews of blog
// posts). This plugin makes it easy to fix incorrect relative file references
// by making sure all paths are absolute before content is featured on other
// sections of your site.
package absolute
import (
"bytes"
"fmt"
"net/url"
"path"
"git.foosoft.net/alex/goldsmith"
"git.foosoft.net/alex/goldsmith/filters/wildcard"
"github.com/PuerkitoBio/goquery"
)
// Absolute chainable plugin context.
type Absolute struct {
attributes []string
baseUrl *url.URL
}
// New creates absolute new instance of the Absolute plugin.
func New() *Absolute {
return &Absolute{attributes: []string{"href", "src"}}
}
// Attributes sets the attributes which are scanned for relative URLs (default: "href", "src").
func (self *Absolute) Attributes(attributes ...string) *Absolute {
self.attributes = attributes
return self
}
// BaseUrl sets the base URL which is prepended to absolute-converted relative paths.
func (self *Absolute) BaseUrl(baseUrl string) *Absolute {
self.baseUrl, _ = url.Parse(baseUrl)
return self
}
func (*Absolute) Name() string {
return "absolute"
}
func (*Absolute) Initialize(context *goldsmith.Context) error {
context.Filter(wildcard.New("**/*.html", "**/*.htm"))
return nil
}
func (self *Absolute) Process(context *goldsmith.Context, inputFile *goldsmith.File) error {
if outputFile := context.RetrieveCachedFile(inputFile.Path(), inputFile); outputFile != nil {
outputFile.CopyProps(inputFile)
context.DispatchFile(outputFile)
return nil
}
fileUrl, err := url.Parse(inputFile.Path())
if err != nil {
return err
}
doc, err := goquery.NewDocumentFromReader(inputFile)
if err != nil {
return err
}
for _, attribute := range self.attributes {
cssPath := fmt.Sprintf("*[%s]", attribute)
doc.Find(cssPath).Each(func(index int, selection *goquery.Selection) {
value, exists := selection.Attr(attribute)
if !exists {
return
}
currUrl, err := url.Parse(value)
if err != nil {
return
}
if currUrl.IsAbs() {
return
}
currUrl = fileUrl.ResolveReference(currUrl)
if self.baseUrl != nil {
rebasedUrl := *self.baseUrl
rebasedUrl.Path = path.Join(rebasedUrl.Path, currUrl.Path)
rebasedUrl.Fragment = currUrl.Fragment
rebasedUrl.RawFragment = currUrl.RawFragment
rebasedUrl.RawQuery = currUrl.RawQuery
currUrl = &rebasedUrl
}
selection.SetAttr(attribute, currUrl.String())
})
}
html, err := doc.Html()
if err != nil {
return err
}
outputFile, err := context.CreateFileFromReader(inputFile.Path(), bytes.NewReader([]byte(html)))
if err != nil {
return err
}
outputFile.CopyProps(inputFile)
context.DispatchAndCacheFile(outputFile, inputFile)
return nil
}

View File

@ -0,0 +1,17 @@
package absolute
import (
"testing"
"git.foosoft.net/alex/goldsmith"
"git.foosoft.net/alex/goldsmith/harness"
)
func Test(self *testing.T) {
harness.Validate(
self,
func(gs *goldsmith.Goldsmith) {
gs.Chain(New().BaseUrl("https://foosoft.net"))
},
)
}

View File

@ -0,0 +1,7 @@
<html><head></head><body>
<a href="https://foosoft.net/">Relative link</a>
<a href="https://foosoft.net">Absolute link</a>
<a href="https://www.example.com/dir/index.html">External link</a>
</body></html>

View File

@ -0,0 +1,7 @@
<html><head></head><body>
<a href="https://foosoft.net/dir/index.html#anchor">Relative link</a>
<a href="https://foosoft.net/index.html?query">Absolute link</a>
<a href="https://www.example.com/dir/index.html">External link</a>
</body></html>

View File

@ -0,0 +1,7 @@
<html>
<body>
<a href="../">Relative link</a>
<a href="https://foosoft.net">Absolute link</a>
<a href="https://www.example.com/dir/index.html">External link</a>
</body>
</html>

View File

@ -0,0 +1,7 @@
<html>
<body>
<a href="dir/index.html#anchor">Relative link</a>
<a href="/index.html?query">Absolute link</a>
<a href="https://www.example.com/dir/index.html">External link</a>
</body>
</html>

View File

@ -0,0 +1,131 @@
// Package breadcrumbs generates metadata required to enable breadcrumb
// navigation. This is particularly helpful for sites that have deep
// hierarchies which may be otherwise confusing to visitors.
package breadcrumbs
import (
"fmt"
"sync"
"git.foosoft.net/alex/goldsmith"
"git.foosoft.net/alex/goldsmith/filters/wildcard"
)
// Crumb provides organizational information about this node and ones before it.
type Crumb struct {
Ancestors []*Node
Node *Node
}
// Node represents information about a specific file in the site's structure.
type Node struct {
File *goldsmith.File
Parent *Node
Children []*Node
parentName string
}
// Breadcrumbs chainable plugin context.
type Breadcrumbs struct {
nameKey string
parentKey string
crumbsKey string
allNodes []*Node
namedNodes map[string]*Node
mutex sync.Mutex
}
// New creates a new instance of the Breadcrumbs plugin.
func New() *Breadcrumbs {
return &Breadcrumbs{
nameKey: "CrumbName",
parentKey: "CrumbParent",
crumbsKey: "Crumbs",
namedNodes: make(map[string]*Node),
}
}
// NameKey sets the metadata key used to access the crumb name (default: "CrumbName").
// Crumb names must be globally unique within any given website.
func (self *Breadcrumbs) NameKey(key string) *Breadcrumbs {
self.nameKey = key
return self
}
// ParentKey sets the metadata key used to access the parent name (default: "CrumbParent").
func (self *Breadcrumbs) ParentKey(key string) *Breadcrumbs {
self.parentKey = key
return self
}
// CrumbsKey sets the metadata key used to store information about crumbs (default: "Crumbs").
func (self *Breadcrumbs) CrumbsKey(key string) *Breadcrumbs {
self.crumbsKey = key
return self
}
func (*Breadcrumbs) Name() string {
return "breadcrumbs"
}
func (*Breadcrumbs) Initialize(context *goldsmith.Context) error {
context.Filter(wildcard.New("**/*.html", "**/*.htm"))
return nil
}
func (self *Breadcrumbs) Process(context *goldsmith.Context, inputFile *goldsmith.File) error {
var parentNameStr string
if parentName, ok := inputFile.Prop(self.parentKey); ok {
parentNameStr, _ = parentName.(string)
}
var nodeNameStr string
if nodeName, ok := inputFile.Prop(self.nameKey); ok {
nodeNameStr, _ = nodeName.(string)
}
self.mutex.Lock()
defer self.mutex.Unlock()
node := &Node{File: inputFile, parentName: parentNameStr}
self.allNodes = append(self.allNodes, node)
if len(nodeNameStr) > 0 {
if _, ok := self.namedNodes[nodeNameStr]; ok {
return fmt.Errorf("duplicate node: %s", nodeNameStr)
}
self.namedNodes[nodeNameStr] = node
}
return nil
}
func (self *Breadcrumbs) Finalize(context *goldsmith.Context) error {
for _, node := range self.allNodes {
if len(node.parentName) == 0 {
continue
}
if parentNode, ok := self.namedNodes[node.parentName]; ok {
parentNode.Children = append(parentNode.Children, node)
node.Parent = parentNode
} else {
return fmt.Errorf("undefined parent: %s", node.parentName)
}
}
for _, node := range self.allNodes {
var ancestors []*Node
for currentNode := node.Parent; currentNode != nil; currentNode = currentNode.Parent {
ancestors = append([]*Node{currentNode}, ancestors...)
}
node.File.SetProp(self.crumbsKey, Crumb{ancestors, node})
context.DispatchFile(node.File)
}
return nil
}

View File

@ -0,0 +1,22 @@
package breadcrumbs
import (
"testing"
"git.foosoft.net/alex/goldsmith"
"git.foosoft.net/alex/goldsmith/harness"
"git.foosoft.net/alex/goldsmith/plugins/frontmatter"
"git.foosoft.net/alex/goldsmith/plugins/layout"
)
func Test(self *testing.T) {
harness.Validate(
self,
func(gs *goldsmith.Goldsmith) {
gs.
Chain(frontmatter.New()).
Chain(New()).
Chain(layout.New())
},
)
}

View File

@ -0,0 +1,26 @@
<html>
<body>
<h1>Child 1</h1>
<ul>
<li><a href="child_1.html">Child 1</a></li>
<li><a href="child_2.html">Child 2</a></li>
<li><a href="child_3.html">Child 3</a></li>
<li><a href="child_4.html">Child 4</a></li>
<li><a href="parent_1.html">Parent 1</a></li>
<li><a href="parent_2.html">Parent 2</a></li>
<li><a href="root_1.html">Root 1</a></li>
<li><a href="root_2.html">Root 2</a></li>
</ul>
<div>
<a href="root_1.html" class="breadcrumb-item">Root 1</a> &gt;
<a href="parent_1.html" class="breadcrumb-item">Parent 1</a> &gt;
<span class="breadcrumb-item active">Child 1</span>
</div>
</body>
</html>

View File

@ -0,0 +1,26 @@
<html>
<body>
<h1>Child 2</h1>
<ul>
<li><a href="child_1.html">Child 1</a></li>
<li><a href="child_2.html">Child 2</a></li>
<li><a href="child_3.html">Child 3</a></li>
<li><a href="child_4.html">Child 4</a></li>
<li><a href="parent_1.html">Parent 1</a></li>
<li><a href="parent_2.html">Parent 2</a></li>
<li><a href="root_1.html">Root 1</a></li>
<li><a href="root_2.html">Root 2</a></li>
</ul>
<div>
<a href="root_1.html" class="breadcrumb-item">Root 1</a> &gt;
<a href="parent_1.html" class="breadcrumb-item">Parent 1</a> &gt;
<span class="breadcrumb-item active">Child 2</span>
</div>
</body>
</html>

View File

@ -0,0 +1,26 @@
<html>
<body>
<h1>Child 3</h1>
<ul>
<li><a href="child_1.html">Child 1</a></li>
<li><a href="child_2.html">Child 2</a></li>
<li><a href="child_3.html">Child 3</a></li>
<li><a href="child_4.html">Child 4</a></li>
<li><a href="parent_1.html">Parent 1</a></li>
<li><a href="parent_2.html">Parent 2</a></li>
<li><a href="root_1.html">Root 1</a></li>
<li><a href="root_2.html">Root 2</a></li>
</ul>
<div>
<a href="root_1.html" class="breadcrumb-item">Root 1</a> &gt;
<a href="parent_2.html" class="breadcrumb-item">Parent 2</a> &gt;
<span class="breadcrumb-item active">Child 3</span>
</div>
</body>
</html>

View File

@ -0,0 +1,26 @@
<html>
<body>
<h1>Child 4</h1>
<ul>
<li><a href="child_1.html">Child 1</a></li>
<li><a href="child_2.html">Child 2</a></li>
<li><a href="child_3.html">Child 3</a></li>
<li><a href="child_4.html">Child 4</a></li>
<li><a href="parent_1.html">Parent 1</a></li>
<li><a href="parent_2.html">Parent 2</a></li>
<li><a href="root_1.html">Root 1</a></li>
<li><a href="root_2.html">Root 2</a></li>
</ul>
<div>
<a href="root_1.html" class="breadcrumb-item">Root 1</a> &gt;
<a href="parent_2.html" class="breadcrumb-item">Parent 2</a> &gt;
<span class="breadcrumb-item active">Child 4</span>
</div>
</body>
</html>

View File

@ -0,0 +1,24 @@
<html>
<body>
<h1>Parent 1</h1>
<ul>
<li><a href="child_1.html">Child 1</a></li>
<li><a href="child_2.html">Child 2</a></li>
<li><a href="child_3.html">Child 3</a></li>
<li><a href="child_4.html">Child 4</a></li>
<li><a href="parent_1.html">Parent 1</a></li>
<li><a href="parent_2.html">Parent 2</a></li>
<li><a href="root_1.html">Root 1</a></li>
<li><a href="root_2.html">Root 2</a></li>
</ul>
<div>
<a href="root_1.html" class="breadcrumb-item">Root 1</a> &gt;
<span class="breadcrumb-item active">Parent 1</span>
</div>
</body>
</html>

View File

@ -0,0 +1,24 @@
<html>
<body>
<h1>Parent 2</h1>
<ul>
<li><a href="child_1.html">Child 1</a></li>
<li><a href="child_2.html">Child 2</a></li>
<li><a href="child_3.html">Child 3</a></li>
<li><a href="child_4.html">Child 4</a></li>
<li><a href="parent_1.html">Parent 1</a></li>
<li><a href="parent_2.html">Parent 2</a></li>
<li><a href="root_1.html">Root 1</a></li>
<li><a href="root_2.html">Root 2</a></li>
</ul>
<div>
<a href="root_1.html" class="breadcrumb-item">Root 1</a> &gt;
<span class="breadcrumb-item active">Parent 2</span>
</div>
</body>
</html>

View File

@ -0,0 +1,17 @@
<html>
<body>
<h1>Root 1</h1>
<ul>
<li><a href="child_1.html">Child 1</a></li>
<li><a href="child_2.html">Child 2</a></li>
<li><a href="child_3.html">Child 3</a></li>
<li><a href="child_4.html">Child 4</a></li>
<li><a href="parent_1.html">Parent 1</a></li>
<li><a href="parent_2.html">Parent 2</a></li>
<li><a href="root_1.html">Root 1</a></li>
<li><a href="root_2.html">Root 2</a></li>
</ul>
</body>
</html>

View File

@ -0,0 +1,17 @@
<html>
<body>
<h1>Root 2</h1>
<ul>
<li><a href="child_1.html">Child 1</a></li>
<li><a href="child_2.html">Child 2</a></li>
<li><a href="child_3.html">Child 3</a></li>
<li><a href="child_4.html">Child 4</a></li>
<li><a href="parent_1.html">Parent 1</a></li>
<li><a href="parent_2.html">Parent 2</a></li>
<li><a href="root_1.html">Root 1</a></li>
<li><a href="root_2.html">Root 2</a></li>
</ul>
</body>
</html>

View File

@ -0,0 +1,5 @@
+++
Layout = "page"
CrumbName = "Child 1"
CrumbParent = "Parent 1"
+++

View File

@ -0,0 +1,5 @@
+++
Layout = "page"
CrumbName = "Child 2"
CrumbParent = "Parent 1"
+++

View File

@ -0,0 +1,5 @@
+++
Layout = "page"
CrumbName = "Child 3"
CrumbParent = "Parent 2"
+++

View File

@ -0,0 +1,5 @@
+++
Layout = "page"
CrumbName = "Child 4"
CrumbParent = "Parent 2"
+++

View File

@ -0,0 +1,5 @@
+++
Layout = "page"
CrumbName = "Parent 1"
CrumbParent = "Root 1"
+++

View File

@ -0,0 +1,5 @@
+++
Layout = "page"
CrumbName = "Parent 2"
CrumbParent = "Root 1"
+++

View File

@ -0,0 +1,4 @@
+++
Layout = "page"
CrumbName = "Root 1"
+++

View File

@ -0,0 +1,4 @@
+++
Layout = "page"
CrumbName = "Root 2"
+++

View 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> &gt;
{{end}}
<span class="breadcrumb-item active">{{.Props.CrumbName}}</span>
</div>
{{end}}
</body>
</html>
{{end}}

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

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

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

View 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