Compare commits

...

13 Commits

Author SHA1 Message Date
Maxim Lebedev dd45c634e8
♻️ Replace only baseof site params on language specific ones
/ docker (push) Successful in 1m3s Details
2023-11-08 08:56:38 +06:00
Maxim Lebedev 76a65f0e3a
Merge branch 'feature/theme' into develop 2023-11-08 08:55:55 +06:00
Maxim Lebedev 94e4691734
🏗️ Use theme templates instead embed
Embed templates must be used for internal pages, dashboard and other
non-customizable views
2023-11-08 08:55:27 +06:00
Maxim Lebedev e71a84bb07
👔 Created basic theme use case implementation 2023-11-08 08:53:32 +06:00
Maxim Lebedev c3245c3588
👔 Created sample theme use case interface 2023-11-08 08:53:17 +06:00
Maxim Lebedev 0e73130b78
🗃️ Created simple theme FileSystem repository implementation 2023-11-08 08:51:55 +06:00
Maxim Lebedev c61ab4d929
🗃️ Created sample theme repository interface 2023-11-08 08:50:44 +06:00
Maxim Lebedev 72eb8627ee
🧑‍💻 Check and create theme dir if not exist 2023-11-08 08:22:07 +06:00
Maxim Lebedev 3e5709e498
🔧 Added ThemeDir config 2023-11-08 08:21:45 +06:00
Maxim Lebedev 372efb088e
Added tail Params support for Site domain 2023-11-08 08:15:08 +06:00
Maxim Lebedev 8e821d9907
Added tail params support for Page domain 2023-11-08 08:14:54 +06:00
Maxim Lebedev f61d06fc39
♻️ Refactored Site params redefining 2023-11-08 08:13:01 +06:00
Maxim Lebedev 5b032f2c99
Added site TimeZone support 2023-11-08 08:08:17 +06:00
11 changed files with 169 additions and 23 deletions

View File

@ -7,8 +7,9 @@ import (
)
type Config struct {
Host string `env:"HOST" envDefault:"0.0.0.0"`
ContentDir string `env:"CONTENT_DIR" envDefault:"content/"`
Host string `env:"HOST" envDefault:"0.0.0.0"`
ThemeDir string `env:"THEME_DIR" envDefault:"theme/"`
Port uint16 `env:"PORT" envDefault:"3000"`
}

View File

@ -4,6 +4,7 @@ import "golang.org/x/text/language"
type Page struct {
Language language.Tag
Params map[string]any
Title string
Content []byte
}

View File

@ -1,10 +1,14 @@
package domain
import (
"time"
"golang.org/x/text/language"
)
type Site struct {
Language language.Tag
TimeZone *time.Location
Params map[string]any
Title string
}

View File

@ -59,5 +59,6 @@ func (repo *fileSystemPageRepository) Get(ctx context.Context, lang language.Tag
Language: lang,
Title: data.Title,
Content: data.Content,
Params: data.Params,
}, nil
}

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"io/fs"
"time"
"github.com/adrg/frontmatter"
"golang.org/x/text/language"
@ -15,9 +16,14 @@ import (
type (
Site struct {
Title string `yaml:"title"`
Params map[string]any `yaml:",inline"`
Content []byte `yaml:"-"`
Title string `yaml:"title"`
TimeZone TimeZone `yaml:"timeZone"`
Params map[string]any `yaml:",inline"`
Content []byte `yaml:"-"`
}
TimeZone struct {
*time.Location `yaml:"-"`
}
fileSystemSiteRepository struct {
@ -58,5 +64,32 @@ func (repo *fileSystemSiteRepository) Get(ctx context.Context, lang language.Tag
return &domain.Site{
Language: lang,
Title: data.Title,
TimeZone: data.TimeZone.Location,
Params: data.Params,
}, nil
}
func (tz *TimeZone) UnmarshalYAML(value *yaml.Node) error {
if value.IsZero() {
tz.Location = time.UTC
return nil
}
loc, err := time.LoadLocation(value.Value)
if err != nil {
return fmt.Errorf("cannot parse timeZone value '%v': %w", value, err)
}
tz.Location = loc
return nil
}
func (tz TimeZone) MarshalYAML() (any, error) {
if tz.Location == nil {
return time.UTC.String(), nil
}
return tz.Location.String(), nil
}

View File

@ -21,19 +21,20 @@ func NewSiteUseCase(sites site.Repository) site.UseCase {
}
func (ucase *siteUseCase) Do(ctx context.Context, lang language.Tag) (*domain.Site, error) {
base, err := ucase.sites.Get(ctx, language.Und)
out, err := ucase.sites.Get(ctx, language.Und)
if err != nil {
return nil, fmt.Errorf("cannot find base site data: %w", err)
}
sub, err := ucase.sites.Get(ctx, lang)
if err != nil {
return base, nil
return out, nil
}
out := &domain.Site{Language: sub.Language}
if sub.Title == "" {
out.Title = base.Title
out.Language = sub.Language
for k, v := range sub.Params {
out.Params[k] = v
}
return out, nil

View File

@ -0,0 +1,11 @@
package theme
import (
"context"
"html/template"
)
type Repository interface {
// TODO(toby3d): use Page context to find it's specific template.
Get(ctx context.Context) (*template.Template, error)
}

View File

@ -0,0 +1,29 @@
package fs
import (
"context"
"fmt"
"html/template"
"io/fs"
"source.toby3d.me/toby3d/home/internal/theme"
)
type fileSystemThemeRepository struct {
dir fs.FS
}
func NewFileSystemThemeRepository(dir fs.FS) theme.Repository {
return &fileSystemThemeRepository{
dir: dir,
}
}
func (repo *fileSystemThemeRepository) Get(ctx context.Context) (*template.Template, error) {
tpl, err := template.New("").ParseFS(repo.dir, "baseof.html", "single.html")
if err != nil {
return nil, fmt.Errorf("cannot find baseof.html: %w", err)
}
return tpl, nil
}

10
internal/theme/usecase.go Normal file
View File

@ -0,0 +1,10 @@
package theme
import (
"context"
"html/template"
)
type UseCase interface {
Do(ctx context.Context) (*template.Template, error)
}

View File

@ -0,0 +1,28 @@
package usecase
import (
"context"
"fmt"
"html/template"
"source.toby3d.me/toby3d/home/internal/theme"
)
type themeUseCase struct {
themes theme.Repository
}
func NewThemeUseCase(themes theme.Repository) theme.UseCase {
return &themeUseCase{
themes: themes,
}
}
func (ucase *themeUseCase) Do(ctx context.Context) (*template.Template, error) {
out, err := ucase.themes.Get(ctx)
if err != nil {
return nil, fmt.Errorf("cannot find theme: %w", err)
}
return out.Lookup("baseof"), nil
}

55
main.go
View File

@ -29,9 +29,15 @@ import (
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"
"source.toby3d.me/toby3d/home/web/template"
themefsrepo "source.toby3d.me/toby3d/home/internal/theme/repository/fs"
themeucase "source.toby3d.me/toby3d/home/internal/theme/usecase"
)
type Context struct {
Site *domain.Site
Page *domain.Page
}
var (
config = new(domain.Config)
logger = log.New(os.Stdout, "home\t", log.Lmsgprefix|log.LstdFlags|log.LUTC)
@ -49,21 +55,26 @@ func init() {
logger.Fatalln("cannot unmarshal configuration into domain:", err)
}
if config.ContentDir, err = filepath.Abs(filepath.Clean(config.ContentDir)); err != nil {
logger.Fatalf("cannot format '%s' content dir path into absolute path: %s", config.ContentDir, err)
}
if _, err = os.Stat(config.ContentDir); err != nil {
if errors.Is(err, os.ErrExist) {
return
for _, dir := range []*string{
&config.ContentDir,
&config.ThemeDir,
} {
if *dir, err = filepath.Abs(filepath.Clean(*dir)); err != nil {
logger.Fatalf("cannot format '%s' into absolute path: %s", *dir, err)
}
if !errors.Is(err, os.ErrNotExist) {
logger.Fatalln("cannot check directory path for content:", err)
}
if _, err = os.Stat(*dir); err != nil {
if errors.Is(err, os.ErrExist) {
return
}
if err = os.MkdirAll(config.ContentDir, os.ModePerm); err != nil {
logger.Fatalln("cannot create directory for content:", err)
if !errors.Is(err, os.ErrNotExist) {
logger.Fatalf("cannot check '%s' path: %v", *dir, err)
}
if err = os.MkdirAll(*dir, os.ModePerm); err != nil {
logger.Fatalf("cannot create directory on '%s': %v", *dir, err)
}
}
}
}
@ -73,6 +84,10 @@ func main() {
matcher := language.NewMatcher(message.DefaultCatalog.Languages())
contentDir := os.DirFS(config.ContentDir)
themeDir := os.DirFS(config.ThemeDir)
themes := themefsrepo.NewFileSystemThemeRepository(themeDir)
themer := themeucase.NewThemeUseCase(themes)
sites := sitefsrepo.NewFileSystemSiteRepository(contentDir)
siter := siteucase.NewSiteUseCase(sites)
@ -104,8 +119,20 @@ func main() {
return
}
tpl, err := themer.Do(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8)
template.WriteTemplate(w, template.NewPage(template.NewBaseOf(site), page))
if err = tpl.Execute(w, &Context{
Site: site,
Page: page,
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}),
ErrorLog: logger,
BaseContext: func(ln net.Listener) context.Context { return ctx },