Compare commits

...

6 Commits
main ... dev

@ -0,0 +1,25 @@
# Notes
(new notes at the top)
## TODO: Incremental Watcher
The DAG based incremental rebuilding will require a better file watcher that can whats entire directories and automatically recheck build rules when a new file is created.
```go
type WatchListener func(path string) error
type Watcher interface {
// OnFileChange will register a file change listener
OnFileChange(l WatchListener)
// OnFileAdded will register a file creation listener
OnFileAdded(l WatchListener)
// Watch will register "path"
Watch(path string) error
// Watch will register files matching "pattern" and all directories along the way for new files
WatchPattern(pattern string) error
}
```

@ -2,10 +2,11 @@
Cabret is a yaml based static site generator, ideally with the same features as Hugo but with a simpler model. Here is a simple example of a _Cabretfile.yaml_ Cabret is a yaml based static site generator, ideally with the same features as Hugo but with a simpler model. Here is a simple example of a _Cabretfile.yaml_
## Build ## Build & Install
```bash shell ```bash shell
$ go build -v -o ./bin/cabret ./cmd/cabret $ go build -v -o ./bin/cabret ./cmd/cabret
$ go install -v ./cmd/cabret
``` ```
## Introduction ## Introduction

@ -4,3 +4,7 @@
Lorem ipsum dolor sit amet consectetur adipisicing elit. Minima, eveniet, dolorum amet, cupiditate quae excepturi aspernatur dolor Lorem ipsum dolor sit amet consectetur adipisicing elit. Minima, eveniet, dolorum amet, cupiditate quae excepturi aspernatur dolor
voluptatem obcaecati ratione quas? Et explicabo illum iure eius porro, dolor quos doloremque! voluptatem obcaecati ratione quas? Et explicabo illum iure eius porro, dolor quos doloremque!
</p> </p>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Minima, eveniet, dolorum amet, cupiditate quae excepturi aspernatur dolor
voluptatem obcaecati ratione quas? Et explicabo illum iure eius porro, dolor quos doloremque!
</p>

@ -5,6 +5,8 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{- if .Title -}}{{ .Title }} | {{ else }}{{ end -}} My Blog</title> <title>{{- if .Title -}}{{ .Title }} | {{ else }}{{ end -}} My Blog</title>
<script type="module" src="/__cabret__/live-reload.js" async></script>
</head> </head>
<body> <body>
{{ .Content }} {{ .Content }}

@ -9,4 +9,5 @@ tags: [a, b]
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Beatae ratione eos saepe veritatis, temporibus nemo vero rerum earum voluptatibus minus voluptatem neque consequatur necessitatibus, error magnam deserunt! Ad, accusantium excepturi? Lorem ipsum dolor sit amet consectetur, adipisicing elit. Beatae ratione eos saepe veritatis, temporibus nemo vero rerum earum voluptatibus minus voluptatem neque consequatur necessitatibus, error magnam deserunt! Ad, accusantium excepturi?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Beatae ratione eos saepe veritatis, temporibus nemo vero rerum earum voluptatibus minus voluptatem neque consequatur necessitatibus, error magnam deserunt! Ad, accusantium excepturi? Lorem ipsum dolor sit amet consectetur, adipisicing elit. Beatae ratione eos saepe veritatis, temporibus nemo vero rerum earum voluptatibus minus voluptatem neque consequatur necessitatibus, error magnam deserunt! Ad, accusantium excepturi?

@ -40,12 +40,16 @@ type Operation interface {
Configure(options map[string]any) error Configure(options map[string]any) error
} }
type Context struct {
Files []string
}
type ListOperation interface { type ListOperation interface {
Operation Operation
ProcessList(items []Content) ([]Content, error) ProcessList(ctx *Context, items []Content) ([]Content, error)
} }
type ItemOperation interface { type ItemOperation interface {
Operation Operation
ProcessItem(item Content) (*Content, error) ProcessItem(ctx *Context, item Content) (*Content, error)
} }

@ -0,0 +1 @@
package main

@ -1,25 +1,185 @@
package main package main
import ( import (
"fmt"
"log" "log"
"os"
"strings"
"github.com/alecthomas/repr"
"github.com/aziis98/cabret/config" "github.com/aziis98/cabret/config"
"github.com/aziis98/cabret/runner" "github.com/aziis98/cabret/runner"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
const HelpMessage = `usage: cabret <SUBCOMMAND> [OPTIONS...]
subcommands:
build [OPTIONS...] Builds the current project
serve [OPTIONS...] Starts an http server and watches files for changes
`
func main() { func main() {
log.SetFlags(0) flagSet := pflag.FlagSet{
Usage: func() {
fmt.Print(HelpMessage)
},
}
if err := flagSet.Parse(os.Args[1:]); err != nil {
if err != pflag.ErrHelp {
log.Fatal(err)
}
return
}
switch flagSet.Arg(0) {
case "serve":
if err := serveSubcommand(os.Args[2:]); err != nil {
if err != pflag.ErrHelp {
log.Fatal(err)
}
return
}
case "build":
if err := buildSubcommand(os.Args[2:]); err != nil {
if err != pflag.ErrHelp {
log.Fatal(err)
}
return
}
default:
fmt.Print(HelpMessage)
}
}
const BuildHelpMessage = `usage: cabret build [OPTIONS...]
options:
optConfig := pflag.StringP("config", "c", "Cabretfile.yaml", `which configuration file to use`) -c, --config <path> Path to configuration file (default is "Cabretfile.yaml")
pflag.Parse() -v, --verbose Be more verbose
cabretfile, err := config.ReadCabretfile(*optConfig) `
func buildSubcommand(args []string) error {
flagSet := pflag.FlagSet{
Usage: func() { fmt.Print(BuildHelpMessage) },
}
var configFile string
flagSet.StringVarP(&configFile, "config", "c",
"Cabretfile.yaml",
`path to configuration file`,
)
var verbose bool
flagSet.BoolVarP(&verbose, "verbose", "v",
false,
`verbose output`,
)
if err := flagSet.Parse(args); err != nil {
return err
}
cabretfile, err := config.ReadCabretfile(configFile)
if err != nil { if err != nil {
log.Fatal(err) return err
} }
if err := runner.RunConfig(cabretfile); err != nil { if err := runner.RunConfig(cabretfile); err != nil {
log.Fatal(err) return err
}
return nil
}
const ServeHelpMessage = `usage: cabret serve [OPTIONS...]
options:
-c, --config <path> Path to configuration file (default is "Cabretfile.yaml")
-v, --verbose Be more verbose
-w, --watch <pattern> List of paths to watch for rebuild
-m, --mount <path> Mount a file or folder on the default static file
server, this will mount file or folder at "<path>"
on route "<path>".
-m, --mount <from>:<to> Mount a file or folder on the default static file
server, this will mount file or folder at "<from>"
on route "<to>".
By default the mount list is "public/:/", "dist/:/", "out/:/".
The server will also serve a special js script on "/cabret/live-reload.js" that
will add live reload on file change for development, this can be included with
the following tag
<script type="module" src="/__cabret__/live-reload.js" async></script>
`
func serveSubcommand(args []string) error {
flagSet := pflag.FlagSet{
Usage: func() { fmt.Print(ServeHelpMessage) },
} }
var configFile string
flagSet.StringVarP(&configFile, "config", "c",
"Cabretfile.yaml",
`path to configuration file`,
)
var verbose bool
flagSet.BoolVarP(&verbose, "verbose", "v",
false,
`verbose output`,
)
var watchedPatterns []string
flagSet.StringArrayVarP(&watchedPatterns, "watch", "w",
[]string{"src/**/"},
`list of paths to watch for rebuild`,
)
var mounts []string
flagSet.StringArrayVarP(&mounts, "mount", "m",
[]string{"public/:/", "out/:/", "dist/:/"},
`list of paths to mount for the static file server`,
)
err := flagSet.Parse(args)
if err != nil {
return err
}
serveMounts := make([]serveMount, len(mounts))
for i, m := range mounts {
path, route, found := strings.Cut(m, ":")
if !found {
if strings.HasPrefix(path, "/") {
return fmt.Errorf(`cannot mount an absolute path without a route: "%s"`, path)
}
route = "/" + path
}
if !strings.HasPrefix(route, "/") {
return fmt.Errorf(`route must start with a slash: "%s"`, route)
}
serveMounts[i] = serveMount{path, route}
}
log.Printf(`configFile = %v`, repr.String(configFile))
log.Printf(`verbose = %v`, repr.String(verbose))
log.Printf(`watchedPatterns = %v`, repr.String(watchedPatterns))
log.Printf(`serveMounts = %v`, repr.String(serveMounts))
return serve(serveConfig{
configFile,
verbose,
watchedPatterns,
serveMounts,
})
} }

@ -0,0 +1,171 @@
package main
import (
"fmt"
"log"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/aziis98/cabret/config"
"github.com/aziis98/cabret/runner"
"github.com/fsnotify/fsnotify"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
)
type serveMount struct {
path, route string
}
type serveConfig struct {
configFile string
verbose bool
watchedPatterns []string
mounts []serveMount
}
const LiveReloadScript = `
const ws = new WebSocket("ws://" + location.host + "/__cabret__/websocket")
ws.addEventListener("message", e => {
console.log("Server: " + e.data)
if (e.data === "reload") {
location.reload()
}
})
ws.addEventListener("close", e => {
console.log("Connection closed")
location.reload()
})
console.log("Live reload enabled")
`
func serve(c serveConfig) error {
cabretFile, err := config.ReadCabretfile(c.configFile)
if err != nil {
return err
}
listeners := map[net.Conn]struct{}{}
// Create new watcher.
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
// Start listening for events.
go func() {
lastRebuild := time.Now()
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if event.Has(fsnotify.Write) {
log.Printf(`[Watcher] event: %s`, event)
if lastRebuild.Add(200 * time.Millisecond).After(time.Now()) {
log.Printf(`[Watcher] too fast, skipping`)
continue
}
// TODO: Make this incremental
// trigger full rebuild
err := runner.RunConfig(cabretFile)
if err != nil {
log.Fatal(err)
return
}
// broadcast refresh
for conn := range listeners {
err := wsutil.WriteServerText(conn, []byte("reload"))
if err != nil {
log.Printf(`[LiveReload] got "%v", removing listener`, err)
delete(listeners, conn)
}
}
lastRebuild = time.Now()
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("error:", err)
}
}
}()
// register watchers
for _, pattern := range c.watchedPatterns {
matches, err := filepath.Glob(pattern)
if err != nil {
return err
}
for _, m := range matches {
log.Printf(`[Watcher] registering "%s"`, m)
if err := watcher.Add(m); err != nil {
return fmt.Errorf(`cannot register pattern "%s": %w`, pattern, err)
}
}
}
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.NoCache)
r.Get("/__cabret__/live-reload.js", func(w http.ResponseWriter, r *http.Request) {
http.ServeContent(w, r, "live-reload.js", time.Time{}, strings.NewReader(LiveReloadScript))
})
r.Get("/__cabret__/websocket", func(w http.ResponseWriter, r *http.Request) {
conn, _, _, err := ws.UpgradeHTTP(r, w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
listeners[conn] = struct{}{}
})
for _, m := range c.mounts {
if strings.HasSuffix(m.route, "/") {
s, _ := os.Stat(m.path)
if s == nil || !s.IsDir() {
continue
}
log.Printf(`[FileServer] mounting directory "%s" on "%s"`, m.path, m.route)
r.Handle(m.route+"*", http.StripPrefix(m.route, http.FileServer(http.Dir(m.path))))
} else {
log.Printf(`[FileServer] mounting file "%s" on "%s"`, m.path, m.route)
r.Get(m.route, func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, m.path)
})
}
}
if err := runner.RunConfig(cabretFile); err != nil {
return err
}
server := &http.Server{Addr: ":5000", Handler: r}
log.Printf(`[FileServer] starting on ":5000"`)
return server.ListenAndServe()
}

@ -6,11 +6,9 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// Operation should have at least one key in "source", "use", "target". The remaining keys are options for that operation
type Operation map[string]any
type Pipeline struct { type Pipeline struct {
Pipeline []Operation `yaml:"pipeline"` // Pipeline is a list of operations, each one should have at least one key in "source", "use", "target". The remaining keys are options for that operation
Pipeline []map[string]any `yaml:"pipeline"`
} }
type BuildOptions struct { type BuildOptions struct {

@ -8,8 +8,15 @@ require (
github.com/bradleyjkemp/cupaloy v2.3.0+incompatible // indirect github.com/bradleyjkemp/cupaloy v2.3.0+incompatible // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.13.0 // indirect github.com/fatih/color v1.13.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-chi/chi/v5 v5.0.8 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.1.0 // indirect
github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-cmp v0.5.9 // indirect
github.com/iancoleman/strcase v0.2.0 // indirect github.com/iancoleman/strcase v0.2.0 // indirect
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/klauspost/compress v1.10.3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-isatty v0.0.16 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
@ -26,4 +33,5 @@ require (
gopkg.in/yaml.v2 v2.3.0 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools v2.2.0+incompatible // indirect gotest.tools v2.2.0+incompatible // indirect
nhooyr.io/websocket v1.8.7 // indirect
) )

@ -4,15 +4,46 @@ github.com/alecthomas/repr v0.1.1 h1:87P60cSmareLAxMc4Hro0r2RBY4ROm0dYwkJNpS4pPs
github.com/alecthomas/repr v0.1.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.1.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/bradleyjkemp/cupaloy v2.3.0+incompatible h1:UafIjBvWQmS9i/xRg+CamMrnLTKNzo+bdmT/oH34c2Y= github.com/bradleyjkemp/cupaloy v2.3.0+incompatible h1:UafIjBvWQmS9i/xRg+CamMrnLTKNzo+bdmT/oH34c2Y=
github.com/bradleyjkemp/cupaloy v2.3.0+incompatible/go.mod h1:Au1Xw1sgaJ5iSFktEhYsS0dbQiS1B0/XMXl+42y9Ilk= github.com/bradleyjkemp/cupaloy v2.3.0+incompatible/go.mod h1:Au1Xw1sgaJ5iSFktEhYsS0dbQiS1B0/XMXl+42y9Ilk=
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -23,6 +54,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -31,6 +64,11 @@ github.com/rakyll/gotest v0.0.6 h1:hBTqkO3jiuwYW/M9gL4bu0oTYcm8J6knQAAPUsJsz1I=
github.com/rakyll/gotest v0.0.6/go.mod h1:SkoesdNCWmiD4R2dljIUcfSnNdVZ12y8qK4ojDkc2Sc= github.com/rakyll/gotest v0.0.6/go.mod h1:SkoesdNCWmiD4R2dljIUcfSnNdVZ12y8qK4ojDkc2Sc=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/yuin/goldmark v1.5.3 h1:3HUJmBFbQW9fhQOzMgseU134xfi6hU+mjWywx5Ty+/M= github.com/yuin/goldmark v1.5.3 h1:3HUJmBFbQW9fhQOzMgseU134xfi6hU+mjWywx5Ty+/M=
github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
@ -43,18 +81,28 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=

@ -53,7 +53,9 @@ func (op *Categorize) Configure(config map[string]any) error {
return nil return nil
} }
func (op *Categorize) ProcessList(contents []cabret.Content) ([]cabret.Content, error) { var _ cabret.ListOperation = &Categorize{}
func (op *Categorize) ProcessList(ctx *cabret.Context, contents []cabret.Content) ([]cabret.Content, error) {
key := strcase.ToCamel(op.Key) key := strcase.ToCamel(op.Key)
categories := map[string][]cabret.Content{} categories := map[string][]cabret.Content{}

@ -30,7 +30,9 @@ func (op *Chunk) Configure(options map[string]any) error {
return nil return nil
} }
func (op *Chunk) ProcessList(items []cabret.Content) ([]cabret.Content, error) { var _ cabret.ListOperation = &Chunk{}
func (op *Chunk) ProcessList(ctx *cabret.Context, items []cabret.Content) ([]cabret.Content, error) {
totalChunks := len(items) / op.Size totalChunks := len(items) / op.Size
chunks := make([][]cabret.Content, totalChunks, totalChunks+1) chunks := make([][]cabret.Content, totalChunks, totalChunks+1)

@ -23,7 +23,9 @@ func (op *Frontmatter) Configure(config map[string]any) error {
return nil return nil
} }
func (op *Frontmatter) ProcessItem(content cabret.Content) (*cabret.Content, error) { var _ cabret.ItemOperation = &Frontmatter{}
func (op *Frontmatter) ProcessItem(ctx *cabret.Context, content cabret.Content) (*cabret.Content, error) {
md := goldmark.New( md := goldmark.New(
goldmark.WithExtensions( goldmark.WithExtensions(
meta.Meta, meta.Meta,

@ -64,7 +64,9 @@ func (op *Layout) Configure(config map[string]any) error {
return fmt.Errorf(`invalid config`) return fmt.Errorf(`invalid config`)
} }
func (op Layout) ProcessItem(content cabret.Content) (*cabret.Content, error) { var _ cabret.ItemOperation = &Layout{}
func (op Layout) ProcessItem(ctx *cabret.Context, content cabret.Content) (*cabret.Content, error) {
// expand glob patterns // expand glob patterns
tmplFiles := []string{} tmplFiles := []string{}
for _, pat := range op.TemplatePatterns { for _, pat := range op.TemplatePatterns {
@ -82,17 +84,17 @@ func (op Layout) ProcessItem(content cabret.Content) (*cabret.Content, error) {
return nil, err return nil, err
} }
ctx := util.CloneMap(content.Metadata) metadata := util.CloneMap(content.Metadata)
if content.Type == HtmlMimeType { if content.Type == HtmlMimeType {
ctx["Content"] = goHtmlTemplate.HTML(content.Data) metadata["Content"] = goHtmlTemplate.HTML(content.Data)
} else { } else {
ctx["Content"] = content.Data metadata["Content"] = content.Data
} }
log.Printf(`[operation.Layout] rendering into layout "%s"`, strings.Join(op.TemplatePatterns, ", ")) log.Printf(`[operation.Layout] rendering into layout "%s"`, strings.Join(op.TemplatePatterns, ", "))
data, err := tmpl.Render(ctx) data, err := tmpl.Render(metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }

@ -24,7 +24,9 @@ func (op *Markdown) Configure(config map[string]any) error {
return nil return nil
} }
func (op Markdown) ProcessItem(content cabret.Content) (*cabret.Content, error) { var _ cabret.ItemOperation = &Markdown{}
func (op Markdown) ProcessItem(ctx *cabret.Context, content cabret.Content) (*cabret.Content, error) {
md := goldmark.New( md := goldmark.New(
goldmark.WithExtensions( goldmark.WithExtensions(
extension.GFM, extension.GFM,

@ -107,7 +107,9 @@ func (op *Program) Configure(options map[string]any) error {
return nil return nil
} }
func (op *Program) ProcessItem(item cabret.Content) (*cabret.Content, error) { var _ cabret.ItemOperation = &Program{}
func (op *Program) ProcessItem(ctx *cabret.Context, item cabret.Content) (*cabret.Content, error) {
ioFmt, ok := ioProgramFormats[op.IOFormat] ioFmt, ok := ioProgramFormats[op.IOFormat]
if !ok { if !ok {
return nil, fmt.Errorf(`unknown io format "%s"`, op.IOFormat) return nil, fmt.Errorf(`unknown io format "%s"`, op.IOFormat)

@ -2,6 +2,7 @@ package operation
import ( import (
"fmt" "fmt"
"log"
"reflect" "reflect"
"github.com/aziis98/cabret" "github.com/aziis98/cabret"
@ -13,6 +14,13 @@ const ShortFormValueKey = "value"
var registry = map[string]reflect.Type{} var registry = map[string]reflect.Type{}
func registerType(name string, op cabret.Operation) { func registerType(name string, op cabret.Operation) {
switch op.(type) {
case cabret.ItemOperation:
case cabret.ListOperation:
default:
log.Fatal(`operation must also satisfy one of cabret.ListOperation or cabret.ItemOperation`)
}
typ := reflect.TypeOf(op).Elem() typ := reflect.TypeOf(op).Elem()
registry[name] = typ registry[name] = typ
} }

@ -36,7 +36,9 @@ func (op *Slice) Configure(options map[string]any) error {
return nil return nil
} }
func (op *Slice) ProcessList(items []cabret.Content) ([]cabret.Content, error) { var _ cabret.ListOperation = &Slice{}
func (op *Slice) ProcessList(ctx *cabret.Context, items []cabret.Content) ([]cabret.Content, error) {
from := op.From from := op.From
to := op.To to := op.To

@ -53,24 +53,21 @@ func (op *Source) Configure(config map[string]any) error {
return fmt.Errorf(`invalid config for "source": %#v`, config) return fmt.Errorf(`invalid config for "source": %#v`, config)
} }
func (op Source) ProcessList(contents []cabret.Content) ([]cabret.Content, error) { var _ cabret.ListOperation = &Source{}
files, err := cabret.FindFiles([]string{})
if err != nil {
return nil, err
}
func (op Source) ProcessList(ctx *cabret.Context, contents []cabret.Content) ([]cabret.Content, error) {
matches := []cabret.MatchResult{} matches := []cabret.MatchResult{}
for _, patternStr := range op.Patterns { for _, rawPattern := range op.Patterns {
pat, err := path.ParsePattern(patternStr) pattern, err := path.ParsePattern(rawPattern)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, f := range files { for _, file := range ctx.Files {
if ok, captures, _ := pat.Match(f); ok { if ok, captures, _ := pattern.Match(file); ok {
matches = append(matches, cabret.MatchResult{ matches = append(matches, cabret.MatchResult{
File: f, File: file,
Captures: captures, Captures: captures,
}) })
} }

@ -36,7 +36,9 @@ func (op *Target) Configure(config map[string]any) error {
return fmt.Errorf(`invalid config for "target": %#v`, config) return fmt.Errorf(`invalid config for "target": %#v`, config)
} }
func (op Target) ProcessItem(c cabret.Content) (*cabret.Content, error) { var _ cabret.ItemOperation = &Target{}
func (op Target) ProcessItem(ctx *cabret.Context, c cabret.Content) (*cabret.Content, error) {
log.Printf(`[operation.Target] expanding pattern "%s"`, op.PathTemplate) log.Printf(`[operation.Target] expanding pattern "%s"`, op.PathTemplate)
target := op.PathTemplate target := op.PathTemplate

@ -32,7 +32,9 @@ func (op *Template) Configure(options map[string]any) error {
return nil return nil
} }
func (op *Template) ProcessList(items []cabret.Content) ([]cabret.Content, error) { var _ cabret.ListOperation = &Template{}
func (op *Template) ProcessList(ctx *cabret.Context, items []cabret.Content) ([]cabret.Content, error) {
var t bytes.Buffer var t bytes.Buffer
// concatenate all templates // concatenate all templates

@ -8,11 +8,12 @@ import (
"github.com/aziis98/cabret/operation" "github.com/aziis98/cabret/operation"
) )
func switchMap(m map[string]any, v *any) func(k string) bool { // switchMapHasKey returns a function that returns true if the map "m" contains the given key and binds the corresponding value to the provided "target" pointer, useful for writing map key checks using a switch instead of an if chain
func switchMapHasKey(m map[string]any, target *any) func(k string) bool {
return func(k string) bool { return func(k string) bool {
val, ok := m[k] val, ok := m[k]
if ok { if ok {
*v = val *target = val
} }
return ok return ok
} }
@ -21,48 +22,48 @@ func switchMap(m map[string]any, v *any) func(k string) bool {
func ParsePipeline(p config.Pipeline) ([]cabret.Operation, error) { func ParsePipeline(p config.Pipeline) ([]cabret.Operation, error) {
ops := []cabret.Operation{} ops := []cabret.Operation{}
for _, opConfig := range p.Pipeline { for _, operationConfig := range p.Pipeline {
var v any var rawValue any
has := switchMap(opConfig, &v) hasKey := switchMapHasKey(operationConfig, &rawValue)
switch { switch {
case has("source"): case hasKey("source"):
value, ok := v.(string) value, ok := rawValue.(string)
if !ok { if !ok {
return nil, fmt.Errorf(`expected string but got "%v" of type %T`, v, v) return nil, fmt.Errorf(`expected string but got "%v" of type %T`, rawValue, rawValue)
} }
opConfig[operation.ShortFormValueKey] = value operationConfig[operation.ShortFormValueKey] = value
op, err := ParseOperation("source", opConfig) op, err := ParseOperation("source", operationConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ops = append(ops, op) ops = append(ops, op)
case has("target"): case hasKey("target"):
value, ok := v.(string) value, ok := rawValue.(string)
if !ok { if !ok {
return nil, fmt.Errorf(`expected string but got "%v" of type %T`, v, v) return nil, fmt.Errorf(`expected string but got "%v" of type %T`, rawValue, rawValue)
} }
opConfig[operation.ShortFormValueKey] = value operationConfig[operation.ShortFormValueKey] = value
op, err := ParseOperation("target", opConfig) op, err := ParseOperation("target", operationConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ops = append(ops, op) ops = append(ops, op)
case has("use"): case hasKey("use"):
name, ok := v.(string) name, ok := rawValue.(string)
if !ok { if !ok {
return nil, fmt.Errorf(`expected string but got "%v" of type %T`, v, v) return nil, fmt.Errorf(`expected string but got "%v" of type %T`, rawValue, rawValue)
} }
op, err := ParseOperation(name, opConfig) op, err := ParseOperation(name, operationConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -70,7 +71,7 @@ func ParsePipeline(p config.Pipeline) ([]cabret.Operation, error) {
ops = append(ops, op) ops = append(ops, op)
default: default:
return nil, fmt.Errorf(`pipeline entry is missing one of "use", "source" or "target", got %#v`, opConfig) return nil, fmt.Errorf(`pipeline entry is missing one of "use", "source" or "target", got %#v`, operationConfig)
} }
} }

@ -16,6 +16,11 @@ type PathPart interface {
regex() string regex() string
} }
var _ PathPart = PathLiteral{}
var _ PathPart = PathShortPattern{}
var _ PathPart = PathLongPattern{}
var _ PathPart = PathString{}
type PathLiteral struct { type PathLiteral struct {
LongAny bool `parser:" @'**'"` LongAny bool `parser:" @'**'"`
ShortAny bool `parser:"| @'*' "` ShortAny bool `parser:"| @'*' "`
@ -25,7 +30,7 @@ type PathLiteral struct {
func (p PathLiteral) regex() string { func (p PathLiteral) regex() string {
switch { switch {
case p.Slash: case p.Slash:
return "/" return regexp.QuoteMeta("/")
case p.ShortAny: case p.ShortAny:
return "([^/]+?)" return "([^/]+?)"
case p.LongAny: case p.LongAny:
@ -52,7 +57,7 @@ func (p PathLongPattern) regex() string {
} }
type PathString struct { type PathString struct {
Value string `parser:"@Ident"` Value string `parser:"@(Ident|Rune)+"`
} }
func (p PathString) regex() string { func (p PathString) regex() string {
@ -63,18 +68,44 @@ type Pattern struct {
Parts []PathPart `parser:"@@+"` Parts []PathPart `parser:"@@+"`
} }
func (pp Pattern) regex() string { var parser = participle.MustBuild[Pattern](
sb := &strings.Builder{} participle.Union[PathPart](
&PathLiteral{},
&PathLongPattern{},
&PathShortPattern{},
&PathString{},
),
participle.Lexer(
lexer.MustSimple([]lexer.SimpleRule{
{Name: "Slash", Pattern: `/`},
{Name: "LongAny", Pattern: `\*\*`},
{Name: "ShortAny", Pattern: `\*`},
{Name: "PatternLongOpen", Pattern: `{{`},
{Name: "PatternLongClose", Pattern: `}}`},
{Name: "PatternShortOpen", Pattern: `{`},
{Name: "PatternShortClose", Pattern: `}`},
{Name: "Ident", Pattern: `[a-zA-Z][a-zA-Z0-9\_]*`},
{Name: "Rune", Pattern: `[^/{}]+`},
}),
),
)
for _, p := range pp.Parts { func ParsePattern(path string) (*Pattern, error) {
sb.WriteString(p.regex()) return parser.ParseString("", path)
}
func MustParsePattern(path string) *Pattern {
p, err := ParsePattern(path)
if err != nil {
panic(err)
} }
return sb.String() return p
} }
func (pp Pattern) Match(s string) (bool, map[string]string, error) { // Match takes a path "s" and matches it against this pattern and returns whether it matched and in that case a map containing all captures for this pattern. This will throw an error if it can't compile the internal regex (should never happen).
r, err := regexp.Compile("^" + pp.regex() + "$") func (p Pattern) Match(s string) (bool, map[string]string, error) {
r, err := regexp.Compile("^" + p.regex() + "$")
if err != nil { if err != nil {
return false, nil, err return false, nil, err
} }
@ -96,26 +127,15 @@ func (pp Pattern) Match(s string) (bool, map[string]string, error) {
return true, ctx, nil return true, ctx, nil
} }
var parser = participle.MustBuild[Pattern]( func (p Pattern) regex() string {
participle.Union[PathPart]( sb := &strings.Builder{}
&PathLiteral{},
&PathLongPattern{}, for _, part := range p.Parts {
&PathShortPattern{}, sb.WriteString(part.regex())
&PathString{}, }
),
participle.Lexer( return sb.String()
lexer.MustSimple([]lexer.SimpleRule{ }
{Name: "Slash", Pattern: `/`},
{Name: "LongAny", Pattern: `\*\*`},
{Name: "ShortAny", Pattern: `\*`},
{Name: "PatternLongOpen", Pattern: `{{`},
{Name: "PatternLongClose", Pattern: `}}`},
{Name: "PatternShortOpen", Pattern: `{`},
{Name: "PatternShortClose", Pattern: `}`},
{Name: "Ident", Pattern: `[^/{}]+`},
}),
),
)
func RenderTemplate(tmpl string, ctx map[string]string) string { func RenderTemplate(tmpl string, ctx map[string]string) string {
s := tmpl s := tmpl
@ -124,16 +144,3 @@ func RenderTemplate(tmpl string, ctx map[string]string) string {
} }
return s return s
} }
func ParsePattern(path string) (*Pattern, error) {
return parser.ParseString("", path)
}
func MustParsePattern(path string) *Pattern {
p, err := ParsePattern(path)
if err != nil {
panic(err)
}
return p
}

@ -2,6 +2,7 @@ package path_test
import ( import (
"testing" "testing"
"unicode"
"github.com/aziis98/cabret/path" "github.com/aziis98/cabret/path"
"gotest.tools/assert" "gotest.tools/assert"
@ -170,3 +171,37 @@ func TestMultipleGroups(t *testing.T) {
}) })
}) })
} }
func FuzzMatch(f *testing.F) {
f.Add(`{a}.txt`)
f.Add(`{a}/{b}.txt`)
f.Add(`src/{{a}}/index.html`)
f.Add(`/a/{b}/c-{d}.go`)
path.MustParsePattern(`{a}.txt`)
path.MustParsePattern(`{a}/{b}.txt`)
path.MustParsePattern(`src/{{a}}/index.html`)
path.MustParsePattern(`/a/{b}/c-{d}.go`)
f.Fuzz(func(t *testing.T, pattern string) {
goodPattern := make([]rune, 0, len(pattern))
// filter in only ascii runes
for _, c := range pattern {
if c <= unicode.MaxASCII {
goodPattern = append(goodPattern, c)
}
}
pattern = string(goodPattern)
p, err := path.ParsePattern(pattern)
if err != nil {
return
}
if _, _, err := p.Match(""); err != nil {
t.Errorf(`invalid regexp generated by pattern %q`, pattern)
}
})
}

@ -9,13 +9,22 @@ import (
) )
func RunConfig(cfg *config.Cabretfile) error { func RunConfig(cfg *config.Cabretfile) error {
files, err := cabret.FindFiles(cfg.Options.Excludes...)
if err != nil {
return err
}
ctx := &cabret.Context{
Files: files,
}
for _, p := range cfg.Build { for _, p := range cfg.Build {
ops, err := parse.ParsePipeline(p) ops, err := parse.ParsePipeline(p)
if err != nil { if err != nil {
return err return err
} }
if _, err := RunPipeline([]cabret.Content{}, ops); err != nil { if _, err := RunPipeline(ops, ctx, []cabret.Content{}); err != nil {
return err return err
} }
} }
@ -23,10 +32,11 @@ func RunConfig(cfg *config.Cabretfile) error {
return nil return nil
} }
func RunPipeline(contents []cabret.Content, ops []cabret.Operation) ([]cabret.Content, error) { // RunPipeline runs the given pipeline "ops" with the context ctx and initial contents "contents"
for _, op := range ops { func RunPipeline(operations []cabret.Operation, ctx *cabret.Context, contents []cabret.Content) ([]cabret.Content, error) {
for _, op := range operations {
var err error var err error
contents, err = RunOperation(op, contents) contents, err = RunOperation(op, ctx, contents)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -35,15 +45,15 @@ func RunPipeline(contents []cabret.Content, ops []cabret.Operation) ([]cabret.Co
return contents, nil return contents, nil
} }
func RunOperation(op cabret.Operation, inputs []cabret.Content) ([]cabret.Content, error) { func RunOperation(op cabret.Operation, ctx *cabret.Context, inputs []cabret.Content) ([]cabret.Content, error) {
switch op := op.(type) { switch op := op.(type) {
case cabret.ListOperation: case cabret.ListOperation:
return op.ProcessList(inputs) return op.ProcessList(ctx, inputs)
case cabret.ItemOperation: case cabret.ItemOperation:
outputs := []cabret.Content{} outputs := []cabret.Content{}
for _, item := range inputs { for _, item := range inputs {
result, err := op.ProcessItem(item) result, err := op.ProcessItem(ctx, item)
if err != nil { if err != nil {
return nil, err return nil, err
} }

@ -8,30 +8,30 @@ import (
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
func FindFiles(excludes []string) ([]string, error) { func FindFiles(excludePatterns ...string) ([]string, error) {
paths := []string{} // TODO: Use [filepath.Glob] instead of [*path.Pattern]
excludeMatchers := []*path.Pattern{} concreteExcludePatterns := []*path.Pattern{}
for _, p := range excludes { for _, rawPattern := range excludePatterns {
m, err := path.ParsePattern(p) pattern, err := path.ParsePattern(rawPattern)
if err != nil { if err != nil {
return nil, err return nil, err
} }
excludeMatchers = append(excludeMatchers, m) concreteExcludePatterns = append(concreteExcludePatterns, pattern)
} }
paths := []string{}
if err := filepath.Walk(".", func(p string, info fs.FileInfo, err error) error { if err := filepath.Walk(".", func(p string, info fs.FileInfo, err error) error {
if info.IsDir() { if info.IsDir() {
return nil return nil
} }
excluded := slices.ContainsFunc(excludeMatchers, func(excludePattern *path.Pattern) bool { if slices.ContainsFunc(concreteExcludePatterns, func(pattern *path.Pattern) bool {
ok, _, _ := excludePattern.Match(p) ok, _, _ := pattern.Match(p)
return ok return ok
}) }) {
if excluded {
return nil return nil
} }

Loading…
Cancel
Save