Initial commit
This commit is contained in:
commit
77d3ff4852
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
build/
|
||||||
10
.idea/.gitignore
vendored
Normal file
10
.idea/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
11
.idea/go.imports.xml
Normal file
11
.idea/go.imports.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GoImports">
|
||||||
|
<option name="excludedPackages">
|
||||||
|
<array>
|
||||||
|
<option value="github.com/pkg/errors" />
|
||||||
|
<option value="golang.org/x/net/context" />
|
||||||
|
</array>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/weiro.iml" filepath="$PROJECT_DIR$/.idea/weiro.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
9
.idea/weiro.iml
Normal file
9
.idea/weiro.iml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
11
go.mod
Normal file
11
go.mod
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
module lmika.dev/lmika/weiro
|
||||||
|
|
||||||
|
go 1.24.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
|
github.com/yuin/goldmark v1.7.16 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
11
go.sum
Normal file
11
go.sum
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||||
|
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
32
main.go
Normal file
32
main.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"lmika.dev/lmika/weiro/providers/sitebuilder"
|
||||||
|
"lmika.dev/lmika/weiro/providers/sitereader"
|
||||||
|
"lmika.dev/lmika/weiro/site_templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
sr := sitereader.New(os.DirFS("build/test-site"))
|
||||||
|
sb, err := sitebuilder.New("build/out", sitebuilder.Options{
|
||||||
|
BasePosts: "/posts",
|
||||||
|
TemplatesFS: site_templates.FS,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
site, err := sr.ReadSite()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sb.BuildSite(site); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Done")
|
||||||
|
}
|
||||||
27
models/sites.go
Normal file
27
models/sites.go
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Site struct {
|
||||||
|
Meta SiteMeta
|
||||||
|
Posts []*Post
|
||||||
|
}
|
||||||
|
|
||||||
|
type SiteMeta struct {
|
||||||
|
Title string
|
||||||
|
Tagline string
|
||||||
|
BaseURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostMeta struct {
|
||||||
|
ID string `yaml:"id"`
|
||||||
|
Title string `yaml:"title"`
|
||||||
|
Date time.Time `yaml:"date"`
|
||||||
|
Tags []string `yaml:"tags"`
|
||||||
|
Slug string `yaml:"slug"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Post struct {
|
||||||
|
Meta PostMeta
|
||||||
|
Content string
|
||||||
|
}
|
||||||
159
providers/sitebuilder/builder.go
Normal file
159
providers/sitebuilder/builder.go
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
package sitebuilder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
"github.com/yuin/goldmark/extension"
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
"github.com/yuin/goldmark/renderer/html"
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Builder struct {
|
||||||
|
site models.Site
|
||||||
|
gmMarkdown goldmark.Markdown
|
||||||
|
opts Options
|
||||||
|
tmpls *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(site models.Site, opts Options) (*Builder, error) {
|
||||||
|
tmpls, err := template.New("").
|
||||||
|
Funcs(templateFns(site, opts)).
|
||||||
|
ParseFS(opts.TemplatesFS, tmplNamePostSingle, tmplNamePostList, tmplNameLayoutMain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Builder{
|
||||||
|
site: site,
|
||||||
|
opts: opts,
|
||||||
|
tmpls: tmpls,
|
||||||
|
gmMarkdown: goldmark.New(
|
||||||
|
goldmark.WithExtensions(extension.GFM),
|
||||||
|
goldmark.WithParserOptions(
|
||||||
|
parser.WithAutoHeadingID(),
|
||||||
|
),
|
||||||
|
goldmark.WithRendererOptions(
|
||||||
|
html.WithHardWraps(),
|
||||||
|
html.WithUnsafe(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) BuildSite(outDir string) error {
|
||||||
|
buildCtx := buildContext{outDir: outDir}
|
||||||
|
|
||||||
|
if err := os.RemoveAll(outDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, post := range b.site.Posts {
|
||||||
|
if err := b.writePost(buildCtx, post); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.renderPostList(buildCtx, b.site.Posts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) renderPostList(ctx buildContext, postList []*models.Post) error {
|
||||||
|
// TODO: paging
|
||||||
|
postCopy := make([]*models.Post, len(postList))
|
||||||
|
copy(postCopy, postList)
|
||||||
|
|
||||||
|
sort.Slice(postCopy, func(i, j int) bool {
|
||||||
|
return postCopy[i].Meta.Date.After(postCopy[j].Meta.Date)
|
||||||
|
})
|
||||||
|
|
||||||
|
pl := postListData{}
|
||||||
|
for _, post := range postCopy {
|
||||||
|
rp, err := b.renderPost(post)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pl.Posts = append(pl.Posts, rp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.createAtPath(ctx, "", func(f io.Writer) error {
|
||||||
|
return b.renderTemplate(f, tmplNamePostList, pl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) renderPost(post *models.Post) (postSingleData, error) {
|
||||||
|
postPath := post.Meta.Slug
|
||||||
|
if b.opts.BasePosts != "" {
|
||||||
|
postPath = filepath.Join(b.opts.BasePosts, strings.TrimPrefix(postPath, "/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var md bytes.Buffer
|
||||||
|
if err := b.gmMarkdown.Convert([]byte(post.Content), &md); err != nil {
|
||||||
|
return postSingleData{}, fmt.Errorf("failed to write post %s: %w", post.Meta.Slug, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return postSingleData{
|
||||||
|
Path: postPath,
|
||||||
|
Meta: post.Meta,
|
||||||
|
HTML: template.HTML(md.String()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) writePost(ctx buildContext, post *models.Post) error {
|
||||||
|
rp, err := b.renderPost(post)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.createAtPath(ctx, rp.Path, func(f io.Writer) error {
|
||||||
|
return b.renderTemplate(f, tmplNamePostSingle, rp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) createAtPath(ctx buildContext, path string, fn func(f io.Writer) error) error {
|
||||||
|
outFile := filepath.Join(ctx.outDir, strings.TrimPrefix(path, "/"))
|
||||||
|
if filepath.Ext(outFile) == "" {
|
||||||
|
outFile = filepath.Join(outFile, "index.html")
|
||||||
|
}
|
||||||
|
log.Printf("Writing %s\n", outFile)
|
||||||
|
|
||||||
|
// Render it within the template
|
||||||
|
if err := os.MkdirAll(filepath.Dir(outFile), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(outFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
return fn(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) renderTemplate(w io.Writer, name string, data interface{}) error {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := b.tmpls.ExecuteTemplate(&buf, name, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.tmpls.ExecuteTemplate(w, tmplNameLayoutMain, layoutData{
|
||||||
|
Body: template.HTML(buf.String()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type buildContext struct {
|
||||||
|
outDir string
|
||||||
|
}
|
||||||
67
providers/sitebuilder/builder_test.go
Normal file
67
providers/sitebuilder/builder_test.go
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
package sitebuilder_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
"lmika.dev/lmika/weiro/providers/sitebuilder"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuilder_BuildSite(t *testing.T) {
|
||||||
|
t.Run("build site", func(t *testing.T) {
|
||||||
|
tmpls := fstest.MapFS{
|
||||||
|
"posts_single.html": {Data: []byte(`{{ .HTML }}`)},
|
||||||
|
"posts_list.html": {Data: []byte(`{{ range .Posts}}<a href="{{url_abs .Path}}">{{.Meta.Title}}</a>,{{ end }}`)},
|
||||||
|
"layout_main.html": {Data: []byte(`{{ .Body }}`)},
|
||||||
|
}
|
||||||
|
|
||||||
|
site := models.Site{
|
||||||
|
Meta: models.SiteMeta{
|
||||||
|
BaseURL: "https://example.com",
|
||||||
|
},
|
||||||
|
Posts: []*models.Post{
|
||||||
|
{
|
||||||
|
Meta: models.PostMeta{
|
||||||
|
Title: "Test Post",
|
||||||
|
Slug: "/2026/02/18/test-post",
|
||||||
|
},
|
||||||
|
Content: "This is a test post",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Meta: models.PostMeta{
|
||||||
|
Title: "Another Post",
|
||||||
|
Slug: "/2026/02/20/another-post",
|
||||||
|
},
|
||||||
|
Content: "This is **another** test post",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
wantFiles := map[string]string{
|
||||||
|
"2026/02/18/test-post/index.html": "<p>This is a test post</p>\n",
|
||||||
|
"2026/02/20/another-post/index.html": "<p>This is <strong>another</strong> test post</p>\n",
|
||||||
|
"index.html": "<a href=\"https://example.com/2026/02/18/test-post\">Test Post</a>,<a href=\"https://example.com/2026/02/20/another-post\">Another Post</a>,",
|
||||||
|
}
|
||||||
|
|
||||||
|
outDir := t.TempDir()
|
||||||
|
|
||||||
|
b, err := sitebuilder.New(site, sitebuilder.Options{
|
||||||
|
TemplatesFS: tmpls,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = b.BuildSite(outDir)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
for file, content := range wantFiles {
|
||||||
|
filePath := filepath.Join(outDir, file)
|
||||||
|
fileContent, err := os.ReadFile(filePath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, content, string(fileContent))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
34
providers/sitebuilder/tmplfns.go
Normal file
34
providers/sitebuilder/tmplfns.go
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
package sitebuilder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func templateFns(site models.Site, opts Options) template.FuncMap {
|
||||||
|
return template.FuncMap{
|
||||||
|
"url_abs": func(basePath string) (string, error) {
|
||||||
|
if site.Meta.BaseURL == "" {
|
||||||
|
return basePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pu, err := url.Parse(site.Meta.BaseURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
pu.Path = filepath.Join(pu.Path, basePath)
|
||||||
|
return pu.String(), nil
|
||||||
|
},
|
||||||
|
"format_date": func(date time.Time) string {
|
||||||
|
loc := opts.RenderTZ
|
||||||
|
if loc == nil {
|
||||||
|
loc = time.Local
|
||||||
|
}
|
||||||
|
return date.In(loc).Format("02 Jan 2006")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
48
providers/sitebuilder/tmpls.go
Normal file
48
providers/sitebuilder/tmpls.go
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
package sitebuilder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"io/fs"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Template names
|
||||||
|
|
||||||
|
// tmplNamePostSingle is the template for single post (postSingleData)
|
||||||
|
tmplNamePostSingle = "posts_single.html"
|
||||||
|
|
||||||
|
// tmplNamePostList is the template for list of posts (postListData)
|
||||||
|
tmplNamePostList = "posts_list.html"
|
||||||
|
|
||||||
|
// tmplNameLayoutMain is the template for the main layout (layoutMainData)
|
||||||
|
tmplNameLayoutMain = "layout_main.html"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
SiteMeta models.SiteMeta
|
||||||
|
|
||||||
|
// BasePosts is the base path for posts.
|
||||||
|
BasePosts string
|
||||||
|
|
||||||
|
// TemplatesFS provides the raw templates for rendering the site.
|
||||||
|
TemplatesFS fs.FS
|
||||||
|
|
||||||
|
RenderTZ *time.Location
|
||||||
|
}
|
||||||
|
|
||||||
|
type postSingleData struct {
|
||||||
|
Meta models.PostMeta
|
||||||
|
HTML template.HTML
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
type postListData struct {
|
||||||
|
Posts []postSingleData
|
||||||
|
}
|
||||||
|
|
||||||
|
type layoutData struct {
|
||||||
|
Body template.HTML
|
||||||
|
}
|
||||||
72
providers/sitereader/provider.go
Normal file
72
providers/sitereader/provider.go
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
package sitereader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider struct {
|
||||||
|
fs fs.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(fs fs.FS) *Provider {
|
||||||
|
return &Provider{
|
||||||
|
fs: fs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) ReadSite() (models.Sites, error) {
|
||||||
|
posts, err := p.ListPosts()
|
||||||
|
if err != nil {
|
||||||
|
return models.Sites{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.Sites{
|
||||||
|
Posts: posts,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) ListPosts() (posts []*models.Post, err error) {
|
||||||
|
err = fs.WalkDir(p.fs, "posts", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
post, err := p.ReadPost(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
posts = append(posts, post)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return posts, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) ReadPost(path string) (*models.Post, error) {
|
||||||
|
data, err := fs.ReadFile(p.fs, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split front matter and content
|
||||||
|
parts := bytes.SplitN(data, []byte("---"), 3)
|
||||||
|
if len(parts) < 3 {
|
||||||
|
return nil, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
var meta models.Meta
|
||||||
|
if err := yaml.Unmarshal(parts[1], &meta); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.Post{
|
||||||
|
Meta: meta,
|
||||||
|
Content: string(bytes.TrimPrefix(parts[2], []byte("\n"))),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
106
providers/sitereader/provider_test.go
Normal file
106
providers/sitereader/provider_test.go
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
package sitereader_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"lmika.dev/lmika/weiro/models"
|
||||||
|
"lmika.dev/lmika/weiro/providers/sitereader"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProvider_ReadPost(t *testing.T) {
|
||||||
|
t.Run("with meta", func(t *testing.T) {
|
||||||
|
testFS := fstest.MapFS{
|
||||||
|
"posts/test.md": {Data: []byte(`---
|
||||||
|
date: 2026-02-18T19:59:00Z
|
||||||
|
title: Test Post Here
|
||||||
|
tags: [test, example]
|
||||||
|
---
|
||||||
|
This is just a test post.
|
||||||
|
`)},
|
||||||
|
}
|
||||||
|
|
||||||
|
pr := sitereader.New(testFS)
|
||||||
|
|
||||||
|
post, err := pr.ReadPost("posts/test.md")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "Test Post Here", post.Meta.Title)
|
||||||
|
assert.Equal(t, time.Date(2026, 2, 18, 19, 59, 0, 0, time.UTC), post.Meta.Date)
|
||||||
|
assert.Equal(t, []string{"test", "example"}, post.Meta.Tags)
|
||||||
|
assert.Equal(t, "This is just a test post.\n", post.Content)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("without meta", func(t *testing.T) {
|
||||||
|
testFS := fstest.MapFS{
|
||||||
|
"posts/test.md": {Data: []byte(`---
|
||||||
|
---
|
||||||
|
This is just a test post.
|
||||||
|
`)},
|
||||||
|
}
|
||||||
|
|
||||||
|
pr := sitereader.New(testFS)
|
||||||
|
|
||||||
|
post, err := pr.ReadPost("posts/test.md")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, models.Meta{}, post.Meta)
|
||||||
|
assert.Equal(t, "This is just a test post.\n", post.Content)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_ListPosts(t *testing.T) {
|
||||||
|
testFS := fstest.MapFS{
|
||||||
|
"posts/01-post1.md": {Data: []byte(`---
|
||||||
|
id: 111
|
||||||
|
date: 2026-02-18T19:59:00Z
|
||||||
|
title: Test Post Here
|
||||||
|
tags: [test, example]
|
||||||
|
---
|
||||||
|
This is just a test post.
|
||||||
|
`)},
|
||||||
|
"posts/02-post2.md": {Data: []byte(`---
|
||||||
|
id: 222
|
||||||
|
---
|
||||||
|
This is just a test post.
|
||||||
|
`)},
|
||||||
|
}
|
||||||
|
|
||||||
|
pr := sitereader.New(testFS)
|
||||||
|
|
||||||
|
posts, err := pr.ListPosts()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 2, len(posts))
|
||||||
|
|
||||||
|
assert.Equal(t, "111", posts[0].Meta.ID)
|
||||||
|
assert.Equal(t, "222", posts[1].Meta.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_ReadSite(t *testing.T) {
|
||||||
|
testFS := fstest.MapFS{
|
||||||
|
"posts/01-post1.md": {Data: []byte(`---
|
||||||
|
id: 111
|
||||||
|
date: 2026-02-18T19:59:00Z
|
||||||
|
title: Test Post Here
|
||||||
|
tags: [test, example]
|
||||||
|
---
|
||||||
|
This is just a test post.
|
||||||
|
`)},
|
||||||
|
"posts/02-post2.md": {Data: []byte(`---
|
||||||
|
id: 222
|
||||||
|
---
|
||||||
|
This is just a test post.
|
||||||
|
`)},
|
||||||
|
}
|
||||||
|
|
||||||
|
pr := sitereader.New(testFS)
|
||||||
|
|
||||||
|
sites, err := pr.ReadSite()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 2, len(sites.Posts))
|
||||||
|
|
||||||
|
assert.Equal(t, "111", sites.Posts[0].Meta.ID)
|
||||||
|
assert.Equal(t, "222", sites.Posts[1].Meta.ID)
|
||||||
|
}
|
||||||
6
site_templates/fs.go
Normal file
6
site_templates/fs.go
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
package site_templates
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed *.html
|
||||||
|
var FS embed.FS
|
||||||
21
site_templates/layout_main.html
Normal file
21
site_templates/layout_main.html
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>My New Website</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>Hello, world</h1>
|
||||||
|
<p>Welcome to my website!</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{{ .Body }}
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>Test stuff</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4
site_templates/posts_list.html
Normal file
4
site_templates/posts_list.html
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{{ range .Posts }}
|
||||||
|
{{ .HTML }}
|
||||||
|
<a href="{{ .Path }}">{{ format_date .Meta.Date }}</a>
|
||||||
|
{{ end }}
|
||||||
2
site_templates/posts_single.html
Normal file
2
site_templates/posts_single.html
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{{ .HTML }}
|
||||||
|
<a href="{{ .Path }}">{{ format_date .Meta.Date }}</a>
|
||||||
Loading…
Reference in a new issue