Merge branch 'feature/funcs' into develop
/ docker (push) Successful in 1m5s Details

This commit is contained in:
Maxim Lebedev 2023-11-09 07:59:47 +06:00
commit 4365d1fa11
Signed by: toby3d
GPG Key ID: 1F14E25B7C119FC5
13 changed files with 278 additions and 27 deletions

View File

@ -42,3 +42,7 @@ func (f File) Dir() string {
func (f File) MediaType() string {
return mime.TypeByExtension(f.Ext())
}
func (f File) GoString() string {
return "domain.File(" + f.Path + ")"
}

View File

@ -1,4 +1,18 @@
package domain
// TODO(toby3d): search by glob pattern, type or name/id.
import "path"
// TODO(toby3d): search by type or name/id.
type Files []*File
func (f Files) GetMatch(pattern string) *File {
for i := range f {
if matched, err := path.Match(pattern, f[i].Path); err != nil || !matched {
continue
}
return f[i]
}
return nil
}

View File

@ -1,6 +1,7 @@
package domain
import (
"net/url"
"time"
"golang.org/x/text/language"
@ -8,6 +9,7 @@ import (
type Site struct {
Language language.Tag
BaseURL *url.URL
TimeZone *time.Location
Params map[string]any
Title string

View File

@ -54,12 +54,14 @@ func (ucase *pageUseCase) Do(ctx context.Context, lang language.Tag, p string) (
return out, nil
}
for j := range out.Files {
if ext := out.Files[j].Ext(); ext != ".html" && ext != ".md" {
for j := 0; j < len(out.Files); j++ {
if ext := out.Files[j].Ext(); ext != "html" && ext != "md" {
continue
}
out.Files = slices.Delete(out.Files, j, j+1)
j--
}
return out, nil

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"io/fs"
"net/url"
"time"
"github.com/adrg/frontmatter"
@ -18,6 +19,7 @@ type (
Site struct {
Title string `yaml:"title"`
TimeZone TimeZone `yaml:"timeZone"`
BaseURL URL `yaml:"baseUrl"`
Params map[string]any `yaml:",inline"`
Content []byte `yaml:"-"`
}
@ -26,6 +28,10 @@ type (
*time.Location `yaml:"-"`
}
URL struct {
*url.URL `yaml:"-"`
}
fileSystemSiteRepository struct {
dir fs.FS
rootPath string
@ -64,6 +70,7 @@ func (repo *fileSystemSiteRepository) Get(ctx context.Context, lang language.Tag
return &domain.Site{
Language: lang,
Title: data.Title,
BaseURL: data.BaseURL.URL,
TimeZone: data.TimeZone.Location,
Params: data.Params,
}, nil
@ -94,6 +101,29 @@ func (tz TimeZone) MarshalYAML() (any, error) {
return tz.Location.String(), nil
}
func (u *URL) UnmarshalYAML(value *yaml.Node) error {
if value.IsZero() {
return nil
}
parsed, err := url.Parse(value.Value)
if err != nil {
return fmt.Errorf("cannot parse URL value '%v': %w", value, err)
}
u.URL = parsed
return nil
}
func (u URL) MarshalYAML() (any, error) {
if u.URL == nil {
return nil, nil
}
return u.URL.String(), nil
}
func NewSite() *Site {
return &Site{
Params: make(map[string]any),

View File

@ -3,6 +3,7 @@ package usecase
import (
"context"
"fmt"
"slices"
"golang.org/x/text/language"
@ -29,7 +30,17 @@ func (ucase *siteUseCase) Do(ctx context.Context, lang language.Tag) (*domain.Si
return nil, fmt.Errorf("cannot find base site data: %w", err)
}
out.Files, _, _ = ucase.statics.Fetch(ctx, ".")
if out.Files, _, err = ucase.statics.Fetch(ctx, "."); err == nil {
for i := 0; i < len(out.Files); i++ {
if ext := out.Files[i].Ext(); ext != "html" && ext != "md" {
continue
}
out.Files = slices.Delete(out.Files, i, i+1)
i--
}
}
sub, err := ucase.sites.Get(ctx, lang)
if err != nil {

View File

@ -0,0 +1,42 @@
package partials
import (
"bytes"
"fmt"
"html/template"
"io/fs"
"path/filepath"
"strings"
)
type Namespace struct {
dir fs.FS
funcMap template.FuncMap
}
func New(dir fs.FS, funcMap template.FuncMap) *Namespace {
return &Namespace{
dir: dir,
funcMap: funcMap,
}
}
func (ns *Namespace) Include(name string, data any) (template.HTML, error) {
name = strings.TrimPrefix(filepath.Clean(name), "partials/")
if filepath.Ext(name) == "" {
name += ".html"
}
tpl, err := template.New(name).Funcs(ns.funcMap).ParseFS(ns.dir, name)
if err != nil {
return "", fmt.Errorf("cannot parse partial: %w", err)
}
buf := bytes.NewBuffer(nil)
if err = tpl.Execute(buf, data); err != nil {
return "", fmt.Errorf("cannot execute partial: %w", err)
}
return template.HTML(buf.String()), nil
}

View File

@ -1,4 +1,4 @@
package templateutil
package safe
import (
"errors"
@ -6,13 +6,23 @@ import (
"reflect"
)
type Namespace struct{}
var ErrSafeHTML error = errors.New("unsupported input type for SafeHTML")
func SafeHTML(v reflect.Value) (template.HTML, error) {
func New() *Namespace {
return &Namespace{}
}
func (Namespace) HTML(v reflect.Value) (template.HTML, error) {
switch v.Kind() {
default:
return "", ErrSafeHTML
case reflect.Slice:
if v.Elem().Kind() != reflect.Uint8 {
return "", ErrSafeHTML
}
return template.HTML(v.Bytes()), nil
case reflect.String:
return template.HTML(v.String()), nil

View File

@ -0,0 +1,17 @@
package strings
import "strings"
type Namespace struct{}
func New() *Namespace {
return &Namespace{}
}
func (ns *Namespace) HasPrefix(s, prefix string) bool {
return strings.HasPrefix(s, prefix)
}
func (ns *Namespace) HasSuffix(s, suffix string) bool {
return strings.HasSuffix(s, suffix)
}

View File

@ -0,0 +1,73 @@
package templateutil
import (
"fmt"
"html/template"
"io/fs"
"source.toby3d.me/toby3d/home/internal/site"
"source.toby3d.me/toby3d/home/internal/templateutil/partials"
"source.toby3d.me/toby3d/home/internal/templateutil/safe"
"source.toby3d.me/toby3d/home/internal/templateutil/strings"
"source.toby3d.me/toby3d/home/internal/templateutil/urls"
)
type Function struct {
Handler func(v ...any) any
Methods template.FuncMap
Name string
}
func New(dir fs.FS, siter site.UseCase) (template.FuncMap, error) {
partialDir, err := fs.Sub(dir, "partials")
if err != nil {
return nil, fmt.Errorf("cannot substitute into partials subdirectory: %w", err)
}
funcMap := make(template.FuncMap)
funcs := make([]Function, 0)
stringsNamespace := strings.New()
funcs = append(funcs, Function{
Name: "strings",
Handler: func(v ...any) any { return stringsNamespace },
Methods: template.FuncMap{
"hasPrefix": stringsNamespace.HasPrefix,
"hasSuffix": stringsNamespace.HasSuffix,
},
})
safeNamespace := safe.New()
funcs = append(funcs, Function{
Name: "safe",
Handler: func(v ...any) any { return safeNamespace },
Methods: template.FuncMap{
"safeHTML": safeNamespace.HTML,
},
})
partialsNamespace := partials.New(partialDir, funcMap)
funcs = append(funcs, Function{
Name: "partials",
Handler: func(v ...any) any { return partialsNamespace },
Methods: template.FuncMap{
"partial": partialsNamespace.Include,
},
})
urlsNamespace := urls.New(siter)
funcs = append(funcs, Function{
Name: "urls",
Handler: func(v ...any) any { return urlsNamespace },
Methods: template.FuncMap{
"absURL": urlsNamespace.AbsURL,
"relURL": urlsNamespace.RelURL,
},
})
for _, f := range funcs {
funcMap[f.Name] = f.Handler
for name, m := range f.Methods {
funcMap[name] = m
}
}
return funcMap, nil
}

View File

@ -0,0 +1,42 @@
package urls
import (
"context"
"errors"
"fmt"
"net/url"
"path"
"golang.org/x/text/language"
"source.toby3d.me/toby3d/home/internal/site"
)
type Namespace struct {
siter site.UseCase
}
var ErrAbsURL error = errors.New("unsupported input type for AbsURL")
func New(siter site.UseCase) *Namespace {
return &Namespace{
siter: siter,
}
}
func (ns *Namespace) AbsURL(p string) (string, error) {
site, err := ns.siter.Do(context.Background(), language.Und)
if err != nil {
return "", fmt.Errorf("cannot fetch site root for AbsURL processing: %w", err)
}
return site.BaseURL.JoinPath(p).String(), nil
}
func (ns *Namespace) RelURL(p string) string {
return path.Clean("/" + p)
}
func (ns *Namespace) Parse(rawUrl string) (*url.URL, error) {
return url.Parse(rawUrl)
}

View File

@ -6,28 +6,23 @@ import (
"html/template"
"io/fs"
"source.toby3d.me/toby3d/home/internal/templateutil"
"source.toby3d.me/toby3d/home/internal/theme"
)
type fileSystemThemeRepository struct {
dir fs.FS
dir fs.FS
funcMap template.FuncMap
}
var helpers = template.FuncMap{
"safeHTML": templateutil.SafeHTML,
}
func NewFileSystemThemeRepository(dir fs.FS) theme.Repository {
func NewFileSystemThemeRepository(dir fs.FS, funcMap template.FuncMap) theme.Repository {
return &fileSystemThemeRepository{
dir: dir,
dir: dir,
funcMap: funcMap,
}
}
func (repo *fileSystemThemeRepository) Get(ctx context.Context) (*template.Template, error) {
tpl, err := template.New("").
Funcs(helpers).
ParseFS(repo.dir, "baseof.html", "single.html")
tpl, err := template.New("").Funcs(repo.funcMap).ParseFS(repo.dir, "*.html")
if err != nil {
return nil, fmt.Errorf("cannot find baseof template: %w", err)
}

29
main.go
View File

@ -34,6 +34,7 @@ import (
siteucase "source.toby3d.me/toby3d/home/internal/site/usecase"
staticfsrepo "source.toby3d.me/toby3d/home/internal/static/repository/fs"
staticucase "source.toby3d.me/toby3d/home/internal/static/usecase"
"source.toby3d.me/toby3d/home/internal/templateutil"
themefsrepo "source.toby3d.me/toby3d/home/internal/theme/repository/fs"
themeucase "source.toby3d.me/toby3d/home/internal/theme/usecase"
)
@ -86,23 +87,31 @@ func init() {
func main() {
ctx := context.Background()
matcher := language.NewMatcher(message.DefaultCatalog.Languages())
themeDir := os.DirFS(config.ThemeDir)
partialsDir, err := fs.Sub(themeDir, "partials")
if err != nil {
logger.Fatalln("cannot subtitute theme directory to partials subdirectory:", err)
}
contentDir := os.DirFS(config.ContentDir)
themeDir := os.DirFS(config.ThemeDir)
statics := staticfsrepo.NewFileServerStaticRepository(contentDir)
staticer := staticucase.NewStaticUseCase(statics)
themes := themefsrepo.NewFileSystemThemeRepository(themeDir)
themer := themeucase.NewThemeUseCase(themes)
sites := sitefsrepo.NewFileSystemSiteRepository(contentDir)
siter := siteucase.NewSiteUseCase(sites, statics)
funcMap, err := templateutil.New(partialsDir, siter)
if err != nil {
logger.Fatalln("cannot setup template.FuncMap for templates: %w", err)
}
staticer := staticucase.NewStaticUseCase(statics)
themes := themefsrepo.NewFileSystemThemeRepository(themeDir, funcMap)
themer := themeucase.NewThemeUseCase(themes)
pages := pagefsrepo.NewFileSystemPageRepository(contentDir)
pager := pageucase.NewPageUseCase(pages, statics)
matcher := language.NewMatcher(message.DefaultCatalog.Languages())
server := &http.Server{
Addr: config.AddrPort().String(),
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -185,14 +194,14 @@ func main() {
go func(server *http.Server) {
logger.Printf("starting server on %d...", config.Port)
if err := server.ListenAndServe(); err != nil {
if err = server.ListenAndServe(); err != nil {
logger.Fatalln("cannot listen and serve server:", err)
}
}(server)
<-done
if err := server.Shutdown(ctx); err != nil {
if err = server.Shutdown(ctx); err != nil {
logger.Fatalln("failed shutdown server:", err)
}