Merge branch 'feature/pages' into develop

This commit is contained in:
Maxim Lebedev 2023-11-08 07:07:52 +06:00
commit b7e9081f48
Signed by: toby3d
GPG Key ID: 1F14E25B7C119FC5
13 changed files with 390 additions and 100 deletions

9
internal/domain/page.go Normal file
View File

@ -0,0 +1,9 @@
package domain
import "golang.org/x/text/language"
type Page struct {
Language language.Tag
Title string
Content []byte
}

View File

@ -0,0 +1,13 @@
package page
import (
"context"
"golang.org/x/text/language"
"source.toby3d.me/toby3d/home/internal/domain"
)
type Repository interface {
Get(ctx context.Context, lang language.Tag, path string) (*domain.Page, error)
}

View File

@ -0,0 +1,63 @@
package fs
import (
"context"
"fmt"
"io/fs"
"github.com/adrg/frontmatter"
"golang.org/x/text/language"
"gopkg.in/yaml.v3"
"source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/page"
)
type (
Page struct {
Title string `yaml:"title"`
Params map[string]any `yaml:",inline"`
Content []byte `yaml:"-"`
}
fileSystemPageRepository struct {
dir fs.FS
rootPath string
}
)
var FrontMatterFormats = []*frontmatter.Format{
frontmatter.NewFormat(`---`, `---`, yaml.Unmarshal),
}
func NewFileSystemPageRepository(rootDir fs.FS) page.Repository {
return &fileSystemPageRepository{
dir: rootDir,
}
}
func (repo *fileSystemPageRepository) Get(ctx context.Context, lang language.Tag, path string) (*domain.Page, error) {
ext := ".md"
if lang != language.Und {
ext = "." + lang.String() + ext
}
target := path + ext
f, err := repo.dir.Open(target)
if err != nil {
return nil, fmt.Errorf("cannot open '%s' page file: %w", target, err)
}
defer f.Close()
data := new(Page)
if data.Content, err = frontmatter.Parse(f, data, FrontMatterFormats...); err != nil {
return nil, fmt.Errorf("cannot parse page content as FrontMatter: %w", err)
}
return &domain.Page{
Language: lang,
Title: data.Title,
Content: data.Content,
}, nil
}

View File

@ -0,0 +1,70 @@
package fs_test
import (
"context"
"path"
"path/filepath"
"testing"
"testing/fstest"
"github.com/google/go-cmp/cmp"
"golang.org/x/text/language"
"source.toby3d.me/toby3d/home/internal/domain"
repository "source.toby3d.me/toby3d/home/internal/page/repository/fs"
)
func TestGet(t *testing.T) {
t.Parallel()
testData := fstest.MapFS{
filepath.Join("both", "index.md"): &fstest.MapFile{Data: []byte("both/index.md")},
filepath.Join("both.md"): &fstest.MapFile{Data: []byte("both.md")},
filepath.Join("file.md"): &fstest.MapFile{Data: []byte("file.md")},
filepath.Join("folder", "index.md"): &fstest.MapFile{Data: []byte("folder/index.md")},
filepath.Join("index.md"): &fstest.MapFile{Data: []byte("index.md")},
}
repo := repository.NewFileSystemPageRepository(testData)
for name, tc := range map[string]struct {
expect *domain.Page
input string
}{
"index": {
input: path.Join("index"),
expect: &domain.Page{Content: []byte("index.md")},
},
"file": {
input: path.Join("file"),
expect: &domain.Page{Content: []byte("file.md")},
},
"folder": {
input: path.Join("folder", "index"),
expect: &domain.Page{Content: []byte("folder/index.md")},
},
"both-file": {
input: path.Join("both"),
expect: &domain.Page{Content: []byte("both.md")},
},
"both-folder": {
input: path.Join("both", "index"),
expect: &domain.Page{Content: []byte("both/index.md")},
},
} {
name, tc := name, tc
t.Run(name, func(t *testing.T) {
t.Parallel()
out, err := repo.Get(context.Background(), language.Und, tc.input)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(out, tc.expect, cmp.AllowUnexported(language.Und)); diff != "" {
t.Error(diff)
}
})
}
}

16
internal/page/usecase.go Normal file
View File

@ -0,0 +1,16 @@
package page
import (
"context"
"errors"
"golang.org/x/text/language"
"source.toby3d.me/toby3d/home/internal/domain"
)
type UseCase interface {
Do(ctx context.Context, lang language.Tag, path string) (*domain.Page, error)
}
var ErrNotExist error = errors.New("page not exists")

View File

@ -0,0 +1,53 @@
package usecase
import (
"context"
"fmt"
"path"
"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/urlutil"
)
type pageUseCase struct {
pages page.Repository
}
func NewPageUseCase(pages page.Repository) page.UseCase {
return &pageUseCase{
pages: pages,
}
}
func (ucase *pageUseCase) Do(ctx context.Context, lang language.Tag, p string) (*domain.Page, error) {
ext := path.Ext(p)
if ext == ".html" {
p = p[:len(p)-len(ext)]
}
hasTrailingSlash := p[len(p)-1] == '/'
head, tail := urlutil.ShiftPath(p)
targets := []string{path.Join(head, tail)}
if tail == "/" {
if hasTrailingSlash || ext == "" {
targets = append([]string{path.Join(head, "index")}, targets...)
}
targets = append(targets, head)
}
for i := range targets {
out, err := ucase.pages.Get(ctx, lang, targets[i])
if err != nil {
continue
}
return out, nil
}
return nil, fmt.Errorf("cannot find page on path '%s': %w", p, page.ErrNotExist)
}

View File

@ -0,0 +1,59 @@
package usecase_test
import (
"context"
"path/filepath"
"testing"
"testing/fstest"
"github.com/google/go-cmp/cmp"
"golang.org/x/text/language"
pagefsrepo "source.toby3d.me/toby3d/home/internal/page/repository/fs"
"source.toby3d.me/toby3d/home/internal/page/usecase"
)
func TestDo(t *testing.T) {
t.Parallel()
pages := pagefsrepo.NewFileSystemPageRepository(fstest.MapFS{
filepath.Join("both", "index.md"): &fstest.MapFile{Data: []byte(`both/index.md`)},
filepath.Join("folder", "index.md"): &fstest.MapFile{Data: []byte(`folder/index.md`)},
filepath.Join("both.md"): &fstest.MapFile{Data: []byte(`both.md`)},
filepath.Join("file.md"): &fstest.MapFile{Data: []byte(`file.md`)},
filepath.Join("index.md"): &fstest.MapFile{Data: []byte(`index.md`)},
})
ucase := usecase.NewPageUseCase(pages)
for name, tc := range map[string]struct {
input string
expect []byte
}{
"file": {input: "/file", expect: []byte(`file.md`)},
"file-slash": {input: "/file/", expect: []byte(`file.md`)},
"file-ext": {input: "/file.html", expect: []byte(`file.md`)},
"folder": {input: "/folder", expect: []byte(`folder/index.md`)},
"folder-slash": {input: "/folder/", expect: []byte(`folder/index.md`)},
"folder-index": {input: "/folder/index.html", expect: []byte(`folder/index.md`)},
"both": {input: "/both", expect: []byte(`both/index.md`)},
"both-slash": {input: "/both/", expect: []byte(`both/index.md`)},
"both-ext": {input: "/both.html", expect: []byte(`both.md`)},
"both-index": {input: "/both/index.html", expect: []byte(`both/index.md`)},
} {
name, tc := name, tc
t.Run(name, func(t *testing.T) {
t.Parallel()
actual, err := ucase.Do(context.Background(), language.Und, tc.input)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(string(actual.Content), string(tc.expect)); diff != "" {
t.Error(diff)
}
})
}
}

View File

@ -37,11 +37,13 @@ func NewFileSystemSiteRepository(rootDir fs.FS) site.Repository {
}
func (repo *fileSystemSiteRepository) Get(ctx context.Context, lang language.Tag) (*domain.Site, error) {
target := "index." + lang.String() + ".md"
if lang == language.Und {
target = "index.md"
ext := ".md"
if lang != language.Und {
ext = "." + lang.String() + ext
}
target := "index" + ext
f, err := repo.dir.Open(target)
if err != nil {
return nil, fmt.Errorf("cannot open '%s' site file: %w", target, err)

23
main.go
View File

@ -25,6 +25,8 @@ import (
"source.toby3d.me/toby3d/home/internal/common"
"source.toby3d.me/toby3d/home/internal/domain"
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"
"source.toby3d.me/toby3d/home/web/template"
@ -70,9 +72,14 @@ func main() {
ctx := context.Background()
matcher := language.NewMatcher(message.DefaultCatalog.Languages())
sites := sitefsrepo.NewFileSystemSiteRepository(os.DirFS(config.ContentDir))
contentDir := os.DirFS(config.ContentDir)
sites := sitefsrepo.NewFileSystemSiteRepository(contentDir)
siter := siteucase.NewSiteUseCase(sites)
pages := pagefsrepo.NewFileSystemPageRepository(contentDir)
pager := pageucase.NewPageUseCase(pages)
server := &http.Server{
Addr: config.AddrPort().String(),
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -90,13 +97,15 @@ func main() {
return
}
page, err := pager.Do(r.Context(), lang, r.URL.Path)
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),
language.English,
[]byte(`hello, world!`),
`toby3d`,
))
template.WriteTemplate(w, template.NewPage(template.NewBaseOf(site), page))
}),
ErrorLog: logger,
BaseContext: func(ln net.Listener) context.Context { return ctx },

View File

@ -15,10 +15,10 @@
{% code
type BaseOf struct {
printer *message.Printer
site domain.Site
site *domain.Site
}
func NewBaseOf(site domain.Site) BaseOf {
func NewBaseOf(site *domain.Site) BaseOf {
return BaseOf{
site: site,
printer: message.NewPrinter(site.Language),

View File

@ -57,10 +57,10 @@ type Pager interface {
//line web/template/baseof.qtpl:16
type BaseOf struct {
printer *message.Printer
site domain.Site
site *domain.Site
}
func NewBaseOf(site domain.Site) BaseOf {
func NewBaseOf(site *domain.Site) BaseOf {
return BaseOf{
site: site,
printer: message.NewPrinter(site.Language),

View File

@ -1,43 +1,41 @@
{% import (
"golang.org/x/text/language"
"source.toby3d.me/toby3d/home/internal/domain"
) %}
{% code
type Page struct {
BaseOf
language language.Tag
title string
content []byte
page *domain.Page
}
func NewPage(base BaseOf, lang language.Tag, content []byte, title string) Page {
func NewPage(base BaseOf, page *domain.Page) Page {
return Page{
BaseOf: base,
language: lang,
title: title,
content: content,
page: page,
}
}
%}
{% stripspace %}
{% func (p Page) Title() %}
{% if p.title != "" %}
{%s p.title %}{% space %}—{% space %}{%= p.BaseOf.Title() %}
{% if p.page.Title != "" %}
{%s p.page.Title %}{% space %}—{% space %}{%= p.BaseOf.Title() %}
{% else %}
{%= p.BaseOf.Title() %}
{% endif %}
{% endfunc %}
{% func (p Page) Lang() %}
{% if p.language != language.Und %}
{%s p.language.String() %}
{% if p.page.Language != language.Und %}
{%s p.page.Language.String() %}
{% else %}
{%= p.BaseOf.Lang() %}
{% endif %}
{% endfunc %}
{% func (p Page) Body() %}
<p>{%z p.content %}</p>
<p>{%z p.page.Content %}</p>
{% endfunc %}
{% endstripspace %}

View File

@ -7,161 +7,159 @@ package template
//line web/template/page.qtpl:1
import (
"golang.org/x/text/language"
"source.toby3d.me/toby3d/home/internal/domain"
)
//line web/template/page.qtpl:5
//line web/template/page.qtpl:7
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
//line web/template/page.qtpl:5
//line web/template/page.qtpl:7
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
//line web/template/page.qtpl:6
//line web/template/page.qtpl:8
type Page struct {
BaseOf
language language.Tag
title string
content []byte
page *domain.Page
}
func NewPage(base BaseOf, lang language.Tag, content []byte, title string) Page {
func NewPage(base BaseOf, page *domain.Page) Page {
return Page{
BaseOf: base,
language: lang,
title: title,
content: content,
BaseOf: base,
page: page,
}
}
//line web/template/page.qtpl:24
//line web/template/page.qtpl:22
func (p Page) StreamTitle(qw422016 *qt422016.Writer) {
//line web/template/page.qtpl:25
if p.title != "" {
//line web/template/page.qtpl:26
qw422016.E().S(p.title)
//line web/template/page.qtpl:26
//line web/template/page.qtpl:23
if p.page.Title != "" {
//line web/template/page.qtpl:24
qw422016.E().S(p.page.Title)
//line web/template/page.qtpl:24
qw422016.N().S(` `)
//line web/template/page.qtpl:26
//line web/template/page.qtpl:24
qw422016.N().S(``)
//line web/template/page.qtpl:26
//line web/template/page.qtpl:24
qw422016.N().S(` `)
//line web/template/page.qtpl:24
p.BaseOf.StreamTitle(qw422016)
//line web/template/page.qtpl:25
} else {
//line web/template/page.qtpl:26
p.BaseOf.StreamTitle(qw422016)
//line web/template/page.qtpl:27
} else {
}
//line web/template/page.qtpl:28
p.BaseOf.StreamTitle(qw422016)
//line web/template/page.qtpl:29
}
//line web/template/page.qtpl:30
}
//line web/template/page.qtpl:30
//line web/template/page.qtpl:28
func (p Page) WriteTitle(qq422016 qtio422016.Writer) {
//line web/template/page.qtpl:30
//line web/template/page.qtpl:28
qw422016 := qt422016.AcquireWriter(qq422016)
//line web/template/page.qtpl:30
//line web/template/page.qtpl:28
p.StreamTitle(qw422016)
//line web/template/page.qtpl:30
//line web/template/page.qtpl:28
qt422016.ReleaseWriter(qw422016)
//line web/template/page.qtpl:30
//line web/template/page.qtpl:28
}
//line web/template/page.qtpl:30
//line web/template/page.qtpl:28
func (p Page) Title() string {
//line web/template/page.qtpl:30
//line web/template/page.qtpl:28
qb422016 := qt422016.AcquireByteBuffer()
//line web/template/page.qtpl:30
//line web/template/page.qtpl:28
p.WriteTitle(qb422016)
//line web/template/page.qtpl:30
//line web/template/page.qtpl:28
qs422016 := string(qb422016.B)
//line web/template/page.qtpl:30
//line web/template/page.qtpl:28
qt422016.ReleaseByteBuffer(qb422016)
//line web/template/page.qtpl:30
//line web/template/page.qtpl:28
return qs422016
//line web/template/page.qtpl:30
//line web/template/page.qtpl:28
}
//line web/template/page.qtpl:32
//line web/template/page.qtpl:30
func (p Page) StreamLang(qw422016 *qt422016.Writer) {
//line web/template/page.qtpl:31
if p.page.Language != language.Und {
//line web/template/page.qtpl:32
qw422016.E().S(p.page.Language.String())
//line web/template/page.qtpl:33
if p.language != language.Und {
//line web/template/page.qtpl:34
qw422016.E().S(p.language.String())
//line web/template/page.qtpl:35
} else {
//line web/template/page.qtpl:36
//line web/template/page.qtpl:34
p.BaseOf.StreamLang(qw422016)
//line web/template/page.qtpl:37
//line web/template/page.qtpl:35
}
//line web/template/page.qtpl:38
//line web/template/page.qtpl:36
}
//line web/template/page.qtpl:38
//line web/template/page.qtpl:36
func (p Page) WriteLang(qq422016 qtio422016.Writer) {
//line web/template/page.qtpl:38
//line web/template/page.qtpl:36
qw422016 := qt422016.AcquireWriter(qq422016)
//line web/template/page.qtpl:38
//line web/template/page.qtpl:36
p.StreamLang(qw422016)
//line web/template/page.qtpl:38
//line web/template/page.qtpl:36
qt422016.ReleaseWriter(qw422016)
//line web/template/page.qtpl:38
//line web/template/page.qtpl:36
}
//line web/template/page.qtpl:38
//line web/template/page.qtpl:36
func (p Page) Lang() string {
//line web/template/page.qtpl:38
//line web/template/page.qtpl:36
qb422016 := qt422016.AcquireByteBuffer()
//line web/template/page.qtpl:38
//line web/template/page.qtpl:36
p.WriteLang(qb422016)
//line web/template/page.qtpl:38
//line web/template/page.qtpl:36
qs422016 := string(qb422016.B)
//line web/template/page.qtpl:38
//line web/template/page.qtpl:36
qt422016.ReleaseByteBuffer(qb422016)
//line web/template/page.qtpl:38
//line web/template/page.qtpl:36
return qs422016
//line web/template/page.qtpl:38
//line web/template/page.qtpl:36
}
//line web/template/page.qtpl:40
//line web/template/page.qtpl:38
func (p Page) StreamBody(qw422016 *qt422016.Writer) {
//line web/template/page.qtpl:40
//line web/template/page.qtpl:38
qw422016.N().S(`<p>`)
//line web/template/page.qtpl:41
qw422016.E().Z(p.content)
//line web/template/page.qtpl:41
//line web/template/page.qtpl:39
qw422016.E().Z(p.page.Content)
//line web/template/page.qtpl:39
qw422016.N().S(`</p>`)
//line web/template/page.qtpl:42
//line web/template/page.qtpl:40
}
//line web/template/page.qtpl:42
//line web/template/page.qtpl:40
func (p Page) WriteBody(qq422016 qtio422016.Writer) {
//line web/template/page.qtpl:42
//line web/template/page.qtpl:40
qw422016 := qt422016.AcquireWriter(qq422016)
//line web/template/page.qtpl:42
//line web/template/page.qtpl:40
p.StreamBody(qw422016)
//line web/template/page.qtpl:42
//line web/template/page.qtpl:40
qt422016.ReleaseWriter(qw422016)
//line web/template/page.qtpl:42
//line web/template/page.qtpl:40
}
//line web/template/page.qtpl:42
//line web/template/page.qtpl:40
func (p Page) Body() string {
//line web/template/page.qtpl:42
//line web/template/page.qtpl:40
qb422016 := qt422016.AcquireByteBuffer()
//line web/template/page.qtpl:42
//line web/template/page.qtpl:40
p.WriteBody(qb422016)
//line web/template/page.qtpl:42
//line web/template/page.qtpl:40
qs422016 := string(qb422016.B)
//line web/template/page.qtpl:42
//line web/template/page.qtpl:40
qt422016.ReleaseByteBuffer(qb422016)
//line web/template/page.qtpl:42
//line web/template/page.qtpl:40
return qs422016
//line web/template/page.qtpl:42
//line web/template/page.qtpl:40
}