Merge branch 'feature/entry' into develop
/ docker (push) Successful in 1m8s Details

This commit is contained in:
Maxim Lebedev 2024-02-03 20:49:58 +06:00
commit 6aae1ffa48
Signed by: toby3d
GPG Key ID: 1F14E25B7C119FC5
30 changed files with 398 additions and 312 deletions

21
go.mod
View File

@ -5,24 +5,17 @@ go 1.21.3
require (
github.com/adrg/frontmatter v0.2.0
github.com/caarlos0/env/v10 v10.0.0
github.com/google/go-cmp v0.6.0
github.com/valyala/quicktemplate v1.7.0
)
require github.com/google/go-cmp v0.6.0
require github.com/yuin/goldmark v1.6.0
require github.com/yuin/goldmark-emoji v1.0.2
require golang.org/x/image v0.14.0
require (
github.com/BurntSushi/toml v1.3.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
github.com/yuin/goldmark v1.7.0
github.com/yuin/goldmark-emoji v1.0.2
golang.org/x/image v0.15.0
golang.org/x/text v0.14.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/text v0.14.0
gopkg.in/yaml.v2 v2.4.0 // indirect
)

8
go.sum
View File

@ -19,13 +19,13 @@ github.com/valyala/quicktemplate v1.7.0 h1:LUPTJmlVcb46OOUY3IeD9DojFpAVbsG+5WFTc
github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -18,10 +18,10 @@ import (
"source.toby3d.me/toby3d/home/internal/common"
"source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/entry"
pagefsrepo "source.toby3d.me/toby3d/home/internal/entry/repository/fs"
pageucase "source.toby3d.me/toby3d/home/internal/entry/usecase"
"source.toby3d.me/toby3d/home/internal/middleware"
"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"
resourcefsrepo "source.toby3d.me/toby3d/home/internal/resource/repository/fs"
resourceucase "source.toby3d.me/toby3d/home/internal/resource/usecase"
servercase "source.toby3d.me/toby3d/home/internal/server/usecase"
@ -40,11 +40,11 @@ type App struct {
func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
themeDir := os.DirFS(config.ThemeDir)
partialsDir, err := fs.Sub(themeDir, "partials")
if err != nil {
return nil, fmt.Errorf("cannot substitute into partials subdirectory: %w", err)
}
contentDir := os.DirFS(config.ContentDir)
resources := resourcefsrepo.NewFileServerResourceRepository(contentDir)
sites := sitefsrepo.NewFileSystemSiteRepository(contentDir)
@ -55,8 +55,8 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
resourcer := resourceucase.NewResourceUseCase(resources)
themes := themefsrepo.NewFileSystemThemeRepository(themeDir)
themer := themeucase.NewThemeUseCase(partialsDir, themes)
pages := pagefsrepo.NewFileSystemPageRepository(contentDir)
pager := pageucase.NewPageUseCase(pages, resources)
entries := pagefsrepo.NewFileSystemPageRepository(contentDir)
entrier := pageucase.NewEntryUseCase(entries, resources)
serverer := servercase.NewServerUseCase(sites)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// INFO(toby3d): any static file is public and unprotected by design, so it's safe to search it
@ -115,9 +115,9 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
return
}
p, err := pager.Do(r.Context(), lang, r.URL.Path)
e, err := entrier.Do(r.Context(), lang, r.URL.Path)
if err != nil {
if !errors.Is(err, page.ErrNotExist) {
if !errors.Is(err, entry.ErrNotExist) {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -158,14 +158,14 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
return
}
contentLanguage := make([]string, len(p.Translations))
for i := range p.Translations {
contentLanguage[i] = p.Translations[i].Language.Code()
contentLanguage := make([]string, len(e.Translations))
for i := range e.Translations {
contentLanguage[i] = e.Translations[i].Language.Code()
}
w.Header().Set(common.HeaderContentLanguage, strings.Join(contentLanguage, ", "))
template, err := themer.Do(r.Context(), s, p)
template, err := themer.Do(r.Context(), s, e)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -7,8 +7,10 @@ const (
)
const (
MIMETextHTML string = "text/html"
MIMETextHTMLCharsetUTF8 string = MIMETextHTML + "; " + charsetUTF8
MIMETextHTML string = "text/html"
MIMETextHTMLCharsetUTF8 string = MIMETextHTML + "; " + charsetUTF8
MIMETextPlain string = "text/plain"
MIMETextPlainCharsetUTF8 string = MIMETextPlain + "; " + charsetUTF8
)
const Und string = "und"

View File

@ -41,11 +41,15 @@ func NewPath(path string) Path {
parts := strings.Split(out.baseFileName, ".")
out.Language = NewLanguage(parts[len(parts)-1])
out.translationBaseName = strings.Join(parts[:len(parts)-1], ".")
out.contentBaseName = out.translationBaseName
if len(parts) == 1 {
out.translationBaseName = parts[0]
out.contentBaseName = filepath.Base(out.dir)
} else {
out.contentBaseName = out.translationBaseName
}
switch out.translationBaseName {
default:
out.contentBaseName = out.translationBaseName
case "_index", "index":
out.contentBaseName = filepath.Base(out.dir)
}
@ -62,6 +66,7 @@ func NewPath(path string) Path {
// /news/a.en.md => a.en
// /news/b/index.en.md => index.en
// /news/_index.en.md => _index.en
// /news/b/photo.jpg => photo
func (p Path) BaseFileName() string {
return p.baseFileName
}
@ -71,6 +76,7 @@ func (p Path) BaseFileName() string {
// /news/a.en.md => a
// /news/b/index.en.md => b
// /news/_index.en.md => news
// /news/b/photo.jpg => b
func (p Path) ContentBaseName() string {
return p.contentBaseName
}
@ -80,6 +86,7 @@ func (p Path) ContentBaseName() string {
// /news/a.en.md => news/
// /news/b/index.en.md => news/b/
// /news/_index.en.md => news/
// /news/b/photo.jpg => news/b/
func (p Path) Dir() string {
return p.dir
}
@ -87,6 +94,7 @@ func (p Path) Dir() string {
// Ext returns file extention:
//
// /news/b/index.en.md => md
// /news/b/photo.jpg => jpg
func (p Path) Ext() string {
return p.ext
}
@ -95,11 +103,12 @@ func (p Path) Filename() string {
return p.filename
}
// LogicalName returns fille file name in directory:
// LogicalName returns file name in directory:
//
// /news/a.en.md => a.en.md
// /news/b/index.en.md => index.en.md
// /news/_index.en.md => _index.en.md
// /news/b/photo.jpg => photo.jpg
func (p Path) LogicalName() string {
return p.logicalName
}
@ -113,6 +122,7 @@ func (p Path) Path() string {
// /news/a.en.md => a
// /news/b/index.en.md => index
// /news/_index.en.md => _index
// /news/b/photo.jpg => photo
func (p Path) TranslationBaseName() string {
return p.translationBaseName
}

View File

@ -11,6 +11,7 @@ var (
testRegularPath string = filepath.Join("news", "a.en.md")
testLeafPath string = filepath.Join("news", "b", "index.en.md")
testBranchPath string = filepath.Join("news", "_index.en.md")
testResource string = filepath.Join("news", "b", "photo.jpg")
)
func TestPath_BaseFileName(t *testing.T) {
@ -19,9 +20,10 @@ func TestPath_BaseFileName(t *testing.T) {
for name, tc := range map[string]struct {
input, expect string
}{
"regular": {testRegularPath, "a.en"},
"leaf": {testLeafPath, "index.en"},
"branch": {testBranchPath, "_index.en"},
"regular": {testRegularPath, "a.en"},
"leaf": {testLeafPath, "index.en"},
"branch": {testBranchPath, "_index.en"},
"resource": {testResource, "photo"},
} {
name, tc := name, tc
@ -41,9 +43,10 @@ func TestPath_ContentBaseName(t *testing.T) {
for name, tc := range map[string]struct {
input, expect string
}{
"regular": {testRegularPath, "a"},
"leaf": {testLeafPath, "b"},
"branch": {testBranchPath, "news"},
"regular": {testRegularPath, "a"},
"leaf": {testLeafPath, "b"},
"branch": {testBranchPath, "news"},
"resource": {testResource, "b"},
} {
name, tc := name, tc
@ -63,9 +66,10 @@ func TestPath_Dir(t *testing.T) {
for name, tc := range map[string]struct {
input, expect string
}{
"regular": {testRegularPath, "news/"},
"leaf": {testLeafPath, "news/b/"},
"branch": {testBranchPath, "news/"},
"regular": {testRegularPath, "news/"},
"leaf": {testLeafPath, "news/b/"},
"branch": {testBranchPath, "news/"},
"resource": {testResource, "news/b/"},
} {
name, tc := name, tc
@ -84,18 +88,21 @@ func TestPath_Ext(t *testing.T) {
const expect string = "md"
for name, input := range map[string]string{
"regular": testRegularPath,
"leaf": testLeafPath,
"branch": testBranchPath,
for name, tc := range map[string]struct {
input, expect string
}{
"regular": {testRegularPath, "md"},
"leaf": {testLeafPath, "md"},
"branch": {testBranchPath, "md"},
"resource": {testResource, "jpg"},
} {
name, input := name, input
name, tc := name, tc
t.Run(name, func(t *testing.T) {
t.Parallel()
if actual := domain.NewPath(input).Ext(); actual != expect {
t.Errorf("Ext() = '%s', want '%s'", actual, expect)
if actual := domain.NewPath(tc.input).Ext(); actual != tc.expect {
t.Errorf("Ext() = '%s', want '%s'", actual, tc.expect)
}
})
}
@ -129,9 +136,10 @@ func TestPath_LogicalName(t *testing.T) {
for name, tc := range map[string]struct {
input, expect string
}{
"regular": {testRegularPath, "a.en.md"},
"leaf": {testLeafPath, "index.en.md"},
"branch": {testBranchPath, "_index.en.md"},
"regular": {testRegularPath, "a.en.md"},
"leaf": {testLeafPath, "index.en.md"},
"branch": {testBranchPath, "_index.en.md"},
"resource": {testResource, "photo.jpg"},
} {
name, tc := name, tc
@ -151,9 +159,10 @@ func TestPath_TranslationBaseName(t *testing.T) {
for name, tc := range map[string]struct {
input, expect string
}{
"regular": {testRegularPath, "a"},
"leaf": {testLeafPath, "index"},
"branch": {testBranchPath, "_index"},
"regular": {testRegularPath, "a"},
"leaf": {testLeafPath, "index"},
"branch": {testBranchPath, "_index"},
"resource": {testResource, "photo"},
} {
name, tc := name, tc

View File

@ -15,15 +15,14 @@ import (
)
type Resource struct {
File Path
modTime time.Time
params map[string]any // TODO(toby3d): set from Page configuration
params map[string]any
File Path
mediaType MediaType
key string
name string // TODO(toby3d): set from Page configuration
title string // TODO(toby3d): set from Page configuration
name string
resourceType ResourceType
title string
image image.Config
}
@ -69,13 +68,13 @@ func (r Resource) MediaType() MediaType {
}
// Width returns width if current r is an image.
func (r Resource) Width() int {
return r.image.Width
func (r Resource) Width() uint {
return uint(r.image.Width)
}
// Height returns height if current r is an image.
func (r Resource) Height() int {
return r.image.Height
func (r Resource) Height() uint {
return uint(r.image.Height)
}
func (r Resource) ResourceType() ResourceType {

View File

@ -0,0 +1,18 @@
package entry
import (
"context"
"errors"
"source.toby3d.me/toby3d/home/internal/domain"
)
type Repository interface {
Get(ctx context.Context, lang domain.Language, path string) (*domain.Page, error)
// Stat checks for the existence of a page on the specified path without
// parsing its contents.
Stat(ctx context.Context, lang domain.Language, path string) (bool, error)
}
var ErrNotExist error = errors.New("entry not exists")

View File

@ -2,6 +2,7 @@ package fs
import (
"context"
"errors"
"fmt"
"io/fs"
@ -9,7 +10,7 @@ import (
"gopkg.in/yaml.v3"
"source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/page"
"source.toby3d.me/toby3d/home/internal/entry"
)
type (
@ -29,7 +30,7 @@ var FrontMatterFormats = []*frontmatter.Format{
frontmatter.NewFormat(`---`, `---`, yaml.Unmarshal),
}
func NewFileSystemPageRepository(dir fs.FS) page.Repository {
func NewFileSystemPageRepository(dir fs.FS) entry.Repository {
return &fileSystemPageRepository{
dir: dir,
}
@ -45,7 +46,7 @@ func (repo *fileSystemPageRepository) Get(ctx context.Context, lang domain.Langu
f, err := repo.dir.Open(target)
if err != nil {
return nil, fmt.Errorf("cannot open '%s' page file: %w", target, err)
return nil, fmt.Errorf("cannot open '%s' entry file: %w", target, err)
}
defer f.Close()
@ -53,7 +54,7 @@ func (repo *fileSystemPageRepository) Get(ctx context.Context, lang domain.Langu
Params: make(map[string]any),
}
if data.Content, err = frontmatter.Parse(f, data, FrontMatterFormats...); err != nil {
return nil, fmt.Errorf("cannot parse page content as FrontMatter: %w", err)
return nil, fmt.Errorf("cannot parse entry content as FrontMatter: %w", err)
}
return &domain.Page{
@ -67,3 +68,14 @@ func (repo *fileSystemPageRepository) Get(ctx context.Context, lang domain.Langu
Translations: make([]*domain.Page, 0),
}, nil
}
func (repo *fileSystemPageRepository) Stat(_ context.Context, l domain.Language, p string) (bool, error) {
ext := ".md"
if l != domain.LanguageUnd {
ext = "." + l.Lang() + ext
}
_, err := fs.Stat(repo.dir, p+ext)
return errors.Is(err, fs.ErrExist), nil
}

View File

@ -10,7 +10,7 @@ import (
"github.com/google/go-cmp/cmp"
"source.toby3d.me/toby3d/home/internal/domain"
repository "source.toby3d.me/toby3d/home/internal/page/repository/fs"
repository "source.toby3d.me/toby3d/home/internal/entry/repository/fs"
)
func TestGet(t *testing.T) {

View File

@ -0,0 +1,41 @@
package memory
import (
"context"
"errors"
"fmt"
"io/fs"
"sync"
"testing/fstest"
"source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/entry"
)
type memoryEntryRepository struct {
mutex *sync.RWMutex
files fstest.MapFS
}
func (repo *memoryEntryRepository) Get(_ context.Context, _ domain.Language, p string) (*domain.Page, error) {
f, err := repo.files.Open(p)
if err != nil {
return nil, fmt.Errorf("cannot get entry from memory: %w", err)
}
defer f.Close()
return nil, nil
}
func (repo *memoryEntryRepository) Stat(_ context.Context, l domain.Language, p string) (bool, error) {
_, err := fs.Stat(repo.files, p)
return errors.Is(err, fs.ErrExist), nil
}
func NewMemoryEntryRepository(files fstest.MapFS) entry.Repository {
return &memoryEntryRepository{
mutex: new(sync.RWMutex),
files: files,
}
}

View File

@ -1,8 +1,7 @@
package page
package entry
import (
"context"
"errors"
"source.toby3d.me/toby3d/home/internal/domain"
)
@ -10,5 +9,3 @@ import (
type UseCase interface {
Do(ctx context.Context, lang domain.Language, path string) (*domain.Page, error)
}
var ErrNotExist error = errors.New("page not exists")

View File

@ -0,0 +1,80 @@
package usecase
import (
"context"
"fmt"
"path"
"strings"
"source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/entry"
"source.toby3d.me/toby3d/home/internal/resource"
"source.toby3d.me/toby3d/home/internal/urlutil"
)
type entryUseCase struct {
entries entry.Repository
resources resource.Repository
}
func NewEntryUseCase(entries entry.Repository, resources resource.Repository) entry.UseCase {
return &entryUseCase{
entries: entries,
resources: resources,
}
}
func (ucase *entryUseCase) Do(ctx context.Context, lang domain.Language, p string) (*domain.Page, error) {
targets := make([]string, 0)
hasExt := path.Ext(p) != ""
head, tail := urlutil.ShiftPath(p)
if tail == "/" {
if head = strings.TrimSuffix(head, path.Ext(head)); head == "" {
head = "index"
}
targets = append(targets, head)
}
if head != "index" {
tail = strings.TrimSuffix(tail, path.Ext(tail))
if !strings.HasSuffix(tail, "/index") {
if hasExt {
targets = append([]string{path.Join(head, tail, "index")}, targets...)
} else {
targets = append(targets, path.Join(head, tail, "index"))
}
} else {
targets = append(targets, path.Join(head, tail))
}
}
for i := len(targets) - 1; 0 <= i; i-- {
result, err := ucase.entries.Get(ctx, lang, targets[i])
if err != nil {
continue
}
if result.Resources, _, err = ucase.resources.Fetch(ctx, result.File.Dir()+"*"); err != nil {
return result, nil
}
for _, res := range result.Resources.GetType(domain.ResourceTypePage) {
if res.File.TranslationBaseName() != result.File.TranslationBaseName() {
continue
}
translation, err := ucase.entries.Get(ctx, res.File.Language, targets[i])
if err != nil {
continue
}
result.Translations = append(result.Translations, translation)
}
return result, nil
}
return nil, fmt.Errorf("cannot find page on path '%s': %w", p, entry.ErrNotExist)
}

View File

@ -0,0 +1,70 @@
package usecase_test
import (
"context"
"path/filepath"
"testing"
"testing/fstest"
"github.com/google/go-cmp/cmp"
"source.toby3d.me/toby3d/home/internal/domain"
entryfsrepo "source.toby3d.me/toby3d/home/internal/entry/repository/fs"
"source.toby3d.me/toby3d/home/internal/entry/usecase"
resourcedummyrepo "source.toby3d.me/toby3d/home/internal/resource/repository/dummy"
)
func TestDo(t *testing.T) {
t.Parallel()
pages := entryfsrepo.NewFileSystemPageRepository(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("foo", "bar", "index.md"): &fstest.MapFile{Data: []byte(`foo/bar/index.md`)},
filepath.Join("foo", "bar.md"): &fstest.MapFile{Data: []byte(`foo/bar.md`)},
filepath.Join("index.md"): &fstest.MapFile{Data: []byte(`index.md`)},
})
ucase := usecase.NewEntryUseCase(pages, resourcedummyrepo.NewDummyResourceRepository())
for name, tc := range map[string]struct {
input string
expect []byte
}{
"root": {"/", []byte(`index.md`)},
"index": {"/index", []byte(`index.md`)},
"index-ext": {"/index.html", []byte(`index.md`)},
"file": {"/file", []byte(`file.md`)},
"file-slash": {"/file/", []byte(`file.md`)},
"file-ext": {"/file.html", []byte(`file.md`)},
"both-ext": {"/both.html", []byte(`both.md`)},
"folder": {"/folder", []byte(`folder/index.md`)},
"folder-slash": {"/folder/", []byte(`folder/index.md`)},
"folder-index": {"/folder/index", []byte(`folder/index.md`)},
"folder-ext": {"/folder/index.html", []byte(`folder/index.md`)},
"both": {"/both", []byte(`both/index.md`)},
"both-slash": {"/both/", []byte(`both/index.md`)},
"both-index": {"/both/index", []byte(`both/index.md`)},
"both-index-ext": {"/both/index.html", []byte(`both/index.md`)},
"sub-folder-index": {"/foo/bar/index", []byte(`foo/bar/index.md`)},
"sub-folder-ext": {"/foo/bar/index.html", []byte(`foo/bar/index.md`)},
"sub-folder-slash": {"/foo/bar/", []byte(`foo/bar/index.md`)},
} {
name, tc := name, tc
t.Run(name, func(t *testing.T) {
t.Parallel()
actual, err := ucase.Do(context.Background(), domain.LanguageUnd, tc.input)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(string(actual.Content), string(tc.expect)); diff != "" {
t.Error(diff)
}
})
}
}

View File

@ -1,11 +0,0 @@
package page
import (
"context"
"source.toby3d.me/toby3d/home/internal/domain"
)
type Repository interface {
Get(ctx context.Context, lang domain.Language, path string) (*domain.Page, error)
}

View File

@ -1,71 +0,0 @@
package usecase
import (
"context"
"fmt"
"path"
"source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/page"
"source.toby3d.me/toby3d/home/internal/resource"
"source.toby3d.me/toby3d/home/internal/urlutil"
)
type pageUseCase struct {
pages page.Repository
resources resource.Repository
}
func NewPageUseCase(pages page.Repository, resources resource.Repository) page.UseCase {
return &pageUseCase{
pages: pages,
resources: resources,
}
}
func (ucase *pageUseCase) Do(ctx context.Context, lang domain.Language, 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
}
if out.Resources, _, err = ucase.resources.Fetch(ctx, out.File.Dir()+"*"); err != nil {
return out, nil
}
for _, res := range out.Resources.GetType(domain.ResourceTypePage) {
if res.File.TranslationBaseName() != out.File.TranslationBaseName() {
continue
}
translation, err := ucase.pages.Get(ctx, res.File.Language, targets[i])
if err != nil {
continue
}
out.Translations = append(out.Translations, translation)
}
return out, nil
}
return nil, fmt.Errorf("cannot find page on path '%s': %w", p, page.ErrNotExist)
}

View File

@ -1,62 +0,0 @@
package usecase_test
import (
"context"
"path/filepath"
"testing"
"testing/fstest"
"github.com/google/go-cmp/cmp"
"source.toby3d.me/toby3d/home/internal/domain"
pagefsrepo "source.toby3d.me/toby3d/home/internal/page/repository/fs"
"source.toby3d.me/toby3d/home/internal/page/usecase"
"source.toby3d.me/toby3d/home/internal/resource"
)
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, resource.NewDummyRepository())
for name, tc := range map[string]struct {
input string
expect []byte
}{
"index": {input: "/", expect: []byte(`index.md`)},
"index-ext": {input: "/index.html", expect: []byte(`index.md`)},
"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(), domain.LanguageUnd, tc.input)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(string(actual.Content), string(tc.expect)); diff != "" {
t.Error(diff)
}
})
}
}

View File

@ -6,26 +6,10 @@ import (
"source.toby3d.me/toby3d/home/internal/domain"
)
type (
Repository interface {
// Get returns Resource on path if exists.
Get(ctx context.Context, path string) (*domain.Resource, error)
type Repository interface {
// Get returns Resource on path if exists.
Get(ctx context.Context, path string) (*domain.Resource, error)
// Fetch returns all resources from dir recursevly.
Fetch(ctx context.Context, pattern string) (domain.Resources, int, error)
}
dummyRepository struct{}
)
func NewDummyRepository() dummyRepository {
return dummyRepository{}
}
func (dummyRepository) Get(ctx context.Context, path string) (*domain.Resource, error) {
return nil, nil
}
func (dummyRepository) Fetch(ctx context.Context, pattern string) (domain.Resources, int, error) {
return nil, 0, nil
// Fetch returns all resources from dir recursevly.
Fetch(ctx context.Context, pattern string) (domain.Resources, int, error)
}

View File

@ -0,0 +1,21 @@
package dummy
import (
"context"
"source.toby3d.me/toby3d/home/internal/domain"
)
type dummyResourceRepository struct{}
func NewDummyResourceRepository() dummyResourceRepository {
return dummyResourceRepository{}
}
func (dummyResourceRepository) Get(_ context.Context, _ string) (*domain.Resource, error) {
return nil, nil
}
func (dummyResourceRepository) Fetch(_ context.Context, _ string) (domain.Resources, int, error) {
return nil, 0, nil
}

View File

@ -3,14 +3,8 @@ package fs
import (
"context"
"fmt"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io/fs"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/webp"
"source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/resource"
)

View File

@ -5,17 +5,11 @@ import (
"context"
"errors"
"fmt"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"io"
"io/fs"
"os"
"path/filepath"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/webp"
"source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/static"
)

View File

@ -8,7 +8,7 @@ goldmark
> A Markdown parser written in Go. Easy to extend, standards-compliant, well-structured.
goldmark is compliant with CommonMark 0.30.
goldmark is compliant with CommonMark 0.31.2.
Motivation
----------------------
@ -260,7 +260,7 @@ You can override autolinking patterns via options.
| Functional option | Type | Description |
| ----------------- | ---- | ----------- |
| `extension.WithLinkifyAllowedProtocols` | `[][]byte` | List of allowed protocols such as `[][]byte{ []byte("http:") }` |
| `extension.WithLinkifyAllowedProtocols` | `[][]byte \| []string` | List of allowed protocols such as `[]string{ "http:" }` |
| `extension.WithLinkifyURLRegexp` | `*regexp.Regexp` | Regexp that defines URLs, including protocols |
| `extension.WithLinkifyWWWRegexp` | `*regexp.Regexp` | Regexp that defines URL starting with `www.`. This pattern corresponds to [the extended www autolink](https://github.github.com/gfm/#extended-www-autolink) |
| `extension.WithLinkifyEmailRegexp` | `*regexp.Regexp` | Regexp that defines email addresses` |
@ -277,9 +277,9 @@ markdown := goldmark.New(
),
goldmark.WithExtensions(
extension.NewLinkify(
extension.WithLinkifyAllowedProtocols([][]byte{
[]byte("http:"),
[]byte("https:"),
extension.WithLinkifyAllowedProtocols([]string{
"http:",
"https:",
}),
extension.WithLinkifyURLRegexp(
xurls.Strict,
@ -297,13 +297,13 @@ This extension has some options:
| Functional option | Type | Description |
| ----------------- | ---- | ----------- |
| `extension.WithFootnoteIDPrefix` | `[]byte` | a prefix for the id attributes.|
| `extension.WithFootnoteIDPrefix` | `[]byte \| string` | a prefix for the id attributes.|
| `extension.WithFootnoteIDPrefixFunction` | `func(gast.Node) []byte` | a function that determines the id attribute for given Node.|
| `extension.WithFootnoteLinkTitle` | `[]byte` | an optional title attribute for footnote links.|
| `extension.WithFootnoteBacklinkTitle` | `[]byte` | an optional title attribute for footnote backlinks. |
| `extension.WithFootnoteLinkClass` | `[]byte` | a class for footnote links. This defaults to `footnote-ref`. |
| `extension.WithFootnoteBacklinkClass` | `[]byte` | a class for footnote backlinks. This defaults to `footnote-backref`. |
| `extension.WithFootnoteBacklinkHTML` | `[]byte` | a class for footnote backlinks. This defaults to `&#x21a9;&#xfe0e;`. |
| `extension.WithFootnoteLinkTitle` | `[]byte \| string` | an optional title attribute for footnote links.|
| `extension.WithFootnoteBacklinkTitle` | `[]byte \| string` | an optional title attribute for footnote backlinks. |
| `extension.WithFootnoteLinkClass` | `[]byte \| string` | a class for footnote links. This defaults to `footnote-ref`. |
| `extension.WithFootnoteBacklinkClass` | `[]byte \| string` | a class for footnote backlinks. This defaults to `footnote-backref`. |
| `extension.WithFootnoteBacklinkHTML` | `[]byte \| string` | a class for footnote backlinks. This defaults to `&#x21a9;&#xfe0e;`. |
Some options can have special substitutions. Occurrences of “^^” in the string will be replaced by the corresponding footnote number in the HTML output. Occurrences of “%%” will be replaced by a number for the reference (footnotes can have multiple references).
@ -319,7 +319,7 @@ for _, path := range files {
markdown := goldmark.New(
goldmark.WithExtensions(
NewFootnote(
WithFootnoteIDPrefix([]byte(path)),
WithFootnoteIDPrefix(path),
),
),
)
@ -379,7 +379,7 @@ This extension provides additional options for CJK users.
| Functional option | Type | Description |
| ----------------- | ---- | ----------- |
| `extension.WithEastAsianLineBreaks` | `...extension.EastAsianLineBreaksStyle` | Soft line breaks are rendered as a newline. Some asian users will see it as an unnecessary space. With this option, soft line breaks between east asian wide characters will be ignored. |
| `extension.WithEastAsianLineBreaks` | `...extension.EastAsianLineBreaksStyle` | Soft line breaks are rendered as a newline. Some asian users will see it as an unnecessary space. With this option, soft line breaks between east asian wide characters will be ignored. This defaults to `EastAsianLineBreaksStyleSimple`. |
| `extension.WithEscapedSpace` | `-` | Without spaces around an emphasis started with east asian punctuations, it is not interpreted as an emphasis(as defined in CommonMark spec). With this option, you can avoid this inconvenient behavior by putting 'not rendered' spaces around an emphasis like `太郎は\ **「こんにちわ」**\ といった`. |
#### Styles of Line Breaking
@ -467,6 +467,7 @@ As you can see, goldmark's performance is on par with cmark's.
Extensions
--------------------
### List of extensions
- [goldmark-meta](https://github.com/yuin/goldmark-meta): A YAML metadata
extension for the goldmark Markdown parser.
@ -490,6 +491,13 @@ Extensions
- [goldmark-d2](https://github.com/FurqanSoftware/goldmark-d2): Adds support for [D2](https://d2lang.com/) diagrams.
- [goldmark-katex](https://github.com/FurqanSoftware/goldmark-katex): Adds support for [KaTeX](https://katex.org/) math and equations.
- [goldmark-img64](https://github.com/tenkoh/goldmark-img64): Adds support for embedding images into the document as DataURL (base64 encoded).
- [goldmark-enclave](https://github.com/quail-ink/goldmark-enclave): Adds support for embedding youtube/bilibili video, X's [oembed tweet](https://publish.twitter.com/), [tradingview](https://www.tradingview.com/widget/)'s chart, [quail](https://quail.ink)'s widget into the document.
- [goldmark-wiki-table](https://github.com/movsb/goldmark-wiki-table): Adds support for embedding Wiki Tables.
### Loading extensions at runtime
[goldmark-dynamic](https://github.com/yuin/goldmark-dynamic) allows you to write a goldmark extension in Lua and load it at runtime without re-compilation.
Please refer to [goldmark-dynamic](https://github.com/yuin/goldmark-dynamic) for details.
goldmark internal(for extension developers)

View File

@ -382,8 +382,8 @@ func (o *withFootnoteIDPrefix) SetFootnoteOption(c *FootnoteConfig) {
}
// WithFootnoteIDPrefix is a functional option that is a prefix for the id attributes generated by footnotes.
func WithFootnoteIDPrefix(a []byte) FootnoteOption {
return &withFootnoteIDPrefix{a}
func WithFootnoteIDPrefix[T []byte | string](a T) FootnoteOption {
return &withFootnoteIDPrefix{[]byte(a)}
}
const optFootnoteIDPrefixFunction renderer.OptionName = "FootnoteIDPrefixFunction"
@ -420,8 +420,8 @@ func (o *withFootnoteLinkTitle) SetFootnoteOption(c *FootnoteConfig) {
}
// WithFootnoteLinkTitle is a functional option that is an optional title attribute for footnote links.
func WithFootnoteLinkTitle(a []byte) FootnoteOption {
return &withFootnoteLinkTitle{a}
func WithFootnoteLinkTitle[T []byte | string](a T) FootnoteOption {
return &withFootnoteLinkTitle{[]byte(a)}
}
const optFootnoteBacklinkTitle renderer.OptionName = "FootnoteBacklinkTitle"
@ -439,8 +439,8 @@ func (o *withFootnoteBacklinkTitle) SetFootnoteOption(c *FootnoteConfig) {
}
// WithFootnoteBacklinkTitle is a functional option that is an optional title attribute for footnote backlinks.
func WithFootnoteBacklinkTitle(a []byte) FootnoteOption {
return &withFootnoteBacklinkTitle{a}
func WithFootnoteBacklinkTitle[T []byte | string](a T) FootnoteOption {
return &withFootnoteBacklinkTitle{[]byte(a)}
}
const optFootnoteLinkClass renderer.OptionName = "FootnoteLinkClass"
@ -458,8 +458,8 @@ func (o *withFootnoteLinkClass) SetFootnoteOption(c *FootnoteConfig) {
}
// WithFootnoteLinkClass is a functional option that is a class for footnote links.
func WithFootnoteLinkClass(a []byte) FootnoteOption {
return &withFootnoteLinkClass{a}
func WithFootnoteLinkClass[T []byte | string](a T) FootnoteOption {
return &withFootnoteLinkClass{[]byte(a)}
}
const optFootnoteBacklinkClass renderer.OptionName = "FootnoteBacklinkClass"
@ -477,8 +477,8 @@ func (o *withFootnoteBacklinkClass) SetFootnoteOption(c *FootnoteConfig) {
}
// WithFootnoteBacklinkClass is a functional option that is a class for footnote backlinks.
func WithFootnoteBacklinkClass(a []byte) FootnoteOption {
return &withFootnoteBacklinkClass{a}
func WithFootnoteBacklinkClass[T []byte | string](a T) FootnoteOption {
return &withFootnoteBacklinkClass{[]byte(a)}
}
const optFootnoteBacklinkHTML renderer.OptionName = "FootnoteBacklinkHTML"
@ -496,8 +496,8 @@ func (o *withFootnoteBacklinkHTML) SetFootnoteOption(c *FootnoteConfig) {
}
// WithFootnoteBacklinkHTML is an HTML content for footnote backlinks.
func WithFootnoteBacklinkHTML(a []byte) FootnoteOption {
return &withFootnoteBacklinkHTML{a}
func WithFootnoteBacklinkHTML[T []byte | string](a T) FootnoteOption {
return &withFootnoteBacklinkHTML{[]byte(a)}
}
// FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that

View File

@ -66,10 +66,12 @@ func (o *withLinkifyAllowedProtocols) SetLinkifyOption(p *LinkifyConfig) {
// WithLinkifyAllowedProtocols is a functional option that specify allowed
// protocols in autolinks. Each protocol must end with ':' like
// 'http:' .
func WithLinkifyAllowedProtocols(value [][]byte) LinkifyOption {
return &withLinkifyAllowedProtocols{
value: value,
func WithLinkifyAllowedProtocols[T []byte | string](value []T) LinkifyOption {
opt := &withLinkifyAllowedProtocols{}
for _, v := range value {
opt.value = append(opt.value, []byte(v))
}
return opt
}
type withLinkifyURLRegexp struct {

View File

@ -115,10 +115,10 @@ func (o *withTypographicSubstitutions) SetTypographerOption(p *TypographerConfig
// WithTypographicSubstitutions is a functional otpion that specify replacement text
// for punctuations.
func WithTypographicSubstitutions(values map[TypographicPunctuation][]byte) TypographerOption {
func WithTypographicSubstitutions[T []byte | string](values map[TypographicPunctuation]T) TypographerOption {
replacements := newDefaultSubstitutions()
for k, v := range values {
replacements[k] = v
replacements[k] = []byte(v)
}
return &withTypographicSubstitutions{replacements}

View File

@ -61,8 +61,8 @@ var allowedBlockTags = map[string]bool{
"option": true,
"p": true,
"param": true,
"search": true,
"section": true,
"source": true,
"summary": true,
"table": true,
"tbody": true,

View File

@ -58,47 +58,38 @@ var closeProcessingInstruction = []byte("?>")
var openCDATA = []byte("<![CDATA[")
var closeCDATA = []byte("]]>")
var closeDecl = []byte(">")
var emptyComment = []byte("<!---->")
var invalidComment1 = []byte("<!-->")
var invalidComment2 = []byte("<!--->")
var emptyComment1 = []byte("<!-->")
var emptyComment2 = []byte("<!--->")
var openComment = []byte("<!--")
var closeComment = []byte("-->")
var doubleHyphen = []byte("--")
func (s *rawHTMLParser) parseComment(block text.Reader, pc Context) ast.Node {
savedLine, savedSegment := block.Position()
node := ast.NewRawHTML()
line, segment := block.PeekLine()
if bytes.HasPrefix(line, emptyComment) {
node.Segments.Append(segment.WithStop(segment.Start + len(emptyComment)))
block.Advance(len(emptyComment))
if bytes.HasPrefix(line, emptyComment1) {
node.Segments.Append(segment.WithStop(segment.Start + len(emptyComment1)))
block.Advance(len(emptyComment1))
return node
}
if bytes.HasPrefix(line, invalidComment1) || bytes.HasPrefix(line, invalidComment2) {
return nil
if bytes.HasPrefix(line, emptyComment2) {
node.Segments.Append(segment.WithStop(segment.Start + len(emptyComment2)))
block.Advance(len(emptyComment2))
return node
}
offset := len(openComment)
line = line[offset:]
for {
hindex := bytes.Index(line, doubleHyphen)
if hindex > -1 {
hindex += offset
}
index := bytes.Index(line, closeComment) + offset
if index > -1 && hindex == index {
if index == 0 || len(line) < 2 || line[index-offset-1] != '-' {
node.Segments.Append(segment.WithStop(segment.Start + index + len(closeComment)))
block.Advance(index + len(closeComment))
return node
}
}
if hindex > 0 {
break
index := bytes.Index(line, closeComment)
if index > -1 {
node.Segments.Append(segment.WithStop(segment.Start + offset + index + len(closeComment)))
block.Advance(offset + index + len(closeComment))
return node
}
offset = 0
node.Segments.Append(segment)
block.AdvanceLine()
line, segment = block.PeekLine()
offset = 0
if line == nil {
break
}

View File

@ -808,7 +808,7 @@ func IsPunct(c byte) bool {
// IsPunctRune returns true if the given rune is a punctuation, otherwise false.
func IsPunctRune(r rune) bool {
return int32(r) <= 256 && IsPunct(byte(r)) || unicode.IsPunct(r)
return unicode.IsSymbol(r) || unicode.IsPunct(r)
}
// IsSpace returns true if the given character is a space, otherwise false.

View File

@ -39,6 +39,7 @@ func decode(r io.Reader, configOnly bool) (image.Image, image.Config, error) {
alpha []byte
alphaStride int
wantAlpha bool
seenVP8X bool
widthMinusOne uint32
heightMinusOne uint32
buf [10]byte
@ -113,6 +114,10 @@ func decode(r io.Reader, configOnly bool) (image.Image, image.Config, error) {
return m, image.Config{}, err
case fccVP8X:
if seenVP8X {
return nil, image.Config{}, errInvalidFormat
}
seenVP8X = true
if chunkLen != 10 {
return nil, image.Config{}, errInvalidFormat
}

6
vendor/modules.txt vendored
View File

@ -21,8 +21,8 @@ github.com/valyala/bytebufferpool
# github.com/valyala/quicktemplate v1.7.0
## explicit; go 1.11
github.com/valyala/quicktemplate
# github.com/yuin/goldmark v1.6.0
## explicit; go 1.18
# github.com/yuin/goldmark v1.7.0
## explicit; go 1.19
github.com/yuin/goldmark
github.com/yuin/goldmark/ast
github.com/yuin/goldmark/extension
@ -37,7 +37,7 @@ github.com/yuin/goldmark/util
github.com/yuin/goldmark-emoji
github.com/yuin/goldmark-emoji/ast
github.com/yuin/goldmark-emoji/definition
# golang.org/x/image v0.14.0
# golang.org/x/image v0.15.0
## explicit; go 1.18
golang.org/x/image/bmp
golang.org/x/image/riff