Compare commits
32 Commits
6b1a81e8cb
...
4365d1fa11
Author | SHA1 | Date |
---|---|---|
Maxim Lebedev | 4365d1fa11 | |
Maxim Lebedev | 1dbce57f2e | |
Maxim Lebedev | 6a4b77fb58 | |
Maxim Lebedev | c680cca04b | |
Maxim Lebedev | df55414167 | |
Maxim Lebedev | db68a8d768 | |
Maxim Lebedev | dd9cc5b2d1 | |
Maxim Lebedev | 70853bc665 | |
Maxim Lebedev | d677254e32 | |
Maxim Lebedev | e3362fcd9f | |
Maxim Lebedev | 675926e420 | |
Maxim Lebedev | 23e24adaa7 | |
Maxim Lebedev | 5e836af040 | |
Maxim Lebedev | 48a2f519f5 | |
Maxim Lebedev | bf6efcdf21 | |
Maxim Lebedev | 0ad5c9694f | |
Maxim Lebedev | a475dacd60 | |
Maxim Lebedev | 6012945d06 | |
Maxim Lebedev | 74b41b972b | |
Maxim Lebedev | f3c4118286 | |
Maxim Lebedev | e0b8a6efcb | |
Maxim Lebedev | d42b96607b | |
Maxim Lebedev | 0fff9a10d9 | |
Maxim Lebedev | 77fa1b0b5e | |
Maxim Lebedev | 7d8246e901 | |
Maxim Lebedev | 6ae785ebc9 | |
Maxim Lebedev | 378ec02778 | |
Maxim Lebedev | f036ea127d | |
Maxim Lebedev | 43e96e2095 | |
Maxim Lebedev | 2cf99dcba1 | |
Maxim Lebedev | e74c0a0b66 | |
Maxim Lebedev | c7158f47dd |
|
@ -0,0 +1,48 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
Path string
|
||||
Updated time.Time
|
||||
Content []byte
|
||||
}
|
||||
|
||||
// LogicalName returns full file name without directory path.
|
||||
func (f File) LogicalName() string {
|
||||
return filepath.Base(f.Path)
|
||||
}
|
||||
|
||||
// BaseFileName returns file name without extention and directory path.
|
||||
func (f File) BaseFileName() string {
|
||||
base := filepath.Base(f.Path)
|
||||
|
||||
return strings.TrimSuffix(base, filepath.Ext(base))
|
||||
}
|
||||
|
||||
// Ext returns file extention.
|
||||
func (f File) Ext() string {
|
||||
if ext := filepath.Ext(f.Path); len(ext) > 1 {
|
||||
return ext[1:]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Dir returns file directory.
|
||||
func (f File) Dir() string {
|
||||
return filepath.Dir(f.Path)
|
||||
}
|
||||
|
||||
func (f File) MediaType() string {
|
||||
return mime.TypeByExtension(f.Ext())
|
||||
}
|
||||
|
||||
func (f File) GoString() string {
|
||||
return "domain.File(" + f.Path + ")"
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package domain
|
||||
|
||||
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
|
||||
}
|
|
@ -7,4 +7,5 @@ type Page struct {
|
|||
Params map[string]any
|
||||
Title string
|
||||
Content []byte
|
||||
Files Files
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
@ -8,7 +9,9 @@ import (
|
|||
|
||||
type Site struct {
|
||||
Language language.Tag
|
||||
BaseURL *url.URL
|
||||
TimeZone *time.Location
|
||||
Params map[string]any
|
||||
Title string
|
||||
Files Files
|
||||
}
|
||||
|
|
|
@ -36,17 +36,17 @@ func NewFileSystemPageRepository(rootDir fs.FS) page.Repository {
|
|||
}
|
||||
}
|
||||
|
||||
func (repo *fileSystemPageRepository) Get(ctx context.Context, lang language.Tag, path string) (*domain.Page, error) {
|
||||
func (repo *fileSystemPageRepository) Get(ctx context.Context, lang language.Tag, p string) (*domain.Page, error) {
|
||||
ext := ".md"
|
||||
if lang != language.Und {
|
||||
ext = "." + lang.String() + ext
|
||||
}
|
||||
|
||||
target := path + ext
|
||||
index := p + ext
|
||||
|
||||
f, err := repo.dir.Open(target)
|
||||
f, err := repo.dir.Open(index)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot open '%s' page file: %w", target, err)
|
||||
return nil, fmt.Errorf("cannot open '%s' page file: %w", index, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
|
@ -60,5 +60,6 @@ func (repo *fileSystemPageRepository) Get(ctx context.Context, lang language.Tag
|
|||
Title: data.Title,
|
||||
Content: data.Content,
|
||||
Params: data.Params,
|
||||
Files: make([]*domain.File, 0),
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -33,23 +33,23 @@ func TestGet(t *testing.T) {
|
|||
}{
|
||||
"index": {
|
||||
input: path.Join("index"),
|
||||
expect: &domain.Page{Content: []byte("index.md")},
|
||||
expect: &domain.Page{Content: []byte("index.md"), Files: make([]*domain.File, 0)},
|
||||
},
|
||||
"file": {
|
||||
input: path.Join("file"),
|
||||
expect: &domain.Page{Content: []byte("file.md")},
|
||||
expect: &domain.Page{Content: []byte("file.md"), Files: make([]*domain.File, 0)},
|
||||
},
|
||||
"folder": {
|
||||
input: path.Join("folder", "index"),
|
||||
expect: &domain.Page{Content: []byte("folder/index.md")},
|
||||
expect: &domain.Page{Content: []byte("folder/index.md"), Files: make([]*domain.File, 0)},
|
||||
},
|
||||
"both-file": {
|
||||
input: path.Join("both"),
|
||||
expect: &domain.Page{Content: []byte("both.md")},
|
||||
expect: &domain.Page{Content: []byte("both.md"), Files: make([]*domain.File, 0)},
|
||||
},
|
||||
"both-folder": {
|
||||
input: path.Join("both", "index"),
|
||||
expect: &domain.Page{Content: []byte("both/index.md")},
|
||||
expect: &domain.Page{Content: []byte("both/index.md"), Files: make([]*domain.File, 0)},
|
||||
},
|
||||
} {
|
||||
name, tc := name, tc
|
||||
|
|
|
@ -4,21 +4,25 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"slices"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/page"
|
||||
"source.toby3d.me/toby3d/home/internal/static"
|
||||
"source.toby3d.me/toby3d/home/internal/urlutil"
|
||||
)
|
||||
|
||||
type pageUseCase struct {
|
||||
pages page.Repository
|
||||
pages page.Repository
|
||||
statics static.Repository
|
||||
}
|
||||
|
||||
func NewPageUseCase(pages page.Repository) page.UseCase {
|
||||
func NewPageUseCase(pages page.Repository, statics static.Repository) page.UseCase {
|
||||
return &pageUseCase{
|
||||
pages: pages,
|
||||
pages: pages,
|
||||
statics: statics,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,6 +50,20 @@ func (ucase *pageUseCase) Do(ctx context.Context, lang language.Tag, p string) (
|
|||
continue
|
||||
}
|
||||
|
||||
if out.Files, _, err = ucase.statics.Fetch(ctx, path.Dir(targets[i])); err != nil {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
|
||||
pagefsrepo "source.toby3d.me/toby3d/home/internal/page/repository/fs"
|
||||
"source.toby3d.me/toby3d/home/internal/page/usecase"
|
||||
"source.toby3d.me/toby3d/home/internal/static"
|
||||
)
|
||||
|
||||
func TestDo(t *testing.T) {
|
||||
|
@ -24,7 +25,7 @@ func TestDo(t *testing.T) {
|
|||
filepath.Join("index.md"): &fstest.MapFile{Data: []byte(`index.md`)},
|
||||
})
|
||||
|
||||
ucase := usecase.NewPageUseCase(pages)
|
||||
ucase := usecase.NewPageUseCase(pages, static.NewDummyRepository())
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
input string
|
||||
|
|
|
@ -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
|
||||
|
@ -56,7 +62,7 @@ func (repo *fileSystemSiteRepository) Get(ctx context.Context, lang language.Tag
|
|||
}
|
||||
defer f.Close()
|
||||
|
||||
data := new(Site)
|
||||
data := NewSite()
|
||||
if data.Content, err = frontmatter.Parse(f, data, FrontMatterFormats...); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse site content as FrontMatter: %w", err)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -93,3 +100,32 @@ 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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ func TestGet(t *testing.T) {
|
|||
expect: &domain.Site{
|
||||
Language: language.English,
|
||||
Title: "example",
|
||||
Params: make(map[string]any),
|
||||
},
|
||||
},
|
||||
"russian": {
|
||||
|
@ -42,6 +43,7 @@ func TestGet(t *testing.T) {
|
|||
expect: &domain.Site{
|
||||
Language: language.Russian,
|
||||
Title: "пример",
|
||||
Params: make(map[string]any),
|
||||
},
|
||||
},
|
||||
} {
|
||||
|
|
|
@ -3,20 +3,24 @@ package usecase
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/site"
|
||||
"source.toby3d.me/toby3d/home/internal/static"
|
||||
)
|
||||
|
||||
type siteUseCase struct {
|
||||
sites site.Repository
|
||||
sites site.Repository
|
||||
statics static.Repository
|
||||
}
|
||||
|
||||
func NewSiteUseCase(sites site.Repository) site.UseCase {
|
||||
func NewSiteUseCase(sites site.Repository, statics static.Repository) site.UseCase {
|
||||
return &siteUseCase{
|
||||
sites: sites,
|
||||
sites: sites,
|
||||
statics: statics,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,6 +30,18 @@ func (ucase *siteUseCase) Do(ctx context.Context, lang language.Tag) (*domain.Si
|
|||
return nil, fmt.Errorf("cannot find base site data: %w", err)
|
||||
}
|
||||
|
||||
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 {
|
||||
return out, nil
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package static
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
)
|
||||
|
||||
type (
|
||||
Repository interface {
|
||||
Get(ctx context.Context, path string) (*domain.File, error)
|
||||
Fetch(ctx context.Context, dir string) (domain.Files, int, error)
|
||||
}
|
||||
|
||||
dummyRepository struct{}
|
||||
)
|
||||
|
||||
func NewDummyRepository() dummyRepository {
|
||||
return dummyRepository{}
|
||||
}
|
||||
|
||||
func (dummyRepository) Get(ctx context.Context, path string) (*domain.File, error) { return nil, nil }
|
||||
|
||||
func (dummyRepository) Fetch(ctx context.Context, dir string) (domain.Files, int, error) {
|
||||
return nil, 0, nil
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package fs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/static"
|
||||
)
|
||||
|
||||
type fileServerStaticRepository struct {
|
||||
root fs.FS
|
||||
}
|
||||
|
||||
func NewFileServerStaticRepository(root fs.FS) static.Repository {
|
||||
return &fileServerStaticRepository{
|
||||
root: root,
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *fileServerStaticRepository) Get(ctx context.Context, p string) (*domain.File, error) {
|
||||
info, err := fs.Stat(repo.root, p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot stat static on path '%s': %w", p, err)
|
||||
}
|
||||
|
||||
content, err := fs.ReadFile(repo.root, p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read static content on path '%s': %w", p, err)
|
||||
}
|
||||
|
||||
return &domain.File{
|
||||
Path: p,
|
||||
Updated: info.ModTime(),
|
||||
Content: content,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (repo *fileServerStaticRepository) Fetch(ctx context.Context, d string) (domain.Files, int, error) {
|
||||
entries, err := fs.ReadDir(repo.root, d)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("cannot read directory on path '%s': %w", d, err)
|
||||
}
|
||||
|
||||
out := make(domain.Files, 0, len(entries))
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := fs.ReadFile(repo.root, filepath.Join(d, entry.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, &domain.File{
|
||||
Path: filepath.Join(d, info.Name()),
|
||||
Updated: info.ModTime(),
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
|
||||
return out, len(out), nil
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package static
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
)
|
||||
|
||||
type UseCase interface {
|
||||
Do(ctx context.Context, path string) (*domain.File, error)
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/static"
|
||||
)
|
||||
|
||||
type staticUseCase struct {
|
||||
statics static.Repository
|
||||
}
|
||||
|
||||
func NewStaticUseCase(statics static.Repository) static.UseCase {
|
||||
return &staticUseCase{
|
||||
statics: statics,
|
||||
}
|
||||
}
|
||||
|
||||
func (ucase *staticUseCase) Do(ctx context.Context, p string) (*domain.File, error) {
|
||||
p = strings.TrimPrefix(path.Clean(p), "/")
|
||||
|
||||
if ext := path.Ext(p); ext == ".html" || ext == ".md" {
|
||||
return nil, fs.ErrNotExist
|
||||
}
|
||||
|
||||
f, err := ucase.statics.Get(ctx, p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get static file: %w", err)
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package safe
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"html/template"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type Namespace struct{}
|
||||
|
||||
var ErrSafeHTML error = errors.New("unsupported input type for SafeHTML")
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -2,42 +2,27 @@ package fs
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"reflect"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/theme"
|
||||
)
|
||||
|
||||
type fileSystemThemeRepository struct {
|
||||
dir fs.FS
|
||||
dir fs.FS
|
||||
funcMap template.FuncMap
|
||||
}
|
||||
|
||||
var ErrSafeHTML error = errors.New("unsupported input type for SafeHTML")
|
||||
|
||||
var helpers = template.FuncMap{
|
||||
"safeHTML": func(v reflect.Value) (template.HTML, error) {
|
||||
switch v.Kind() {
|
||||
default:
|
||||
return "", ErrSafeHTML
|
||||
case reflect.Slice:
|
||||
return template.HTML(v.Bytes()), nil
|
||||
case reflect.String:
|
||||
return template.HTML(v.String()), nil
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
66
main.go
66
main.go
|
@ -5,9 +5,11 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
|
@ -25,10 +27,14 @@ import (
|
|||
|
||||
"source.toby3d.me/toby3d/home/internal/common"
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/page"
|
||||
pagefsrepo "source.toby3d.me/toby3d/home/internal/page/repository/fs"
|
||||
pageucase "source.toby3d.me/toby3d/home/internal/page/usecase"
|
||||
sitefsrepo "source.toby3d.me/toby3d/home/internal/site/repository/fs"
|
||||
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"
|
||||
)
|
||||
|
@ -81,19 +87,30 @@ 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)
|
||||
|
||||
themes := themefsrepo.NewFileSystemThemeRepository(themeDir)
|
||||
themer := themeucase.NewThemeUseCase(themes)
|
||||
|
||||
statics := staticfsrepo.NewFileServerStaticRepository(contentDir)
|
||||
sites := sitefsrepo.NewFileSystemSiteRepository(contentDir)
|
||||
siter := siteucase.NewSiteUseCase(sites)
|
||||
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)
|
||||
pager := pageucase.NewPageUseCase(pages, statics)
|
||||
|
||||
matcher := language.NewMatcher(message.DefaultCatalog.Languages())
|
||||
|
||||
server := &http.Server{
|
||||
Addr: config.AddrPort().String(),
|
||||
|
@ -105,16 +122,35 @@ func main() {
|
|||
|
||||
lang, _, _ := matcher.Match(tags...)
|
||||
|
||||
site, err := siter.Do(r.Context(), lang)
|
||||
s, err := siter.Do(r.Context(), lang)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
page, err := pager.Do(r.Context(), lang, r.URL.Path)
|
||||
p, err := pager.Do(r.Context(), lang, r.URL.Path)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
if !errors.Is(err, page.ErrNotExist) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
f, err := staticer.Do(r.Context(), r.URL.Path)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeContent(w, r, f.LogicalName(), f.Updated, bytes.NewReader(f.Content))
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -128,8 +164,8 @@ func main() {
|
|||
|
||||
w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8)
|
||||
if err = tpl.Execute(w, &Context{
|
||||
Site: site,
|
||||
Page: page,
|
||||
Site: s,
|
||||
Page: p,
|
||||
}); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
@ -158,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)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue