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_
## Build
## Build & Install
```bash shell
$ go build -v -o ./bin/cabret ./cmd/cabret
$ go install -v ./cmd/cabret
```
## Introduction

@ -4,3 +4,7 @@
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>
<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 name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{- if .Title -}}{{ .Title }} | {{ else }}{{ end -}} My Blog</title>
<script type="module" src="/__cabret__/live-reload.js" async></script>
</head>
<body>
{{ .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?

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

@ -0,0 +1 @@
package main

@ -1,25 +1,185 @@
package main
import (
"fmt"
"log"
"os"
"strings"
"github.com/alecthomas/repr"
"github.com/aziis98/cabret/config"
"github.com/aziis98/cabret/runner"
"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() {
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`)
pflag.Parse()
-c, --config <path> Path to configuration file (default is "Cabretfile.yaml")
-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 {
log.Fatal(err)
return err
}
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"
)
// 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 {
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 {

@ -8,8 +8,15 @@ require (
github.com/bradleyjkemp/cupaloy v2.3.0+incompatible // indirect
github.com/davecgh/go-spew v1.1.1 // 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/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-isatty v0.0.16 // 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.v3 v3.0.1 // 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/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/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/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/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/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/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.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
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.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
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/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
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-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-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-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/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/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/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/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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
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/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
}
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)
categories := map[string][]cabret.Content{}

@ -30,7 +30,9 @@ func (op *Chunk) Configure(options map[string]any) error {
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
chunks := make([][]cabret.Content, totalChunks, totalChunks+1)

@ -23,7 +23,9 @@ func (op *Frontmatter) Configure(config map[string]any) error {
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(
goldmark.WithExtensions(
meta.Meta,

@ -64,7 +64,9 @@ func (op *Layout) Configure(config map[string]any) error {
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
tmplFiles := []string{}
for _, pat := range op.TemplatePatterns {
@ -82,17 +84,17 @@ func (op Layout) ProcessItem(content cabret.Content) (*cabret.Content, error) {
return nil, err
}
ctx := util.CloneMap(content.Metadata)
metadata := util.CloneMap(content.Metadata)
if content.Type == HtmlMimeType {
ctx["Content"] = goHtmlTemplate.HTML(content.Data)
metadata["Content"] = goHtmlTemplate.HTML(content.Data)
} else {
ctx["Content"] = content.Data
metadata["Content"] = content.Data
}
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 {
return nil, err
}

@ -24,7 +24,9 @@ func (op *Markdown) Configure(config map[string]any) error {
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(
goldmark.WithExtensions(
extension.GFM,

@ -107,7 +107,9 @@ func (op *Program) Configure(options map[string]any) error {
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]
if !ok {
return nil, fmt.Errorf(`unknown io format "%s"`, op.IOFormat)

@ -2,6 +2,7 @@ package operation
import (
"fmt"
"log"
"reflect"
"github.com/aziis98/cabret"
@ -13,6 +14,13 @@ const ShortFormValueKey = "value"
var registry = map[string]reflect.Type{}
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()
registry[name] = typ
}

@ -36,7 +36,9 @@ func (op *Slice) Configure(options map[string]any) error {
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
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)
}
func (op Source) ProcessList(contents []cabret.Content) ([]cabret.Content, error) {
files, err := cabret.FindFiles([]string{})
if err != nil {
return nil, err
}
var _ cabret.ListOperation = &Source{}
func (op Source) ProcessList(ctx *cabret.Context, contents []cabret.Content) ([]cabret.Content, error) {
matches := []cabret.MatchResult{}
for _, patternStr := range op.Patterns {
pat, err := path.ParsePattern(patternStr)
for _, rawPattern := range op.Patterns {
pattern, err := path.ParsePattern(rawPattern)
if err != nil {
return nil, err
}
for _, f := range files {
if ok, captures, _ := pat.Match(f); ok {
for _, file := range ctx.Files {
if ok, captures, _ := pattern.Match(file); ok {
matches = append(matches, cabret.MatchResult{
File: f,
File: file,
Captures: captures,
})
}

@ -36,7 +36,9 @@ func (op *Target) Configure(config map[string]any) error {
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)
target := op.PathTemplate

@ -32,7 +32,9 @@ func (op *Template) Configure(options map[string]any) error {
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
// concatenate all templates

@ -8,11 +8,12 @@ import (
"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 {
val, ok := m[k]
if ok {
*v = val
*target = val
}
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) {
ops := []cabret.Operation{}
for _, opConfig := range p.Pipeline {
var v any
has := switchMap(opConfig, &v)
for _, operationConfig := range p.Pipeline {
var rawValue any
hasKey := switchMapHasKey(operationConfig, &rawValue)
switch {
case has("source"):
value, ok := v.(string)
case hasKey("source"):
value, ok := rawValue.(string)
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 {
return nil, err
}
ops = append(ops, op)
case has("target"):
value, ok := v.(string)
case hasKey("target"):
value, ok := rawValue.(string)
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 {
return nil, err
}
ops = append(ops, op)
case has("use"):
name, ok := v.(string)
case hasKey("use"):
name, ok := rawValue.(string)
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 {
return nil, err
}
@ -70,7 +71,7 @@ func ParsePipeline(p config.Pipeline) ([]cabret.Operation, error) {
ops = append(ops, op)
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
}
var _ PathPart = PathLiteral{}
var _ PathPart = PathShortPattern{}
var _ PathPart = PathLongPattern{}
var _ PathPart = PathString{}
type PathLiteral struct {
LongAny bool `parser:" @'**'"`
ShortAny bool `parser:"| @'*' "`
@ -25,7 +30,7 @@ type PathLiteral struct {
func (p PathLiteral) regex() string {
switch {
case p.Slash:
return "/"
return regexp.QuoteMeta("/")
case p.ShortAny:
return "([^/]+?)"
case p.LongAny:
@ -52,7 +57,7 @@ func (p PathLongPattern) regex() string {
}
type PathString struct {
Value string `parser:"@Ident"`
Value string `parser:"@(Ident|Rune)+"`
}
func (p PathString) regex() string {
@ -63,18 +68,44 @@ type Pattern struct {
Parts []PathPart `parser:"@@+"`
}
func (pp Pattern) regex() string {
sb := &strings.Builder{}
var parser = participle.MustBuild[Pattern](
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 {
sb.WriteString(p.regex())
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 sb.String()
return p
}
func (pp Pattern) Match(s string) (bool, map[string]string, error) {
r, err := regexp.Compile("^" + pp.regex() + "$")
// 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).
func (p Pattern) Match(s string) (bool, map[string]string, error) {
r, err := regexp.Compile("^" + p.regex() + "$")
if err != nil {
return false, nil, err
}
@ -96,26 +127,15 @@ func (pp Pattern) Match(s string) (bool, map[string]string, error) {
return true, ctx, nil
}
var parser = participle.MustBuild[Pattern](
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: `[^/{}]+`},
}),
),
)
func (p Pattern) regex() string {
sb := &strings.Builder{}
for _, part := range p.Parts {
sb.WriteString(part.regex())
}
return sb.String()
}
func RenderTemplate(tmpl string, ctx map[string]string) string {
s := tmpl
@ -124,16 +144,3 @@ func RenderTemplate(tmpl string, ctx map[string]string) string {
}
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 (
"testing"
"unicode"
"github.com/aziis98/cabret/path"
"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 {
files, err := cabret.FindFiles(cfg.Options.Excludes...)
if err != nil {
return err
}
ctx := &cabret.Context{
Files: files,
}
for _, p := range cfg.Build {
ops, err := parse.ParsePipeline(p)
if err != nil {
return err
}
if _, err := RunPipeline([]cabret.Content{}, ops); err != nil {
if _, err := RunPipeline(ops, ctx, []cabret.Content{}); err != nil {
return err
}
}
@ -23,10 +32,11 @@ func RunConfig(cfg *config.Cabretfile) error {
return nil
}
func RunPipeline(contents []cabret.Content, ops []cabret.Operation) ([]cabret.Content, error) {
for _, op := range ops {
// RunPipeline runs the given pipeline "ops" with the context ctx and initial contents "contents"
func RunPipeline(operations []cabret.Operation, ctx *cabret.Context, contents []cabret.Content) ([]cabret.Content, error) {
for _, op := range operations {
var err error
contents, err = RunOperation(op, contents)
contents, err = RunOperation(op, ctx, contents)
if err != nil {
return nil, err
}
@ -35,15 +45,15 @@ func RunPipeline(contents []cabret.Content, ops []cabret.Operation) ([]cabret.Co
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) {
case cabret.ListOperation:
return op.ProcessList(inputs)
return op.ProcessList(ctx, inputs)
case cabret.ItemOperation:
outputs := []cabret.Content{}
for _, item := range inputs {
result, err := op.ProcessItem(item)
result, err := op.ProcessItem(ctx, item)
if err != nil {
return nil, err
}

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

Loading…
Cancel
Save