Compare commits
8 Commits
715445ee5d
...
6aae1ffa48
Author | SHA1 | Date |
---|---|---|
Maxim Lebedev | 6aae1ffa48 | |
Maxim Lebedev | c7ecb8dbaf | |
Maxim Lebedev | e18e265d1a | |
Maxim Lebedev | 677ae81a33 | |
Maxim Lebedev | 16491e58aa | |
Maxim Lebedev | 542348f635 | |
Maxim Lebedev | 4026fb9192 | |
Maxim Lebedev | b046f13098 |
21
go.mod
21
go.mod
|
@ -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
8
go.sum
|
@ -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=
|
||||
|
|
|
@ -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,9 +55,9 @@ 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)
|
||||
serverer := servercase.NewServerUseCase()
|
||||
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
|
||||
// first before deep down to any page or it's resource which might be secured by middleware or
|
||||
|
@ -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)
|
||||
|
||||
|
@ -180,11 +180,9 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
|
|||
chain := middleware.Chain{
|
||||
middleware.LogFmt(),
|
||||
middleware.Redirect(middleware.RedirectConfig{
|
||||
Siter: siter,
|
||||
Serverer: serverer,
|
||||
}),
|
||||
middleware.Header(middleware.HeaderConfig{
|
||||
Siter: siter,
|
||||
Serverer: serverer,
|
||||
}),
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
|
@ -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
|
||||
}
|
|
@ -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) {
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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")
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -5,13 +5,11 @@ import (
|
|||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/server"
|
||||
"source.toby3d.me/toby3d/home/internal/site"
|
||||
"source.toby3d.me/toby3d/home/internal/urlutil"
|
||||
)
|
||||
|
||||
type HeaderConfig struct {
|
||||
Skipper Skipper
|
||||
Siter site.UseCase
|
||||
Serverer server.UseCase
|
||||
}
|
||||
|
||||
|
@ -20,10 +18,6 @@ func Header(config HeaderConfig) Interceptor {
|
|||
config.Skipper = DefaultSkipper
|
||||
}
|
||||
|
||||
if config.Siter == nil {
|
||||
panic("middleware: header: Siter is nil")
|
||||
}
|
||||
|
||||
if config.Serverer == nil {
|
||||
panic("middleware: header: Serverer is nil")
|
||||
}
|
||||
|
@ -42,14 +36,7 @@ func Header(config HeaderConfig) Interceptor {
|
|||
}
|
||||
}
|
||||
|
||||
site, err := config.Siter.Do(r.Context(), lang)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
server, err := config.Serverer.Do(r.Context(), *site)
|
||||
server, err := config.Serverer.Do(r.Context(), lang)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
|
|
|
@ -5,14 +5,12 @@ import (
|
|||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/server"
|
||||
"source.toby3d.me/toby3d/home/internal/site"
|
||||
"source.toby3d.me/toby3d/home/internal/urlutil"
|
||||
)
|
||||
|
||||
type (
|
||||
RedirectConfig struct {
|
||||
Skipper Skipper
|
||||
Siter site.UseCase
|
||||
Serverer server.UseCase
|
||||
}
|
||||
|
||||
|
@ -28,10 +26,6 @@ func Redirect(config RedirectConfig) Interceptor {
|
|||
config.Skipper = DefaultSkipper
|
||||
}
|
||||
|
||||
if config.Siter == nil {
|
||||
panic("middleware: redirect: Siter is nil")
|
||||
}
|
||||
|
||||
if config.Serverer == nil {
|
||||
panic("middleware: redirect: Serverer is nil")
|
||||
}
|
||||
|
@ -50,14 +44,7 @@ func Redirect(config RedirectConfig) Interceptor {
|
|||
}
|
||||
}
|
||||
|
||||
site, err := config.Siter.Do(r.Context(), lang)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
server, err := config.Serverer.Do(r.Context(), *site)
|
||||
server, err := config.Serverer.Do(r.Context(), lang)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
)
|
||||
|
||||
type UseCase interface {
|
||||
Do(ctx context.Context, site domain.Site) (*domain.Server, error)
|
||||
Do(ctx context.Context, lang domain.Language) (*domain.Server, error)
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
|
@ -2,21 +2,31 @@ package usecase
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/server"
|
||||
"source.toby3d.me/toby3d/home/internal/site"
|
||||
)
|
||||
|
||||
type serverUseCase struct{}
|
||||
|
||||
func NewServerUseCase() server.UseCase {
|
||||
return serverUseCase{}
|
||||
type serverUseCase struct {
|
||||
sites site.Repository
|
||||
}
|
||||
|
||||
func (serverUseCase) Do(ctx context.Context, site domain.Site) (*domain.Server, error) {
|
||||
out := domain.NewServer()
|
||||
func NewServerUseCase(sites site.Repository) server.UseCase {
|
||||
return &serverUseCase{
|
||||
sites: sites,
|
||||
}
|
||||
}
|
||||
|
||||
func (ucase *serverUseCase) Do(ctx context.Context, lang domain.Language) (*domain.Server, error) {
|
||||
site, err := ucase.sites.Get(ctx, domain.LanguageUnd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot find base site data: %w", err)
|
||||
}
|
||||
|
||||
out := domain.NewServer()
|
||||
if site.Params == nil {
|
||||
return out, nil
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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 `↩︎`. |
|
||||
| `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 `↩︎`. |
|
||||
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue