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

142
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): 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.

View File

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

110
file.go
View File

@ -2,19 +2,21 @@ 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
@ -22,22 +24,16 @@ type File struct {
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
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 package goldsmith
type filterEntry struct { type (
filterEntry struct {
filter Filter filter Filter
index int 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 {

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