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

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

View File

@ -8,7 +8,6 @@ import (
"os"
"path/filepath"
"sort"
"strings"
)
type cache struct {
@ -16,7 +15,7 @@ type cache struct {
}
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 {
return nil, err
}
@ -33,8 +32,8 @@ func (self *cache) retrieveFile(context *Context, outputPath string, inputFiles
return outputFile, nil
}
func (self *cache) storeFile(outputFile *File, inputFiles []*File) error {
cachePath, err := self.buildCachePath(outputFile.Path(), inputFiles)
func (self *cache) storeFile(context *Context, outputFile *File, inputFiles []*File) error {
cachePath, err := self.buildCachePath(context, outputFile.Path(), inputFiles)
if err != nil {
return err
}
@ -69,13 +68,11 @@ func (self *cache) storeFile(outputFile *File, inputFiles []*File) error {
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.Write([]byte(outputPath))
sort.Slice(inputFiles, func(i, j int) bool {
return strings.Compare(inputFiles[i].Path(), inputFiles[j].Path()) < 0
})
sort.Sort(filesByPath(inputFiles))
for _, inputFile := range inputFiles {
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
// that enable plugins to inject new files into the chain.
type Context struct {
chain *chainState
goldsmith *Goldsmith
plugin Plugin
filtersExt filterStack
@ -28,19 +29,15 @@ type Context struct {
}
// CreateFileFrom data creates a new file instance from the provided data buffer.
func (self *Context) CreateFileFromReader(relPath string, reader io.Reader) (*File, error) {
if filepath.IsAbs(relPath) {
return nil, errors.New("file paths must be relative")
}
func (self *Context) CreateFileFromReader(sourcePath string, reader io.Reader) (*File, error) {
data, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
file := &File{
relPath: relPath,
props: make(FileProps),
relPath: sourcePath,
props: make(map[string]Prop),
modTime: time.Now(),
size: int64(len(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.
func (self *Context) CreateFileFromAsset(relPath, dataPath string) (*File, error) {
if filepath.IsAbs(relPath) {
return nil, errors.New("file paths must be relative")
func (self *Context) CreateFileFromAsset(sourcePath, dataPath string) (*File, error) {
if filepath.IsAbs(sourcePath) {
return nil, errors.New("source paths must be relative")
}
if filepath.IsAbs(dataPath) {
return nil, errors.New("data paths must be relative")
}
info, err := os.Stat(dataPath)
@ -61,12 +62,12 @@ func (self *Context) CreateFileFromAsset(relPath, dataPath string) (*File, error
return nil, err
}
if info.IsDir() {
return nil, errors.New("file paths cannot be directories")
return nil, errors.New("assets must be files")
}
file := &File{
relPath: relPath,
props: make(FileProps),
relPath: sourcePath,
props: make(map[string]Prop),
modTime: info.ModTime(),
size: info.Size(),
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
// passes it to the next link in the chain.
func (self *Context) DispatchAndCacheFile(outputFile *File, inputFiles ...*File) {
if self.chain.cache != nil {
self.chain.cache.storeFile(outputFile, inputFiles)
if self.goldsmith.cache != nil {
self.goldsmith.cache.storeFile(self, outputFile, inputFiles)
}
self.DispatchFile(outputFile)
self.filesOut <- outputFile
}
// 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.
func (self *Context) RetrieveCachedFile(outputPath string, inputFiles ...*File) *File {
var outputFile *File
if self.chain.cache != nil {
outputFile, _ = self.chain.cache.retrieveFile(self, outputPath, inputFiles)
if self.goldsmith.cache != nil {
outputFile, _ = self.goldsmith.cache.retrieveFile(self, outputPath, inputFiles)
}
return outputFile
@ -124,7 +125,7 @@ func (self *Context) step() {
if initializer, ok := self.plugin.(Initializer); ok {
if err := initializer.Initialize(self); err != nil {
self.chain.fault(self.plugin.Name(), nil, err)
self.goldsmith.fault(self.plugin.Name(), nil, err)
return
}
}
@ -145,13 +146,13 @@ func (self *Context) step() {
for inputFile := range self.filesIn {
if processor != nil && self.filtersInt.accept(inputFile) && self.filtersExt.accept(inputFile) {
if _, err := inputFile.Seek(0, io.SeekStart); err != nil {
self.chain.fault("core", inputFile, err)
self.goldsmith.fault("core", inputFile, err)
}
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 {
self.DispatchFile(inputFile)
self.filesOut <- inputFile
}
}
}()
@ -162,7 +163,7 @@ func (self *Context) step() {
if finalizer, ok := self.plugin.(Finalizer); ok {
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
}
)

110
file.go
View File

@ -2,21 +2,19 @@ package goldsmith
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"time"
)
type (
FileProp any
FileProps map[string]FileProp
type Prop interface{}
type PropMap map[string]Prop
// File represents in-memory or on-disk files in a chain.
File struct {
// File represents in-memory or on-disk files in a chain.
type File struct {
relPath string
props FileProps
props map[string]Prop
modTime time.Time
size int64
@ -24,16 +22,22 @@ type (
reader *bytes.Reader
index int
}
)
}
// Rename modifies the file path relative to the source directory.
func (self *File) Rename(path string) error {
if filepath.IsAbs(path) {
return fmt.Errorf("unexpected absolute path: %s", path)
func (self *File) Rename(path string) {
self.relPath = path
}
func (self *File) Rewrite(reader io.Reader) error {
data, err := io.ReadAll(reader)
if err != nil {
return err
}
self.relPath = path
self.reader = bytes.NewReader(data)
self.modTime = time.Now()
self.size = int64(len(data))
return nil
}
@ -42,16 +46,16 @@ func (self *File) Path() string {
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.
func (self *File) Name() string {
return filepath.Base(self.relPath)
}
// Dir returns the containing directory of the file.
func (self *File) Dir() string {
return filepath.ToSlash(filepath.Dir(self.relPath))
}
// Ext returns the extension of the file.
func (self *File) Ext() string {
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)
}
// GoString returns value for string formatting.
// Returns value for string formatting.
func (self *File) GoString() string {
return self.relPath
}
// 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) {
func (self *File) SetProp(name string, value Prop) {
self.props[name] = value
}
// Prop returns the metadata property for the provided name.
func (self *File) Prop(name string) (FileProp, bool) {
func (self *File) CopyProps(file *File) {
for key, value := range file.props {
self.props[key] = value
}
}
func (self *File) Prop(name string) (Prop, bool) {
value, ok := self.props[name]
return value, ok
}
// PropOrDef returns the metadata property for the provided name or the default.
func (self *File) PropOrDef(name string, valueDef FileProp) FileProp {
func (self *File) Props() PropMap {
return self.props
}
func (self *File) PropOrDefault(name string, valueDef Prop) Prop {
if value, ok := self.Prop(name); ok {
return value
}
@ -128,16 +134,44 @@ func (self *File) PropOrDef(name string, valueDef FileProp) FileProp {
return valueDef
}
// Props returns all of the metadata properties.
func (self *File) Props() FileProps {
return self.props
}
func (self *File) export(targetDir string) error {
targetPath := filepath.Join(targetDir, self.relPath)
// CopyProps copies all metadata properties from the provided file.
func (self *File) CopyProps(file *File) {
for key, value := range file.props {
self.props[key] = value
if targetInfo, err := os.Stat(targetPath); err == nil && !targetInfo.ModTime().Before(self.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 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 {

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
type (
filterEntry struct {
type filterEntry struct {
filter Filter
index int
}
}
filterStack []filterEntry
)
type filterStack []filterEntry
func (self *filterStack) accept(file *File) bool {
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
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
)
go 1.13

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

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