Compare commits

..

No commits in common. "f5cee223248961f0f9a469e6a6b08e18ae9c48da" and "e4eacea3dd449db2b46cd6fd4ce2cd98e0d82367" have entirely different histories.

273 changed files with 411 additions and 6660 deletions

1
.gitignore vendored
View File

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

View File

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

View File

@ -1,32 +0,0 @@
package goldsmith
import (
"fmt"
"sync"
)
type chainState struct {
contexts []*Context
cache *cache
filters filterStack
clean bool
index int
errors []error
mutex sync.Mutex
}
func (self *chainState) fault(name string, file *File, err error) {
self.mutex.Lock()
defer self.mutex.Unlock()
var faultError error
if file == nil {
faultError = fmt.Errorf("[%s]: %w", name, err)
} else {
faultError = fmt.Errorf("[%s@%v]: %w", name, file, err)
}
self.errors = append(self.errors, faultError)
}

View File

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

View File

@ -1,115 +0,0 @@
// Package devserver makes it easy to view statically generated websites and
// automatically rebuild them when source data changes. When combined with the
// "livejs" plugin, it is possible to have a live preview of your site.
package devserver
import (
"fmt"
"log"
"net/http"
"os"
"path"
"sync"
"time"
"github.com/fsnotify/fsnotify"
)
// Builder interface should be implemented by you to contain the required
// goldsmith chain to generate your website.
type Builder interface {
Build(sourceDir, targetDir, cacheDir string)
}
// DevServe should be called to start a web server using the provided builder.
// While the source directory will be watched for changes by default, it is
// possible to pass in additional directories to watch; modification of these
// directories will automatically trigger a site rebuild. This function does
// not return and will continue watching for file changes and serving your
// website until it is terminated.
func DevServe(builder Builder, port int, sourceDir, targetDir, cacheDir string, watchDirs ...string) {
dirs := append(watchDirs, sourceDir)
build(dirs, func() {
builder.Build(sourceDir, targetDir, cacheDir)
})
httpAddr := fmt.Sprintf(":%d", port)
httpHandler := http.FileServer(http.Dir(targetDir))
log.Fatal(http.ListenAndServe(httpAddr, httpHandler))
}
func build(dirs []string, callback func()) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
var mutex sync.Mutex
timestamp := time.Now()
dirty := true
go func() {
for {
select {
case event := <-watcher.Events:
mutex.Lock()
timestamp = time.Now()
dirty = true
mutex.Unlock()
if event.Op&fsnotify.Create == fsnotify.Create {
info, err := os.Stat(event.Name)
if os.IsNotExist(err) {
continue
}
if err != nil {
log.Fatal(err)
}
if info.IsDir() {
watch(event.Name, watcher)
} else {
watcher.Add(event.Name)
}
}
case err := <-watcher.Errors:
log.Fatal(err)
}
}
}()
go func() {
for range time.Tick(10 * time.Millisecond) {
if dirty && time.Now().Sub(timestamp) > 100*time.Millisecond {
mutex.Lock()
dirty = false
mutex.Unlock()
callback()
}
}
}()
for _, dir := range dirs {
watch(dir, watcher)
}
}
func watch(dir string, watcher *fsnotify.Watcher) {
watcher.Add(dir)
items, err := os.ReadDir(dir)
if err != nil {
log.Fatal(err)
}
for _, item := range items {
fullPath := path.Join(dir, item.Name())
if item.IsDir() {
watch(fullPath, watcher)
} else {
watcher.Add(fullPath)
}
}
}

View File

@ -1,32 +0,0 @@
package goldsmith
// Plugin contains the minimum set of methods required on plugins. Plugins can
// also optionally implement Initializer, Processor, and Finalizer interfaces.
type (
Plugin interface {
Name() string
}
// Initializer is used to optionally initialize a plugin and to specify a
// filter to be used for determining which files will be processed.
Initializer interface {
Initialize(context *Context) error
}
// Processor allows for optional processing of files passing through a plugin.
Processor interface {
Process(context *Context, file *File) error
}
// Finalizer allows for optional finalization of a plugin after all files
// queued in the chain have passed through it.
Finalizer interface {
Finalize(context *Context) error
}
// Filter is used to determine which files should continue in the chain.
Filter interface {
Name() string
Accept(file *File) bool
}
)

122
file.go
View File

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

View File

@ -1,90 +0,0 @@
package goldsmith
import (
"io"
"os"
"path/filepath"
)
type fileExporter struct {
targetDir string
clean bool
tokens map[string]bool
}
func (*fileExporter) Name() string {
return "exporter"
}
func (self *fileExporter) Initialize(context *Context) error {
self.tokens = make(map[string]bool)
context.Threads(1)
return nil
}
func (self *fileExporter) Process(context *Context, file *File) error {
slicePath := func(path string) string {
if filepath.IsAbs(path) {
var err error
if path, err = filepath.Rel("/", path); err != nil {
panic(err)
}
}
return filepath.Clean(path)
}
for token := slicePath(file.Path()); token != "."; token = filepath.Dir(token) {
self.tokens[token] = true
}
targetPath := filepath.Join(self.targetDir, file.Path())
if targetInfo, err := os.Stat(targetPath); err == nil && !targetInfo.ModTime().Before(file.ModTime()) {
return nil
}
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return err
}
fw, err := os.Create(targetPath)
if err != nil {
return err
}
defer fw.Close()
if _, err := file.Seek(0, io.SeekStart); err != nil {
return err
}
if _, err := file.WriteTo(fw); err != nil {
return err
}
return nil
}
func (self *fileExporter) Finalize(context *Context) error {
if !self.clean {
return nil
}
return filepath.Walk(self.targetDir, func(path string, info os.FileInfo, err error) error {
if path == self.targetDir {
return nil
}
relPath, err := filepath.Rel(self.targetDir, path)
if err != nil {
panic(err)
}
if tokenized, _ := self.tokens[relPath]; !tokenized {
if err := os.RemoveAll(path); err != nil {
return err
}
}
return nil
})
}

View File

@ -1,35 +0,0 @@
package goldsmith
import (
"os"
"path/filepath"
)
type fileImporter struct {
sourceDir string
}
func (*fileImporter) Name() string {
return "importer"
}
func (self *fileImporter) Initialize(context *Context) error {
return filepath.Walk(self.sourceDir, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
relPath, err := filepath.Rel(self.sourceDir, path)
if err != nil {
return err
}
file, err := context.CreateFileFromAsset(relPath, path)
if err != nil {
return err
}
context.DispatchFile(file)
return nil
})
}

49
file_util.go Normal file
View File

@ -0,0 +1,49 @@
package goldsmith
import (
"os"
"path/filepath"
"strings"
)
type filesByPath []*File
func (self filesByPath) Len() int {
return len(self)
}
func (self filesByPath) Swap(i, j int) {
self[i], self[j] = self[j], self[i]
}
func (self filesByPath) Less(i, j int) bool {
return strings.Compare(self[i].Path(), self[j].Path()) < 0
}
type fileInfo struct {
os.FileInfo
path string
}
func cleanPath(path string) string {
if filepath.IsAbs(path) {
var err error
if path, err = filepath.Rel("/", path); err != nil {
panic(err)
}
}
return filepath.Clean(path)
}
func scanDir(rootDir string, infos chan fileInfo) {
defer close(infos)
filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err == nil {
infos <- fileInfo{FileInfo: info, path: path}
}
return err
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

27
go.mod
View File

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

96
go.sum
View File

@ -1,96 +0,0 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gorilla/feeds v1.1.2 h1:pxzZ5PD3RJdhFH2FsJJ4x6PqMqbgFk1+Vez4XWBW8Iw=
github.com/gorilla/feeds v1.1.2/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tdewolff/minify/v2 v2.20.17 h1:zGqEDhspr3XjSrQI/56vw9IdAhLAaKTLXWnDBsxNVt8=
github.com/tdewolff/minify/v2 v2.20.17/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM=
github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ=
github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

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

View File

@ -1,111 +0,0 @@
// Package harness provides a simple way to test goldsmith plugins and filters.
// It executes a goldsmith chain on provided "source" data and compares the
// generated "target" resuts with the known to be good "reference" data.
package harness
import (
"errors"
"fmt"
"hash/crc32"
"io"
"log"
"os"
"path/filepath"
"testing"
"git.foosoft.net/alex/goldsmith"
)
// StagingCallback callback function is used to set up a goldsmith chain.
type StagingCallback func(gs *goldsmith.Goldsmith)
// Validate enables validation of a single, unnamed case (test data is stored in "testdata").
func Validate(t *testing.T, stager StagingCallback) {
ValidateCase(t, "", stager)
}
// ValidateCase enables enables of a single, named case (test data is stored in "testdata/caseName").
func ValidateCase(t *testing.T, caseName string, stager StagingCallback) {
var (
caseDir = filepath.Join("testdata", caseName)
sourceDir = filepath.Join(caseDir, "source")
targetDir = filepath.Join(caseDir, "target")
cacheDir = filepath.Join(caseDir, "cache")
referenceDir = filepath.Join(caseDir, "reference")
)
if errs := validate(sourceDir, targetDir, cacheDir, referenceDir, stager); len(errs) > 0 {
for _, err := range errs {
log.Println(err)
}
t.Fail()
}
}
func validate(sourceDir, targetDir, cacheDir, referenceDir string, stager StagingCallback) []error {
if err := os.RemoveAll(targetDir); err != nil {
return []error{err}
}
if err := os.RemoveAll(cacheDir); err != nil {
return []error{err}
}
defer os.RemoveAll(cacheDir)
for i := 0; i < 2; i++ {
if errs := execute(sourceDir, targetDir, cacheDir, stager); errs != nil {
return errs
}
if hashDirState(targetDir) != hashDirState(referenceDir) {
return []error{errors.New("directory contents do not match")}
}
}
return nil
}
func execute(sourceDir, targetDir, cacheDir string, stager StagingCallback) []error {
var gs goldsmith.Goldsmith
gs.Begin(sourceDir).Cache(cacheDir).Clean(true)
stager(&gs)
return gs.End(targetDir)
}
func hashDirState(dir string) uint32 {
hasher := crc32.NewIEEE()
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(dir, path)
if err != nil {
return err
}
summary := fmt.Sprintf("%s %t", relPath, info.IsDir())
if _, err := hasher.Write([]byte(summary)); err != nil {
return err
}
if !info.IsDir() {
fp, err := os.Open(path)
if err != nil {
return err
}
defer fp.Close()
if _, err := io.Copy(hasher, fp); err != nil {
return err
}
}
return nil
})
return hasher.Sum32()
}

30
interface.go Normal file
View File

@ -0,0 +1,30 @@
package goldsmith
// Plugin contains the minimum set of methods required on plugins. Plugins can
// also optionally implement Initializer, Processor, and Finalizer interfaces.
type Plugin interface {
Name() string
}
// Initializer is used to optionally initialize a plugin and to specify a
// filter to be used for determining which files will be processed.
type Initializer interface {
Initialize(context *Context) error
}
// Processor allows for optional processing of files passing through a plugin.
type Processor interface {
Process(context *Context, file *File) error
}
// Finalizer allows for optional finalization of a plugin after all files
// queued in the chain have passed through it.
type Finalizer interface {
Finalize(context *Context) error
}
// Filter is used to determine which files should continue in the chain.
type Filter interface {
Name() string
Accept(file *File) bool
}

30
loader.go Normal file
View File

@ -0,0 +1,30 @@
package goldsmith
import "path/filepath"
type loader struct{}
func (*loader) Name() string {
return "loader"
}
func (*loader) Initialize(context *Context) error {
scannedInfo := make(chan fileInfo)
go scanDir(context.goldsmith.sourceDir, scannedInfo)
for info := range scannedInfo {
if info.IsDir() {
continue
}
relPath, _ := filepath.Rel(context.goldsmith.sourceDir, info.path)
file, err := context.CreateFileFromAsset(relPath, info.path)
if err != nil {
return err
}
context.DispatchFile(file)
}
return nil
}

View File

@ -1,114 +0,0 @@
// Package absolute converts relative file references in HTML documents to
// absolute paths. This is useful when working with plugins like "layout" and
// "collection", which can render a 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +0,0 @@
{{define "page"}}
<html>
<body>
<h1>{{.Props.CrumbName}}</h1>
<ul>
<li><a href="child_1.html">Child 1</a></li>
<li><a href="child_2.html">Child 2</a></li>
<li><a href="child_3.html">Child 3</a></li>
<li><a href="child_4.html">Child 4</a></li>
<li><a href="parent_1.html">Parent 1</a></li>
<li><a href="parent_2.html">Parent 2</a></li>
<li><a href="root_1.html">Root 1</a></li>
<li><a href="root_2.html">Root 2</a></li>
</ul>
{{if .Props.CrumbParent}}
<div>
{{range .Props.Crumbs.Ancestors}}
<a href="{{.File.Path}}" class="breadcrumb-item">{{.File.Props.CrumbName}}</a> &gt;
{{end}}
<span class="breadcrumb-item active">{{.Props.CrumbName}}</span>
</div>
{{end}}
</body>
</html>
{{end}}

View File

@ -1,131 +0,0 @@
// Package collection groups related pages into named collections. This can be
// useful for presenting blog posts on your front page, and displaying summary
// information about other types of content on your website. It can be used in
// conjunction with the "pager" plugin to create large collections which are
// split across several pages.
package collection
import (
"sort"
"strings"
"sync"
"git.foosoft.net/alex/goldsmith"
"git.foosoft.net/alex/goldsmith/filters/wildcard"
)
// A Comparer callback function is used to sort files within a collection group.
type Comparer func(i, j *goldsmith.File) (less bool)
// Collection chainable plugin context.
type Collection struct {
collectionKey string
groupsKey string
comparer Comparer
groups map[string][]*goldsmith.File
files []*goldsmith.File
mutex sync.Mutex
}
// New creates a new instance of the Collection plugin.
func New() *Collection {
return &Collection{
collectionKey: "Collection",
groupsKey: "Groups",
groups: make(map[string][]*goldsmith.File),
}
}
// CollectionKey sets the metadata key used to access the collection name (default: "Collection").
// The metadata associated with this key can be either a single string or an array of strings.
func (self *Collection) CollectionKey(collectionKey string) *Collection {
self.collectionKey = collectionKey
return self
}
// GroupsKey sets the metadata key used to store information about collection groups (default: "Groups").
// This information is stored as a mapping of group names to contained files.
func (self *Collection) GroupsKey(groupsKey string) *Collection {
self.groupsKey = groupsKey
return self
}
// Comparer sets the function used to sort files in collection groups (default: sort by filenames).
func (plugin *Collection) Comparer(comparer Comparer) *Collection {
plugin.comparer = comparer
return plugin
}
func (*Collection) Name() string {
return "collection"
}
func (*Collection) Initialize(context *goldsmith.Context) error {
context.Filter(wildcard.New("**/*.html", "**/*.htm"))
return nil
}
func (self *Collection) Process(context *goldsmith.Context, inputFile *goldsmith.File) error {
self.mutex.Lock()
defer func() {
inputFile.SetProp(self.groupsKey, self.groups)
self.files = append(self.files, inputFile)
self.mutex.Unlock()
}()
collectionRaw, ok := inputFile.Prop(self.collectionKey)
if !ok {
return nil
}
var collectionNames []string
switch t := collectionRaw.(type) {
case string:
collectionNames = append(collectionNames, t)
case []string:
collectionNames = append(collectionNames, t...)
}
for _, collectionName := range collectionNames {
files, _ := self.groups[collectionName]
files = append(files, inputFile)
self.groups[collectionName] = files
}
return nil
}
func (self *Collection) Finalize(context *goldsmith.Context) error {
for _, files := range self.groups {
fg := &fileSorter{files, self.comparer}
sort.Sort(fg)
}
for _, file := range self.files {
context.DispatchFile(file)
}
return nil
}
type fileSorter struct {
files []*goldsmith.File
comparer Comparer
}
func (self fileSorter) Len() int {
return len(self.files)
}
func (self fileSorter) Swap(i, j int) {
self.files[i], self.files[j] = self.files[j], self.files[i]
}
func (self fileSorter) Less(i, j int) bool {
if self.comparer == nil {
return strings.Compare(self.files[i].Path(), self.files[j].Path()) < 0
}
return self.comparer(self.files[i], self.files[j])
}

View File

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

View File

@ -1,46 +0,0 @@
<html>
<body>
<div>
<h1>Group 1</h1>
<ul>
<li>
<a href="page_1.html">Page 1</a>
</li>
<li>
<a href="page_2.html">Page 2</a>
</li>
<li>
<a href="page_3.html">Page 3</a>
</li>
</ul>
</div>
<div>
<h1>Group 2</h1>
<ul>
<li>
<a href="page_4.html">Page 4</a>
</li>
<li>
<a href="page_5.html">Page 5</a>
</li>
<li>
<a href="page_6.html">Page 6</a>
</li>
</ul>
</div>
</body>
</html>

View File

@ -1,6 +0,0 @@
<html>
<body>
<h1>Page 1</h1>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More