Minimal working prototype

main
Antonio De Lucreziis 2 years ago
parent 7d22424cf1
commit 38b1017681

@ -0,0 +1,57 @@
# Cabret
A yaml based static site generator, ideally with the same features as Hugo but with a simpler model.
```yaml
entryPoints:
- source: index.html
pipeline:
- layout: layouts/base.html
- target: dist/index.html
- source: posts/{id}.md
pipeline:
- plugin: markdown
- layout: layouts/base.html
- target: dist/posts/{id}/index.html
```
## ToDo
### Tags
A case of fan-in (get all posts and group by tags) and fan-out (generate all tag pages with back-links to posts)
```yaml
entryPoints:
...
- source: posts/{id}.md
pipeline:
- plugin: frontmatter
- plugin: group
metadataKey: tag
key: tags
pipeline:
- layout: layouts/tag.html
- layout: layouts/base.html
- target: dist/tags/{tag}/index.html # ...{tag}... is the same as "metadataKey" (?)
```
### Pagination
A case of fan-out with (various data leakages)
```yaml
entryPoints:
...
- pipeline:
- plugin: paginate
items:
pipeline:
- source: posts/{id}.md
- plugin: frontmatter
pageSize: 10
metadataKey: page
pipeline:
- layout: layouts/list.html
```

@ -0,0 +1,25 @@
package cabret
const MatchResult = "MatchResult"
type Map map[string]any
type File struct {
Path string
Content
}
type Content struct {
// Type for known content formats is just the mime-type
Type string
// Data is the content of the file
Data []byte
// Metadata is any extra data of the file (e.g. yaml frontmatter) or injected by plugins
Metadata Map
}
type Operation interface {
Process(content Content) (*Content, error)
}

@ -3,17 +3,23 @@ package main
import ( import (
"log" "log"
"github.com/alecthomas/repr"
"github.com/aziis98/cabret/config" "github.com/aziis98/cabret/config"
"github.com/aziis98/cabret/exec"
) )
func main() { func main() {
log.SetFlags(0)
log.Printf("Rendering current project") log.Printf("Rendering current project")
site, err := config.ParseCabretfile("./Cabretfile.yaml") site, err := config.ReadCabretfile("./Cabretfile.yaml")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
repr.Println(site) // repr.Println(site)
if err := exec.Execute(site); err != nil {
log.Fatal(err)
}
} }

@ -8,34 +8,38 @@ import (
// Operation is an enum of various operations // Operation is an enum of various operations
type Operation struct { type Operation struct {
Layout string `yaml:"layout,omitempty"` Layout string `yaml:",omitempty"`
Target string `yaml:"target,omitempty"` Target string `yaml:",omitempty"`
Plugin string `yaml:"plugin,omitempty"` Plugin string `yaml:",omitempty"`
Options map[string]any `yaml:",omitempty"`
} }
type EntryPoint struct { type EntryPoint struct {
Source string `yaml:"source"` Source string `yaml:",omitempty"`
Pipeline []Operation `yaml:"pipeline"` Pipeline []Operation `yaml:",omitempty"`
} }
type Config struct { type Options struct {
Output string `yaml:"output,omitempty"` Excludes []string `yaml:",omitempty"`
// Include []string `yaml:",omitempty"`
Output string `yaml:",omitempty"`
} }
// Site has some configuration for the // Cabretfile has some configuration for the
type Site struct { type Cabretfile struct {
Config Config `yaml:"config,omitempty"` Options Options `yaml:",omitempty"`
EntryPoints []EntryPoint `yaml:"entry-points"` EntryPoints []*EntryPoint `yaml:"entryPoints"`
} }
func ParseCabretfile(file string) (*Site, error) { func ReadCabretfile(file string) (*Cabretfile, error) {
f, err := os.Open(file) f, err := os.Open(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
site := new(Site) site := new(Cabretfile)
if err := yaml.NewDecoder(f).Decode(&site); err != nil { if err := yaml.NewDecoder(f).Decode(site); err != nil {
return nil, err return nil, err
} }

@ -1,12 +1,12 @@
site: entryPoints:
entry-points: - source: index.html
- source: index.html pipeline:
pipeline: - layout: layouts/base.html
- layout: layouts/base - target: dist/index.html
- target: dist/index.html - source: posts/{id}.md
- source: 'posts/{post-name}.md' pipeline:
pipeline: - plugin: markdown
- plugin: markdown - layout: layouts/base.html
- layout: layouts/base - target: dist/posts/{id}/index.html
- layout: layouts/post.html
- target: 'dist/posts/{post-name}/index.html'

@ -0,0 +1,5 @@
<h1>My Website</h1>
<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>

@ -4,7 +4,9 @@
<meta charset="UTF-8" /> <meta charset="UTF-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>{{ .Metadata.Title }}</title> <title>{{- if .Title -}}{{ .Title }} | {{ else }}{{ end -}} My Blog</title>
</head> </head>
<body></body> <body>
{{- .Content -}}
</body>
</html> </html>

@ -0,0 +1,116 @@
package exec
import (
"log"
"mime"
"os"
gopath "path"
"github.com/alecthomas/repr"
"github.com/aziis98/cabret"
"github.com/aziis98/cabret/config"
"github.com/aziis98/cabret/operation"
"github.com/aziis98/cabret/path"
)
type matchResult struct {
file string
context map[string]string
}
func BuildOperation(op config.Operation) cabret.Operation {
switch {
case op.Layout != "":
path := op.Layout
return &operation.Layout{
TemplateFilesPattern: path,
Options: op.Options,
}
case op.Target != "":
path := op.Target
return &operation.Target{
PathTemplate: path,
}
case op.Plugin == "markdown":
return &operation.Markdown{
Options: op.Options,
}
default:
log.Fatalf(`invalid operation: %s`, op.Plugin)
}
return nil
}
func Execute(cfg *config.Cabretfile) error {
files, err := cabret.FindFiles([]string{})
if err != nil {
return err
}
// the first index is the entrypoint ID, the second is for the array of matched files for this entrypoint
entryPointMatches := make([][]matchResult, len(cfg.EntryPoints))
// load all files to process
for id, ep := range cfg.EntryPoints {
pat, err := path.ParsePattern(ep.Source)
if err != nil {
return err
}
matchedFiles := []matchResult{}
for _, f := range files {
if ok, ctx, _ := pat.Match(f); ok {
log.Printf(`[Preload] [EntryPoint %d] Found "%s": %#v`, id, f, ctx)
matchedFiles = append(matchedFiles, matchResult{f, ctx})
}
}
entryPointMatches[id] = matchedFiles
}
// TODO: preload all metadata...
// process all entrypoints
for id, ep := range cfg.EntryPoints {
log.Printf(`[EntryPoint %d] starting to process %d file(s)`, id, len(entryPointMatches[id]))
for _, m := range entryPointMatches[id] {
log.Printf(`[EntryPoint %d] ["%s"] reading file`, id, m.file)
data, err := os.ReadFile(m.file)
if err != nil {
return err
}
content := cabret.Content{
Type: mime.TypeByExtension(gopath.Ext(m.file)),
Data: data,
Metadata: cabret.Map{
cabret.MatchResult: m.context,
},
}
for i, opConfig := range ep.Pipeline {
op := BuildOperation(opConfig)
log.Printf(`[EntryPoint %d] ["%s"] [Operation(%d)] applying %s`, id, m.file, i, repr.String(op))
newContent, err := op.Process(content)
if err != nil {
return err
}
if newContent == nil {
break
}
// log.Printf(`[EntryPoint %d] ["%s"] [Operation(%d)] [Metadata] %s`, id, m.file, i, repr.String(newContent.Metadata, repr.Indent(" ")))
content = *newContent
}
log.Printf(`[EntryPoint %d] ["%s"] done`, id, m.file)
}
}
return nil
}

@ -5,9 +5,19 @@ go 1.19
require ( require (
github.com/alecthomas/participle/v2 v2.0.0-beta.5 // indirect github.com/alecthomas/participle/v2 v2.0.0-beta.5 // indirect
github.com/alecthomas/repr v0.1.1 // indirect github.com/alecthomas/repr v0.1.1 // indirect
github.com/fatih/color v1.13.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/mattn/go-colorable v0.1.13 // 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
github.com/rakyll/gotest v0.0.6 // indirect
github.com/yuin/goldmark v1.5.3 // indirect github.com/yuin/goldmark v1.5.3 // indirect
github.com/yuin/goldmark-meta v1.1.0 // indirect
golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/text v0.5.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
) )

@ -2,13 +2,46 @@ github.com/alecthomas/participle/v2 v2.0.0-beta.5 h1:y6dsSYVb1G5eK6mgmy+BgI3Mw35
github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0qdokritYSNR3wV5cVwmIEaMM= github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0qdokritYSNR3wV5cVwmIEaMM=
github.com/alecthomas/repr v0.1.1 h1:87P60cSmareLAxMc4Hro0r2RBY4ROm0dYwkJNpS4pPs= 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/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/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/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
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=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
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/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/rakyll/gotest v0.0.6 h1:hBTqkO3jiuwYW/M9gL4bu0oTYcm8J6knQAAPUsJsz1I=
github.com/rakyll/gotest v0.0.6/go.mod h1:SkoesdNCWmiD4R2dljIUcfSnNdVZ12y8qK4ojDkc2Sc=
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/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15 h1:5oN1Pz/eDhCpbMbLstvIPa0b/BEQo6g6nwV3pLjfM6w=
golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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-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.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
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.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 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=

@ -0,0 +1,73 @@
package operation
import (
"html/template"
"log"
"mime"
"path/filepath"
"strings"
gopath "path"
"github.com/alecthomas/repr"
"github.com/aziis98/cabret"
"github.com/aziis98/cabret/operation/layout"
"github.com/aziis98/cabret/util"
)
var HtmlMimeType = mime.TypeByExtension(".html")
var _ cabret.Operation = Layout{}
type Layout struct {
// TemplateFilesPattern is a comma separated list of unix glob patterns
TemplateFilesPattern string
Options map[string]any
}
func (op Layout) Process(content cabret.Content) (*cabret.Content, error) {
var tmpl layout.Template
patterns := strings.Split(op.TemplateFilesPattern, ",")
tmplFiles := []string{}
for _, pat := range patterns {
files, err := filepath.Glob(strings.TrimSpace(pat))
if err != nil {
return nil, err
}
tmplFiles = append(tmplFiles, files...)
}
log.Printf(`[Layout] template pattern "%s" expanded to %s`, op.TemplateFilesPattern, repr.String(tmplFiles))
if gopath.Ext(tmplFiles[0]) == ".html" {
var err error
if tmpl, err = layout.NewHtmlTemplate(tmplFiles...); err != nil {
return nil, err
}
} else {
var err error
if tmpl, err = layout.NewTextTemplate(tmplFiles...); err != nil {
return nil, err
}
}
ctx := util.CloneMap(content.Metadata)
if content.Type == HtmlMimeType {
ctx["Content"] = template.HTML(content.Data)
} else {
ctx["Content"] = content.Data
}
data, err := tmpl.Render(ctx)
if err != nil {
return nil, err
}
content.Data = data
return &content, nil
}

@ -0,0 +1,31 @@
package layout
import (
"bytes"
"html/template"
)
var _ Template = HtmlTemplate{}
type HtmlTemplate struct {
*template.Template
}
func NewHtmlTemplate(files ...string) (*HtmlTemplate, error) {
t, err := template.ParseFiles(files...)
if err != nil {
return nil, err
}
return &HtmlTemplate{t}, nil
}
func (t HtmlTemplate) Render(ctx map[string]any) ([]byte, error) {
var b bytes.Buffer
if err := t.Template.Execute(&b, ctx); err != nil {
return nil, err
}
return b.Bytes(), nil
}

@ -0,0 +1,5 @@
package layout
type Template interface {
Render(ctx map[string]any) ([]byte, error)
}

@ -0,0 +1,31 @@
package layout
import (
"bytes"
"text/template"
)
var _ Template = TextTemplate{}
type TextTemplate struct {
*template.Template
}
func NewTextTemplate(files ...string) (*TextTemplate, error) {
t, err := template.ParseFiles(files...)
if err != nil {
return nil, err
}
return &TextTemplate{t}, nil
}
func (t TextTemplate) Render(ctx map[string]any) ([]byte, error) {
var b bytes.Buffer
if err := t.Template.Execute(&b, ctx); err != nil {
return nil, err
}
return b.Bytes(), nil
}

@ -0,0 +1,46 @@
package operation
import (
"bytes"
"github.com/aziis98/cabret"
"github.com/iancoleman/strcase"
"github.com/yuin/goldmark"
meta "github.com/yuin/goldmark-meta"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
)
type Markdown struct {
Options map[string]any
}
func (op Markdown) Process(content cabret.Content) (*cabret.Content, error) {
md := goldmark.New(
goldmark.WithExtensions(
extension.GFM,
meta.Meta,
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
)
var buf bytes.Buffer
context := parser.NewContext()
if err := md.Convert(content.Data, &buf, parser.WithContext(context)); err != nil {
panic(err)
}
frontmatter := meta.Get(context)
for k, v := range frontmatter {
content.Metadata[strcase.ToCamel(k)] = v
}
return &cabret.Content{
Type: HtmlMimeType,
Data: buf.Bytes(),
Metadata: content.Metadata,
}, nil
}

@ -0,0 +1,36 @@
package operation
import (
"fmt"
"os"
gopath "path"
"github.com/aziis98/cabret"
"github.com/aziis98/cabret/path"
)
var _ cabret.Operation = Target{}
type Target struct {
PathTemplate string
}
func (op Target) Process(c cabret.Content) (*cabret.Content, error) {
mr, ok := c.Metadata[cabret.MatchResult].(map[string]string)
if !ok {
return nil, fmt.Errorf(`invalid match result type %T`, c.Metadata[cabret.MatchResult])
}
target := path.RenderTemplate(op.PathTemplate, mr)
if err := os.MkdirAll(gopath.Dir(target), 0777); err != nil {
return nil, err
}
if err := os.WriteFile(target, c.Data, 0666); err != nil {
return nil, err
}
c.Metadata["Target"] = op.PathTemplate
return &c, nil
}

@ -9,20 +9,34 @@ import (
"github.com/alecthomas/participle/v2/lexer" "github.com/alecthomas/participle/v2/lexer"
) )
// FullMatch represents the complete match in a match result context map
const FullMatch = "*"
type PathPart interface { type PathPart interface {
regex() string regex() string
} }
type PathSlash struct { type PathLiteral struct {
Value string `"/"` LongAny bool `parser:" @'**'"`
ShortAny bool `parser:"| @'*' "`
Slash bool `parser:"| @'/' "`
} }
func (p PathSlash) regex() string { func (p PathLiteral) regex() string {
return "/" switch {
case p.Slash:
return "/"
case p.ShortAny:
return "([^/]+?)"
case p.LongAny:
return "(.+?)"
default:
panic("illegal enum state")
}
} }
type PathShortPattern struct { type PathShortPattern struct {
Name string `"{" @Ident "}"` Name string `parser:"'{' @Ident '}'"`
} }
func (p PathShortPattern) regex() string { func (p PathShortPattern) regex() string {
@ -30,7 +44,7 @@ func (p PathShortPattern) regex() string {
} }
type PathLongPattern struct { type PathLongPattern struct {
Name string `"{{" @Ident "}}"` Name string `parser:"'{{' @Ident '}}'"`
} }
func (p PathLongPattern) regex() string { func (p PathLongPattern) regex() string {
@ -38,18 +52,18 @@ func (p PathLongPattern) regex() string {
} }
type PathString struct { type PathString struct {
Value string `@Ident` Value string `parser:"@Ident"`
} }
func (p PathString) regex() string { func (p PathString) regex() string {
return regexp.QuoteMeta(p.Value) return regexp.QuoteMeta(p.Value)
} }
type PathPattern struct { type Pattern struct {
Parts []PathPart `@@+` Parts []PathPart `parser:"@@+"`
} }
func (pp PathPattern) regex() string { func (pp Pattern) regex() string {
sb := &strings.Builder{} sb := &strings.Builder{}
for _, p := range pp.Parts { for _, p := range pp.Parts {
@ -59,7 +73,7 @@ func (pp PathPattern) regex() string {
return sb.String() return sb.String()
} }
func (pp PathPattern) Match(s string) (bool, map[string]string, error) { func (pp Pattern) Match(s string) (bool, map[string]string, error) {
r, err := regexp.Compile("^" + pp.regex() + "$") r, err := regexp.Compile("^" + pp.regex() + "$")
if err != nil { if err != nil {
return false, nil, err return false, nil, err
@ -72,15 +86,19 @@ func (pp PathPattern) Match(s string) (bool, map[string]string, error) {
ctx := map[string]string{} ctx := map[string]string{}
for i, name := range r.SubexpNames() { for i, name := range r.SubexpNames() {
ctx[name] = ms[i] if name != "" {
ctx[name] = ms[i]
}
} }
ctx[FullMatch] = ms[0]
return true, ctx, nil return true, ctx, nil
} }
var parser = participle.MustBuild[PathPattern]( var parser = participle.MustBuild[Pattern](
participle.Union[PathPart]( participle.Union[PathPart](
&PathSlash{}, &PathLiteral{},
&PathLongPattern{}, &PathLongPattern{},
&PathShortPattern{}, &PathShortPattern{},
&PathString{}, &PathString{},
@ -88,6 +106,8 @@ var parser = participle.MustBuild[PathPattern](
participle.Lexer( participle.Lexer(
lexer.MustSimple([]lexer.SimpleRule{ lexer.MustSimple([]lexer.SimpleRule{
{Name: "Slash", Pattern: `/`}, {Name: "Slash", Pattern: `/`},
{Name: "LongAny", Pattern: `\*\*`},
{Name: "ShortAny", Pattern: `\*`},
{Name: "PatternLongOpen", Pattern: `{{`}, {Name: "PatternLongOpen", Pattern: `{{`},
{Name: "PatternLongClose", Pattern: `}}`}, {Name: "PatternLongClose", Pattern: `}}`},
{Name: "PatternShortOpen", Pattern: `{`}, {Name: "PatternShortOpen", Pattern: `{`},
@ -97,11 +117,19 @@ var parser = participle.MustBuild[PathPattern](
), ),
) )
func ParsePattern(path string) (*PathPattern, error) { func RenderTemplate(tmpl string, ctx map[string]string) string {
s := tmpl
for k, v := range ctx {
s = strings.ReplaceAll(s, "{"+k+"}", v)
}
return s
}
func ParsePattern(path string) (*Pattern, error) {
return parser.ParseString("", path) return parser.ParseString("", path)
} }
func MustParsePattern(path string) *PathPattern { func MustParsePattern(path string) *Pattern {
p, err := ParsePattern(path) p, err := ParsePattern(path)
if err != nil { if err != nil {
panic(err) panic(err)

@ -7,53 +7,124 @@ import (
"gotest.tools/assert" "gotest.tools/assert"
) )
func Test1(t *testing.T) { func TestPath(t *testing.T) {
p, err := path.ParsePattern("/foo/bar") p, err := path.ParsePattern("/foo/bar")
assert.NilError(t, err) assert.NilError(t, err)
assert.DeepEqual(t, p, &path.PathPattern{ assert.DeepEqual(t, p, &path.Pattern{
Parts: []path.PathPart{ Parts: []path.PathPart{
&path.PathSlash{}, &path.PathString{"foo"}, &path.PathSlash{}, &path.PathString{"bar"}, &path.PathLiteral{Slash: true},
&path.PathString{"foo"},
&path.PathLiteral{Slash: true},
&path.PathString{"bar"},
}, },
}) })
} }
func Test2(t *testing.T) { func TestSimplePattern(t *testing.T) {
p, err := path.ParsePattern("posts/{name}.md") p, err := path.ParsePattern("posts/{name}.md")
assert.NilError(t, err) assert.NilError(t, err)
assert.DeepEqual(t, p, &path.PathPattern{ assert.DeepEqual(t, p, &path.Pattern{
Parts: []path.PathPart{ Parts: []path.PathPart{
&path.PathString{"posts"}, &path.PathSlash{}, &path.PathShortPattern{"name"}, &path.PathString{".md"}, &path.PathString{"posts"},
&path.PathLiteral{Slash: true},
&path.PathShortPattern{"name"},
&path.PathString{".md"},
}, },
}) })
} }
func Test3(t *testing.T) { func TestSimplePatternWithAny(t *testing.T) {
p, err := path.ParsePattern("posts/*/{name}.md")
assert.NilError(t, err)
assert.DeepEqual(t, p, &path.Pattern{
Parts: []path.PathPart{
&path.PathString{"posts"},
&path.PathLiteral{Slash: true},
&path.PathLiteral{ShortAny: true},
&path.PathLiteral{Slash: true},
&path.PathShortPattern{"name"},
&path.PathString{".md"},
},
})
t.Run("MatchGood", func(t *testing.T) {
m, ctx, err := p.Match("posts/2022/post-1.md")
assert.NilError(t, err, nil)
assert.Equal(t, m, true)
assert.DeepEqual(t, ctx, map[string]string{
path.FullMatch: "posts/2022/post-1.md",
"name": "post-1",
})
})
t.Run("MatchBad", func(t *testing.T) {
m, ctx, err := p.Match("posts/2022/12/post-1.md")
assert.NilError(t, err)
assert.Assert(t, m == false)
assert.Assert(t, ctx == nil)
})
}
func TestSimplePatternCombo(t *testing.T) {
p, err := path.ParsePattern("posts/{{path}}/{name}.md") p, err := path.ParsePattern("posts/{{path}}/{name}.md")
assert.NilError(t, err) assert.NilError(t, err)
assert.DeepEqual(t, p, &path.PathPattern{ assert.DeepEqual(t, p, &path.Pattern{
Parts: []path.PathPart{ Parts: []path.PathPart{
&path.PathString{"posts"}, &path.PathString{"posts"},
&path.PathSlash{}, &path.PathLiteral{Slash: true},
&path.PathLongPattern{"path"}, &path.PathLongPattern{"path"},
&path.PathSlash{}, &path.PathLiteral{Slash: true},
&path.PathShortPattern{"name"}, &path.PathShortPattern{"name"},
&path.PathString{".md"}, &path.PathString{".md"},
}, },
}) })
} }
func Test4(t *testing.T) { func TestPatternCombo(t *testing.T) {
p, err := path.ParsePattern("posts/**/{date}_{slug}.md")
assert.NilError(t, err)
assert.DeepEqual(t, p, &path.Pattern{
Parts: []path.PathPart{
&path.PathString{"posts"},
&path.PathLiteral{Slash: true},
&path.PathLiteral{LongAny: true},
&path.PathLiteral{Slash: true},
&path.PathShortPattern{"date"},
&path.PathString{"_"},
&path.PathShortPattern{"slug"},
&path.PathString{".md"},
},
})
t.Run("MatchGood", func(t *testing.T) {
ok, ctx, err := p.Match("posts/a/b/c/2022-12-25_example.md")
assert.Assert(t, ok)
assert.NilError(t, err)
assert.DeepEqual(t, ctx, map[string]string{
path.FullMatch: "posts/a/b/c/2022-12-25_example.md",
"date": "2022-12-25",
"slug": "example",
})
})
}
func TestStrangePatternCombo(t *testing.T) {
p, err := path.ParsePattern("foo{{path}}/{name}.md") p, err := path.ParsePattern("foo{{path}}/{name}.md")
assert.NilError(t, err) assert.NilError(t, err)
assert.DeepEqual(t, p, &path.PathPattern{ assert.DeepEqual(t, p, &path.Pattern{
Parts: []path.PathPart{ Parts: []path.PathPart{
&path.PathString{"foo"}, &path.PathString{"foo"},
&path.PathLongPattern{"path"}, &path.PathLongPattern{"path"},
&path.PathSlash{}, &path.PathLiteral{Slash: true},
&path.PathShortPattern{"name"}, &path.PathShortPattern{"name"},
&path.PathString{".md"}, &path.PathString{".md"},
}, },
@ -65,9 +136,37 @@ func Test4(t *testing.T) {
assert.NilError(t, err, nil) assert.NilError(t, err, nil)
assert.Equal(t, m, true) assert.Equal(t, m, true)
assert.DeepEqual(t, ctx, map[string]string{ assert.DeepEqual(t, ctx, map[string]string{
"": "foo-1/bar/baz/post-1.md", path.FullMatch: "foo-1/bar/baz/post-1.md",
"name": "post-1", "name": "post-1",
"path": "-1/bar/baz", "path": "-1/bar/baz",
})
})
}
func TestMultipleGroups(t *testing.T) {
p, err := path.ParsePattern("{a}/{b}/{c}")
assert.NilError(t, err)
assert.DeepEqual(t, p, &path.Pattern{
Parts: []path.PathPart{
&path.PathShortPattern{"a"},
&path.PathLiteral{Slash: true},
&path.PathShortPattern{"b"},
&path.PathLiteral{Slash: true},
&path.PathShortPattern{"c"},
},
})
t.Run("match", func(t *testing.T) {
m, ctx, err := p.Match("foo/bar/baz")
assert.NilError(t, err, nil)
assert.Equal(t, m, true)
assert.DeepEqual(t, ctx, map[string]string{
path.FullMatch: "foo/bar/baz",
"a": "foo",
"b": "bar",
"c": "baz",
}) })
}) })
} }

@ -0,0 +1,9 @@
package util
func CloneMap[K comparable, V any](m1 map[K]V) map[K]V {
m2 := map[K]V{}
for k, v := range m1 {
m2[k] = v
}
return m2
}

@ -0,0 +1,45 @@
package cabret
import (
"io/fs"
"path/filepath"
"github.com/aziis98/cabret/path"
"golang.org/x/exp/slices"
)
func FindFiles(excludes []string) ([]string, error) {
paths := []string{}
excludeMatchers := []*path.Pattern{}
for _, p := range excludes {
m, err := path.ParsePattern(p)
if err != nil {
return nil, err
}
excludeMatchers = append(excludeMatchers, m)
}
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)
return ok
})
if excluded {
return nil
}
paths = append(paths, p)
return nil
}); err != nil {
return nil, err
}
return paths, nil
}
Loading…
Cancel
Save