Minimal working prototype
This commit is contained in:
@@ -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)
|
||||
}
|
||||
+9
-3
@@ -3,17 +3,23 @@ package main
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/alecthomas/repr"
|
||||
"github.com/aziis98/cabret/config"
|
||||
"github.com/aziis98/cabret/exec"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.Printf("Rendering current project")
|
||||
|
||||
site, err := config.ParseCabretfile("./Cabretfile.yaml")
|
||||
site, err := config.ReadCabretfile("./Cabretfile.yaml")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
repr.Println(site)
|
||||
// repr.Println(site)
|
||||
|
||||
if err := exec.Execute(site); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+18
-14
@@ -8,34 +8,38 @@ import (
|
||||
|
||||
// Operation is an enum of various operations
|
||||
type Operation struct {
|
||||
Layout string `yaml:"layout,omitempty"`
|
||||
Target string `yaml:"target,omitempty"`
|
||||
Plugin string `yaml:"plugin,omitempty"`
|
||||
Layout string `yaml:",omitempty"`
|
||||
Target string `yaml:",omitempty"`
|
||||
Plugin string `yaml:",omitempty"`
|
||||
|
||||
Options map[string]any `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
type EntryPoint struct {
|
||||
Source string `yaml:"source"`
|
||||
Pipeline []Operation `yaml:"pipeline"`
|
||||
Source string `yaml:",omitempty"`
|
||||
Pipeline []Operation `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Output string `yaml:"output,omitempty"`
|
||||
type Options struct {
|
||||
Excludes []string `yaml:",omitempty"`
|
||||
// Include []string `yaml:",omitempty"`
|
||||
Output string `yaml:",omitempty"`
|
||||
}
|
||||
|
||||
// Site has some configuration for the
|
||||
type Site struct {
|
||||
Config Config `yaml:"config,omitempty"`
|
||||
EntryPoints []EntryPoint `yaml:"entry-points"`
|
||||
// Cabretfile has some configuration for the
|
||||
type Cabretfile struct {
|
||||
Options Options `yaml:",omitempty"`
|
||||
EntryPoints []*EntryPoint `yaml:"entryPoints"`
|
||||
}
|
||||
|
||||
func ParseCabretfile(file string) (*Site, error) {
|
||||
func ReadCabretfile(file string) (*Cabretfile, error) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
site := new(Site)
|
||||
if err := yaml.NewDecoder(f).Decode(&site); err != nil {
|
||||
site := new(Cabretfile)
|
||||
if err := yaml.NewDecoder(f).Decode(site); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
site:
|
||||
entry-points:
|
||||
- source: index.html
|
||||
pipeline:
|
||||
- layout: layouts/base
|
||||
- target: dist/index.html
|
||||
- source: 'posts/{post-name}.md'
|
||||
pipeline:
|
||||
- plugin: markdown
|
||||
- layout: layouts/base
|
||||
- layout: layouts/post.html
|
||||
- target: 'dist/posts/{post-name}/index.html'
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -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 http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<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>
|
||||
<body></body>
|
||||
<body>
|
||||
{{- .Content -}}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+116
@@ -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 (
|
||||
github.com/alecthomas/participle/v2 v2.0.0-beta.5 // 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/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/rakyll/gotest v0.0.6 // 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
|
||||
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/repr v0.1.1 h1:87P60cSmareLAxMc4Hro0r2RBY4ROm0dYwkJNpS4pPs=
|
||||
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/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/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/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/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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+44
-16
@@ -9,20 +9,34 @@ import (
|
||||
"github.com/alecthomas/participle/v2/lexer"
|
||||
)
|
||||
|
||||
// FullMatch represents the complete match in a match result context map
|
||||
const FullMatch = "*"
|
||||
|
||||
type PathPart interface {
|
||||
regex() string
|
||||
}
|
||||
|
||||
type PathSlash struct {
|
||||
Value string `"/"`
|
||||
type PathLiteral struct {
|
||||
LongAny bool `parser:" @'**'"`
|
||||
ShortAny bool `parser:"| @'*' "`
|
||||
Slash bool `parser:"| @'/' "`
|
||||
}
|
||||
|
||||
func (p PathSlash) regex() string {
|
||||
return "/"
|
||||
func (p PathLiteral) regex() string {
|
||||
switch {
|
||||
case p.Slash:
|
||||
return "/"
|
||||
case p.ShortAny:
|
||||
return "([^/]+?)"
|
||||
case p.LongAny:
|
||||
return "(.+?)"
|
||||
default:
|
||||
panic("illegal enum state")
|
||||
}
|
||||
}
|
||||
|
||||
type PathShortPattern struct {
|
||||
Name string `"{" @Ident "}"`
|
||||
Name string `parser:"'{' @Ident '}'"`
|
||||
}
|
||||
|
||||
func (p PathShortPattern) regex() string {
|
||||
@@ -30,7 +44,7 @@ func (p PathShortPattern) regex() string {
|
||||
}
|
||||
|
||||
type PathLongPattern struct {
|
||||
Name string `"{{" @Ident "}}"`
|
||||
Name string `parser:"'{{' @Ident '}}'"`
|
||||
}
|
||||
|
||||
func (p PathLongPattern) regex() string {
|
||||
@@ -38,18 +52,18 @@ func (p PathLongPattern) regex() string {
|
||||
}
|
||||
|
||||
type PathString struct {
|
||||
Value string `@Ident`
|
||||
Value string `parser:"@Ident"`
|
||||
}
|
||||
|
||||
func (p PathString) regex() string {
|
||||
return regexp.QuoteMeta(p.Value)
|
||||
}
|
||||
|
||||
type PathPattern struct {
|
||||
Parts []PathPart `@@+`
|
||||
type Pattern struct {
|
||||
Parts []PathPart `parser:"@@+"`
|
||||
}
|
||||
|
||||
func (pp PathPattern) regex() string {
|
||||
func (pp Pattern) regex() string {
|
||||
sb := &strings.Builder{}
|
||||
|
||||
for _, p := range pp.Parts {
|
||||
@@ -59,7 +73,7 @@ func (pp PathPattern) regex() 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() + "$")
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
@@ -72,15 +86,19 @@ func (pp PathPattern) Match(s string) (bool, map[string]string, error) {
|
||||
|
||||
ctx := map[string]string{}
|
||||
for i, name := range r.SubexpNames() {
|
||||
ctx[name] = ms[i]
|
||||
if name != "" {
|
||||
ctx[name] = ms[i]
|
||||
}
|
||||
}
|
||||
|
||||
ctx[FullMatch] = ms[0]
|
||||
|
||||
return true, ctx, nil
|
||||
}
|
||||
|
||||
var parser = participle.MustBuild[PathPattern](
|
||||
var parser = participle.MustBuild[Pattern](
|
||||
participle.Union[PathPart](
|
||||
&PathSlash{},
|
||||
&PathLiteral{},
|
||||
&PathLongPattern{},
|
||||
&PathShortPattern{},
|
||||
&PathString{},
|
||||
@@ -88,6 +106,8 @@ var parser = participle.MustBuild[PathPattern](
|
||||
participle.Lexer(
|
||||
lexer.MustSimple([]lexer.SimpleRule{
|
||||
{Name: "Slash", Pattern: `/`},
|
||||
{Name: "LongAny", Pattern: `\*\*`},
|
||||
{Name: "ShortAny", Pattern: `\*`},
|
||||
{Name: "PatternLongOpen", Pattern: `{{`},
|
||||
{Name: "PatternLongClose", 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)
|
||||
}
|
||||
|
||||
func MustParsePattern(path string) *PathPattern {
|
||||
func MustParsePattern(path string) *Pattern {
|
||||
p, err := ParsePattern(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
+124
-25
@@ -7,53 +7,124 @@ import (
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func Test1(t *testing.T) {
|
||||
func TestPath(t *testing.T) {
|
||||
p, err := path.ParsePattern("/foo/bar")
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, p, &path.PathPattern{
|
||||
assert.DeepEqual(t, p, &path.Pattern{
|
||||
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")
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, p, &path.PathPattern{
|
||||
Parts: []path.PathPart{
|
||||
&path.PathString{"posts"}, &path.PathSlash{}, &path.PathShortPattern{"name"}, &path.PathString{".md"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func Test3(t *testing.T) {
|
||||
p, err := path.ParsePattern("posts/{{path}}/{name}.md")
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, p, &path.PathPattern{
|
||||
assert.DeepEqual(t, p, &path.Pattern{
|
||||
Parts: []path.PathPart{
|
||||
&path.PathString{"posts"},
|
||||
&path.PathSlash{},
|
||||
&path.PathLongPattern{"path"},
|
||||
&path.PathSlash{},
|
||||
&path.PathLiteral{Slash: true},
|
||||
&path.PathShortPattern{"name"},
|
||||
&path.PathString{".md"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func Test4(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")
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, p, &path.Pattern{
|
||||
Parts: []path.PathPart{
|
||||
&path.PathString{"posts"},
|
||||
&path.PathLiteral{Slash: true},
|
||||
&path.PathLongPattern{"path"},
|
||||
&path.PathLiteral{Slash: true},
|
||||
&path.PathShortPattern{"name"},
|
||||
&path.PathString{".md"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, p, &path.PathPattern{
|
||||
assert.DeepEqual(t, p, &path.Pattern{
|
||||
Parts: []path.PathPart{
|
||||
&path.PathString{"foo"},
|
||||
&path.PathLongPattern{"path"},
|
||||
&path.PathSlash{},
|
||||
&path.PathLiteral{Slash: true},
|
||||
&path.PathShortPattern{"name"},
|
||||
&path.PathString{".md"},
|
||||
},
|
||||
@@ -65,9 +136,37 @@ func Test4(t *testing.T) {
|
||||
assert.NilError(t, err, nil)
|
||||
assert.Equal(t, m, true)
|
||||
assert.DeepEqual(t, ctx, map[string]string{
|
||||
"": "foo-1/bar/baz/post-1.md",
|
||||
"name": "post-1",
|
||||
"path": "-1/bar/baz",
|
||||
path.FullMatch: "foo-1/bar/baz/post-1.md",
|
||||
"name": "post-1",
|
||||
"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
|
||||
}
|
||||
Reference in New Issue
Block a user