-
Notifications
You must be signed in to change notification settings - Fork 2
/
post.go
195 lines (164 loc) · 5.11 KB
/
post.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// SPDX-License-Identifier: MIT
package loader
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"unicode"
"github.com/issue9/sliceutil"
meta "github.com/yuin/goldmark-meta"
"github.com/yuin/goldmark/parser"
"gopkg.in/yaml.v2"
"github.com/caixw/blogit/internal/vars"
)
// 表示 Post.State 的各类值
const (
StateTop = "top" // 置顶
StateLast = "last" // 放在尾部
StateDraft = "draft" // 表示为草稿,不会加载此条数据
StateDefault = "" // 默认值
)
// Post 表示文章的信息
type Post struct {
Title string `yaml:"title"`
Created time.Time `yaml:"created"` // 创建时间
Modified time.Time `yaml:"modified"` // 修改时间
Summary string `yaml:"summary,omitempty"` // 摘要,同时也作为 meta.description 的内容
// 关联的标签列表
//
// 标签名为各个标签的 slug 值,可以保证其唯一。
Tags []string `yaml:"tags"`
// State 表示文章的状态,有以下四种值:
// - top 表示文章被置顶;
// - last 表示文章会被放置在最后;
// - draft 表示这是一篇草稿,并不会被加地到内存中;
// - 空值 按默认的方式进行处理。
State string `yaml:"state,omitempty"`
// 封面地址,可以为空。
Image string `yaml:"image,omitempty"`
// 自定义 JSON-LD 数据
//
// 不需要包含 <script> 标签,只需要返回 JSON 格式数据好可。
// 如果为空,则支自己生成 BlogPosting 类型的数据。
JSONLD string `yaml:"jsonld,omitempty"`
// 以下内容不存在时,则会使用全局的默认选项
Authors []*Author `yaml:"author,omitempty"`
License *Link `yaml:"license,omitempty"`
Template string `yaml:"template,omitempty"`
Language string `yaml:"language,omitempty"`
Keywords string `yaml:"keywords,omitempty"`
Content string `yaml:"-"` // markdown 内容
Slug string `yaml:"-"`
}
// LoadPosts 加载所有的文章
func LoadPosts(dir string) ([]*Post, error) {
paths := make([]string, 0, 10)
err := filepath.Walk(filepath.Join(dir, vars.PostsDir), func(path string, info os.FileInfo, err error) error {
if err == nil && !info.IsDir() && strings.ToLower(filepath.Ext(info.Name())) == vars.MarkdownExt {
paths = append(paths, path)
}
return err
})
if err != nil {
return nil, err
}
if len(paths) == 0 {
return nil, nil
}
posts := make([]*Post, 0, len(paths))
for _, path := range paths {
post, err := loadPost(dir, path)
if err != nil {
return nil, err
}
posts = append(posts, post)
}
for _, p := range posts {
cnt := sliceutil.Count(posts, func(i int) bool {
return p.Slug == posts[i].Slug && p.Slug != posts[i].Slug
})
if cnt > 1 {
return nil, &FieldError{Message: "存在重复的值", Field: "slug"}
}
}
return posts, nil
}
func loadPost(dir, path string) (*Post, error) {
bs, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
ctx := parser.NewContext()
buf := new(bytes.Buffer)
if err := markdown.Convert(bs, buf, parser.WithContext(ctx)); err != nil {
return nil, err
}
metadata, err := yaml.Marshal(meta.Get(ctx))
if err != nil {
return nil, err
}
post := &Post{}
if err := yaml.Unmarshal(metadata, post); err != nil {
return nil, err
}
post.Content = buf.String()
if err := post.sanitize(dir, path); err != nil {
err.File = path
return nil, err
}
return post, nil
}
func (p *Post) sanitize(dir, path string) *FieldError {
if p.Title == "" {
return &FieldError{Field: "title", Message: "不能为空"}
}
slug := Slug(dir, path)
if strings.HasSuffix(strings.ToLower(slug[len(slug)-3:]), vars.MarkdownExt) {
slug = slug[:len(slug)-len(vars.MarkdownExt)] // 不能用 strings.TrimSuffix,后缀名可能是大写的
}
if strings.IndexFunc(slug, func(r rune) bool { return unicode.IsSpace(r) }) >= 0 {
return &FieldError{Field: "slug", Message: "不能包含空格", Value: slug}
}
if !strings.HasPrefix(slug, vars.PostsDir+"/") {
return &FieldError{Field: "slug", Message: fmt.Sprintf("文章必须位于 %s 目录之下", vars.PostsDir), Value: slug}
}
p.Slug = slug
if len(p.Tags) == 0 {
return &FieldError{Field: "tags", Message: "不能为空"}
}
// state
if p.State != StateDefault && p.State != StateLast && p.State != StateTop {
return &FieldError{Message: "无效的值", Field: "state", Value: p.State}
}
// template
if p.Template == "" {
p.Template = vars.DefaultTemplate
}
for i, a := range p.Authors {
if err := a.sanitize(); err != nil {
err.Field = "author[" + strconv.Itoa(i) + "]." + err.Field
return err
}
}
if p.License != nil {
if err := p.License.sanitize(); err != nil {
err.Field = "license." + err.Field
return err
}
}
return nil
}
// Slug 返回文章的唯一 ID
//
// 含扩展名,相对于 dir 目录。
func Slug(dir, p string) string {
// Clean 同时会将分隔符转换成系统对应的字符,所以先 Clean 再 ToSlash
p = filepath.ToSlash(filepath.Clean(p))
dir = filepath.ToSlash(filepath.Clean(dir)) + "/" // 防止 dir = "p" p = "post/p1.md" 会被处理在成 ost/p1.md
return strings.TrimLeft(strings.TrimPrefix(p, dir), "./")
}