diff --git a/cmd.go b/cmd.go index 2117fc6..2608896 100644 --- a/cmd.go +++ b/cmd.go @@ -7,6 +7,7 @@ import ( meta "github.com/yuin/goldmark-meta" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" + "go.abhg.dev/goldmark/mermaid" ) var ( @@ -17,7 +18,7 @@ var ( markdown = goldmark.New( goldmark.WithParserOptions(parser.WithAutoHeadingID()), - goldmark.WithExtensions(extension.GFM, meta.Meta, emoji.Emoji), + goldmark.WithExtensions(extension.GFM, meta.Meta, emoji.Emoji, &mermaid.Extender{}), ) ) @@ -39,6 +40,6 @@ func init() { func main() { err := rootCmd.Execute() if err != nil { - panic(err) + logger.Fatal(err) } } diff --git a/config.go b/config.go index af6c7e4..9f66aca 100644 --- a/config.go +++ b/config.go @@ -1,44 +1,97 @@ package main -import "time" +import ( + "os" -// Config 配置文件 -type Config struct { - Title string `yaml:"title"` // 网站标题 - SourceVersion string `yaml:"-"` // 资源版本号 - Seo SEO `yaml:"seo"` // seo相关信息 - Person Person `yaml:"person"` // 博主个人信息 - Theme string `yaml:"theme"` // 主题 - Site Site `yaml:"site"` // 站点配置信息 - Now time.Time `yaml:"-"` // 当前时间 - Deploy Deploy `yaml:"deploy"` // 部署配置 + "gopkg.in/yaml.v3" +) + +// Logo 头像配置 +type Logo struct { + Enabled bool `yaml:"enabled"` // 显示或隐藏 + Width int64 `yaml:"width"` // 宽度控制 + Height int64 `yaml:"height"` // 高度控制 + Url string `yaml:"url"` // 源url } +// Favicon 自定义favicon +type Favicon struct { + Url string `yaml:"url"` +} + +// SocialLinks 社交媒体链接 +type SocialLinks struct { + Github string `yaml:"github"` + Email string `yaml:"email"` + QQ string `yaml:"qq"` + Wechat string `yaml:"wechat"` + Twitter string `yaml:"twitter"` + Telegram string `yaml:"telegram"` +} + +// ICP 备案配置 +type ICP struct { + Enabled bool `yaml:"enabled"` // 显示或隐藏 + Url string `yaml:"url"` // 重定向地址 + Text string `yaml:"text"` // 内容 +} + +// CDN 厂商信息 +type CDN struct { + Enabled bool `yaml:"enabled"` + Url string `yaml:"url"` + Image string `yaml:"image"` + Text string `yaml:"text"` +} + +// Comment 评论功能 +type Comment struct { + Enabled bool `yaml:"enabled"` +} + +// Site 站点配置 type Site struct { - LogoName string `yaml:"logo_name"` // logo名称 - IcpEnable bool `yaml:"icp_enable"` // 是否展示备案信息 - IcpName string `yaml:"icp_name"` // 备案信息 - IcpLink string `yaml:"icp_link"` // 备案链接 - CdnEnable bool `yaml:"cdn_enable"` // 开启cdn - CdnName string `yaml:"cdn_name"` // cdn名字 - CdnImage string `yaml:"cdn_image"` // cdn图片 - CdnLink string `yaml:"cdn_link"` // cdn链接 - Paginate bool `yaml:"paginate"` // 是否开启分页 - PageSize int64 `yaml:"page_size"` // 每页数 -} - -type Person struct { - Author string `yaml:"author"` // 作者 - Summary string `yaml:"summary"` // 首页描述 - Email string `yaml:"email"` // 邮件地址 - GithubName string `yaml:"github_name"` // github名 - WechatQrcode string `yaml:"wechat_qrcode"` // 微信名片二维码 -} - -type SEO struct { + Title string `yaml:"title"` // 网站标题 Subtitle string `yaml:"subtitle"` // 副标题 Description string `yaml:"description"` // 描述 Keywords string `yaml:"keywords"` // 关键字 + Author string `yaml:"author"` // 您的名字 + Summary string `yaml:"summary"` // 个人总结 + Theme string `yaml:"theme"` // 主题 +} + +// PageConfig 页面配置 +type PageConfig struct { + Paginate bool `yaml:"paginate"` // 是否开启分页 + Size int64 `yaml:"size"` // 每页数 + Total int // 页总数 + CurrentSize int // 当前页数 +} + +// Config 配置文件 +type Config struct { + Logo Logo `yaml:"logo"` + Favicon Favicon `yaml:"favicon"` + SocialLinks SocialLinks `yaml:"social_links"` + ICP ICP `yaml:"icp"` + CDN CDN `yaml:"cdn"` + Comment Comment `yaml:"comment"` + PageConfig PageConfig `yaml:"page"` + Site Site `yaml:"site"` // 站点配置信息 + Deploy Deploy `yaml:"deploy"` // 部署配置 +} + +func (c *Config) load() error { + configBuffer, err := os.ReadFile("config.yaml") + if err != nil { + logger.Errorf("read config file failed: %v", err) + return err + } + if err := yaml.Unmarshal(configBuffer, c); err != nil { + logger.Errorf("read config file failed: %v", err) + return err + } + return nil } type DeployType string @@ -52,36 +105,3 @@ type Deploy struct { Type DeployType `yaml:"type"` UpyunAuth string `yaml:"upyun_auth"` } - -// Post 文章属性 -type Post struct { - Title string // 文章标题 - FileBasename string // 文件名 - Link string // 链接 - Category string // 分类 - CategoryAlias string // 分类别名 - Tags []string // 标签 - CreatedAt time.Time // 创建时间 - CreatedAtFormat string // 创建时间格式化 - UpdatedAt time.Time // 更新时间 - UpdatedAtFormat string // 更新时间格式化 - MD string // 文章内容 - TOC string // 文章toc -} - -// Page 页面属性 -type Page struct { - Title string // 展示名 - Link string // 链接名 - MD string // 页面内容 -} - -type Theme struct { - Name string // 主题名 - BaseLayout []byte // 基本布局 - PostLayout []byte // 文章布局 - PageLayout []byte // 页面布局 - IndexLayout []byte // 首页布局 - ArchiveLayout []byte // 文章归档布局 - TagLayout []byte // 标签归档布局 -} diff --git a/deploy.go b/deploy_command.go similarity index 66% rename from deploy.go rename to deploy_command.go index 12e9f49..d27b283 100644 --- a/deploy.go +++ b/deploy_command.go @@ -9,13 +9,16 @@ func deployCmd() *cobra.Command { Aliases: []string{"d"}, Short: "deploy project to upyun", PreRun: func(cmd *cobra.Command, args []string) { - outter.Config = readConfigFile() + if err := outter.loadConfig(); err != nil { + logger.Fatalf("load config failed: %v", err) + } config := outter.Config.Deploy switch config.Type { case UpyunDeploy: if !isCommandExist("upx") { if err := goInstall("github.com/upyun/upx"); err != nil { - sfault("install upx failed: %+v", err) + logger.Errorf("install upx failed: %+v", err) + return } } case GitDeploy: @@ -23,7 +26,8 @@ func deployCmd() *cobra.Command { // generate source if err := generateCmd().Execute(); err != nil { - sfault("generate project failed: %v", err) + logger.Errorf("generate project failed: %v", err) + return } }, Run: func(cmd *cobra.Command, args []string) { @@ -31,14 +35,14 @@ func deployCmd() *cobra.Command { switch config.Type { case UpyunDeploy: if config.UpyunAuth == "" { - serr("please config upyun auth string: upx auth [bucket] [operator] [password]") + logger.Errorf("please config upyun auth string: upx auth [bucket] [operator] [password]") return } if err := uploadToUpyun(config.UpyunAuth); err != nil { - serr("%v", err) + logger.Errorf("upload dist to upyun failed: %v", err) return } - sout("deploy to upyun success") + logger.Infof("deploy to upyun success") } }, } diff --git a/generate.go b/generate.go deleted file mode 100644 index 3a1b06f..0000000 --- a/generate.go +++ /dev/null @@ -1,239 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "os" - "sort" - "strings" - "time" - - toc "github.com/abhinav/goldmark-toc" - "github.com/spf13/cobra" - meta "github.com/yuin/goldmark-meta" - "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/text" -) - -func generateCmd() *cobra.Command { - var outter Outter - var startAt time.Time - cmd := &cobra.Command{ - Use: "generate", - Aliases: []string{"g"}, - Short: "generate project to dist folder", - PreRun: func(cmd *cobra.Command, args []string) { - startAt = time.Now() - // 读取配置文件 - outter.Config = readConfigFile() - outter.Config.SourceVersion = randString(8) - }, - Run: func(cmd *cobra.Command, args []string) { - // 读取资源文件 - outter.MetaData = readDataSource() - // 读取文章列表 - outter.Posts = readAllPosts() - // 读取页面列表 - outter.Pages = readAllPages() - // 读取主题模板文件 - outter.Theme = readTheme(outter.Config.Theme) - // 数据写入模板文件 - outter.generate() - - }, - PostRun: func(cmd *cobra.Command, args []string) { - endAt := time.Since(startAt) - sout("generate used: %s", endAt.String()) - }, - } - return cmd -} - -// readDataSource 读取资源文件 -func readDataSource() map[string]interface{} { - var source = make(map[string]interface{}) - const dataDir = "./data" - dirs, err := os.ReadDir(dataDir) - if err != nil { - return nil - } - for _, dir := range dirs { - if dir.IsDir() { - continue - } - // 不是json文件 - if !strings.HasSuffix(dir.Name(), ".json") { - continue - } - body := readFile(fmt.Sprintf("%s/%s", dataDir, dir.Name())) - var obj interface{} - if err := json.Unmarshal(body, &obj); err != nil { - sfault("unmarshal file failed: %v", err) - } - source[strings.ReplaceAll(dir.Name(), ".json", "")] = obj - } - return source -} - -// readPosts 读取文章 -func readAllPosts() []Post { - const postDir = "./posts" - dirs, err := os.ReadDir(postDir) - if err != nil { - sfault("read directory failed: %v", err) - } - var posts []Post - for _, info := range dirs { - // 获取文件信息 - fsInfo, err := info.Info() - if err != nil { - sfault("read file info failed: %v", err) - } - // 不是目录 没有分类 - if !info.IsDir() { - if !strings.HasSuffix(info.Name(), ".md") { - continue - } - post := readPost(fmt.Sprintf("%s/%s", postDir, info.Name())) - post.UpdatedAt = fsInfo.ModTime() - post.UpdatedAtFormat = post.UpdatedAt.Format("2006-01-02") - post.FileBasename = strings.ReplaceAll(fsInfo.Name(), ".md", "") - post.Link = fmt.Sprintf("/default/%s.html", post.FileBasename) - post.Category = "default" - posts = append(posts, post) - continue - } - // 是目录 - posts = append(posts, readPosts(postDir, info.Name())...) - } - sort.Slice(posts, func(i, j int) bool { - return posts[i].CreatedAt.Unix() > posts[j].CreatedAt.Unix() - }) - - return posts -} - -func readPost(fp string) Post { - var post = Post{} - content := readFile(fp) - var buf bytes.Buffer - - var parserContext = parser.NewContext() - doc := markdown.Parser().Parse(text.NewReader(content), parser.WithContext(parserContext)) - - tocTree, err := toc.Inspect(doc, content) - if err != nil { - sfault("read content toc tree failed: %v", err) - } - - list := toc.RenderList(tocTree) - - if list != nil { - if err := markdown.Renderer().Render(&buf, content, list); err != nil { - sfault("render toc content failed: %v", err) - } - post.TOC = buf.String() - buf.Reset() - } - - if err := markdown.Renderer().Render(&buf, content, doc); err != nil { - sfault("render content failed: %v", err) - } - - metaData := meta.Get(parserContext) - post.CreatedAt, err = datetimeStringToTime(mustString(metaData["date"])) - if err != nil { - serr("get post %s created_at time failed, please check file content", post.FileBasename) - } - post.CreatedAtFormat = post.CreatedAt.Format("2006-01-02") - post.Title = mustString(metaData["title"]) - post.Tags = mustStringSlice(metaData["tags"]) - post.Category = mustString(metaData["category"]) - post.MD = buf.String() - return post -} - -func readPage(fp string) Page { - var page Page - content := readFile(fp) - var buf bytes.Buffer - context := parser.NewContext() - if err := markdown.Convert(content, &buf, parser.WithContext(context)); err != nil { - sfault("render content failed: %v", err) - } - metaData := meta.Get(context) - page.Title = mustString(metaData["title"]) - page.MD = buf.String() - return page -} - -func readPosts(base, category string) []Post { - var dir = fmt.Sprintf("%s/%s", base, category) - dirs, err := os.ReadDir(dir) - if err != nil { - sfault("read directory failed: %v", err) - } - var posts []Post - for _, info := range dirs { - if info.IsDir() { - continue - } - if !strings.HasSuffix(info.Name(), ".md") { - continue - } - // 获取文件信息 - fsInfo, err := info.Info() - if err != nil { - sfault("read file info failed: %v", err) - } - post := readPost(fmt.Sprintf("%s/%s", dir, info.Name())) - post.UpdatedAt = fsInfo.ModTime() - post.UpdatedAtFormat = post.UpdatedAt.Format("2006-01-02") - post.FileBasename = strings.ReplaceAll(fsInfo.Name(), ".md", "") - post.Link = fmt.Sprintf("/%s/%s.html", strings.ToLower(category), post.FileBasename) - post.Category = category - posts = append(posts, post) - } - return posts -} - -func readAllPages() []Page { - var dir = "./pages" - dirs, err := os.ReadDir(dir) - if err != nil { - sfault("read directory failed: %v", err) - } - - var pages []Page - for _, info := range dirs { - if info.IsDir() { - continue - } - if !strings.HasSuffix(info.Name(), ".md") { - continue - } - page := readPage(fmt.Sprintf("%s/%s", dir, info.Name())) - page.Link = strings.ReplaceAll(info.Name(), ".md", "") - pages = append(pages, page) - } - return pages -} - -func readTheme(themeName string) Theme { - var theme Theme - var dir = fmt.Sprintf("./themes/%s", themeName) - theme.BaseLayout = readFile(fmt.Sprintf("%s/base.html", dir)) - theme.IndexLayout = readFile(fmt.Sprintf("%s/index.html", dir)) - theme.PageLayout = readFile(fmt.Sprintf("%s/page.html", dir)) - theme.PostLayout = readFile(fmt.Sprintf("%s/post.html", dir)) - theme.ArchiveLayout = readFile(fmt.Sprintf("%s/archive.html", dir)) - theme.TagLayout = readFile(fmt.Sprintf("%s/tag.html", dir)) - - theme.IndexLayout = bytes.ReplaceAll(theme.BaseLayout, []byte("{{layout_placeholder}}"), theme.IndexLayout) - theme.PageLayout = bytes.ReplaceAll(theme.BaseLayout, []byte("{{layout_placeholder}}"), theme.PageLayout) - theme.PostLayout = bytes.ReplaceAll(theme.BaseLayout, []byte("{{layout_placeholder}}"), theme.PostLayout) - theme.ArchiveLayout = bytes.ReplaceAll(theme.BaseLayout, []byte("{{layout_placeholder}}"), theme.ArchiveLayout) - theme.TagLayout = bytes.ReplaceAll(theme.BaseLayout, []byte("{{layout_placeholder}}"), theme.TagLayout) - return theme -} diff --git a/generate_command.go b/generate_command.go new file mode 100644 index 0000000..8f6cbf9 --- /dev/null +++ b/generate_command.go @@ -0,0 +1,872 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "math" + "os" + "os/exec" + "sort" + "strings" + "text/template" + "time" + + "github.com/spf13/cobra" + meta "github.com/yuin/goldmark-meta" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/text" + "go.abhg.dev/goldmark/toc" +) + +func generateCmd() *cobra.Command { + var startAt time.Time + var outter = newOutter() + cmd := &cobra.Command{ + Use: "generate", + Aliases: []string{"g"}, + Short: "generate project to dist folder", + PreRun: func(cmd *cobra.Command, args []string) { + startAt = time.Now() + // 读取配置文件 + if err := outter.loadConfig(); err != nil { + logger.Fatalf("load config file failed: %v", err) + } + }, + Run: func(cmd *cobra.Command, args []string) { + // 读取资源文件 + if err := outter.readDataSource(); err != nil { + logger.Errorf("read data source failed: %v", err) + return + } + // 读取主题模板文件 + if err := outter.readTheme(outter.Config.Site.Theme); err != nil { + logger.Errorf("read theme source failed: %v", err) + return + } + // 读取页面列表 + if err := outter.readAllPages(); err != nil { + logger.Errorf("read page source failed: %v", err) + return + } + // 读取文章列表 + if err := outter.readAllPosts(); err != nil { + logger.Errorf("read post source failed: %v", err) + return + } + // 数据写入模板文件 + outter.generate() + }, + PostRun: func(cmd *cobra.Command, args []string) { + endAt := time.Since(startAt) + logger.Infof("generate used: %s", endAt.String()) + }, + } + return cmd +} + +// readAllPosts 读取所有post文章 +func (o *Outter) readAllPosts() error { + const postDir = "./posts" + dirs, err := os.ReadDir(postDir) + if err != nil { + logger.Errorf("read directory failed: %v", err) + return err + } + for _, info := range dirs { + // 获取文件信息 + fsInfo, err := info.Info() + if err != nil { + logger.Errorf("read file info failed: %v", err) + continue + } + // 不是目录 没有分类 + if info.IsDir() { + o.Posts = append(o.Posts, o.readPosts(postDir, info.Name())...) + continue + } + if !strings.HasSuffix(info.Name(), ".md") { + continue + } + post, err := o.readPost(fmt.Sprintf("%s/%s", postDir, info.Name())) + if err != nil { + logger.Errorf("read post file failed: %v", err) + continue + } + post.UpdatedAtFormat = fsInfo.ModTime().Format(time.DateOnly) + post.FileBasename = strings.ReplaceAll(fsInfo.Name(), ".md", "") + post.Link = fmt.Sprintf("/default/%s.html", post.FileBasename) + post.Category = "default" + o.Posts = append(o.Posts, post) + } + sort.Slice(o.Posts, func(i, j int) bool { + return o.Posts[i].CreatedAt.Unix() > o.Posts[j].CreatedAt.Unix() + }) + return nil +} + +func (o *Outter) readPost(fp string) (*Post, error) { + var post = new(Post) + content, err := os.ReadFile(fp) + if err != nil { + logger.Errorf("read file %s failed: %v", fp, err) + return nil, err + } + var buf bytes.Buffer + var parserContext = parser.NewContext() + doc := markdown.Parser().Parse(text.NewReader(content), parser.WithContext(parserContext)) + + tocTree, err := toc.Inspect(doc, content) + if err != nil { + logger.Errorf("read content toc tree failed: %v", err) + return nil, err + } + + list := toc.RenderList(tocTree) + if list != nil { + if err := markdown.Renderer().Render(&buf, content, list); err != nil { + logger.Errorf("render toc content failed: %v", err) + return nil, err + } + post.TOC = buf.String() + buf.Reset() + } + if err := markdown.Renderer().Render(&buf, content, doc); err != nil { + logger.Errorf("render content failed: %v", err) + return nil, err + } + + metaData := meta.Get(parserContext) + post.CreatedAt, err = datetimeStringToTime(mustString(metaData["date"])) + if err != nil { + logger.Errorf("get post %s created_at time failed, please check file content", post.FileBasename) + return nil, err + } + post.CreatedAtFormat = post.CreatedAt.Format("2006-01-02") + post.Title = mustString(metaData["title"]) + post.Tags = mustStringSlice(metaData["tags"]) + post.Category = mustString(metaData["category"]) + post.MD = buf.String() + return post, nil +} + +func (o *Outter) readPage(fp string) (*Page, error) { + var page = new(Page) + content, err := os.ReadFile(fp) + if err != nil { + logger.Errorf("read page file failed: %v", err) + return nil, err + } + var buf bytes.Buffer + context := parser.NewContext() + if err := markdown.Convert(content, &buf, parser.WithContext(context)); err != nil { + logger.Errorf("render content failed: %v", err) + return nil, err + } + metaData := meta.Get(context) + page.Title = mustString(metaData["title"]) + page.MD = buf.String() + return page, nil +} + +func (o *Outter) readPosts(base, category string) []*Post { + var dir = fmt.Sprintf("%s/%s", base, category) + dirs, err := os.ReadDir(dir) + if err != nil { + logger.Errorf("read directory failed: %v", err) + return nil + } + var posts []*Post + for _, info := range dirs { + if info.IsDir() { + continue + } + if !strings.HasSuffix(info.Name(), ".md") { + continue + } + // 获取文件信息 + fsInfo, err := info.Info() + if err != nil { + logger.Errorf("read file info failed: %v", err) + return nil + } + post, err := o.readPost(fmt.Sprintf("%s/%s", dir, info.Name())) + if err != nil { + logger.Errorf("read post failed: %v", err) + continue + } + post.UpdatedAtFormat = fsInfo.ModTime().Format(time.DateOnly) + post.FileBasename = strings.ReplaceAll(fsInfo.Name(), ".md", "") + post.Link = fmt.Sprintf("/%s/%s.html", strings.ToLower(category), post.FileBasename) + post.Category = category + posts = append(posts, post) + } + return posts +} + +func (o *Outter) readAllPages() error { + var dir = "./pages" + dirs, err := os.ReadDir(dir) + if err != nil { + logger.Errorf("read directory failed: %v", err) + return err + } + for _, info := range dirs { + if info.IsDir() { + continue + } + if !strings.HasSuffix(info.Name(), ".md") { + continue + } + page, err := o.readPage(fmt.Sprintf("%s/%s", dir, info.Name())) + if err != nil { + logger.Errorf("read page file failed: %v", err) + continue + } + page.Link = strings.ReplaceAll(info.Name(), ".md", "") + o.Pages = append(o.Pages, page) + } + return nil +} + +func (o *Outter) readTheme(themeName string) (err error) { + o.Theme = new(Theme) + var dir = fmt.Sprintf("./themes/%s", themeName) + o.Theme.BaseLayout, err = os.ReadFile(fmt.Sprintf("%s/base.html", dir)) + if err != nil { + logger.Errorf("read base.html theme file failed: %v", err) + return err + } + o.Theme.IndexLayout, err = os.ReadFile(fmt.Sprintf("%s/index.html", dir)) + if err != nil { + logger.Errorf("read index.html theme file failed: %v", err) + return err + } + o.Theme.PageLayout, err = os.ReadFile(fmt.Sprintf("%s/page.html", dir)) + if err != nil { + logger.Errorf("read page.html theme file failed: %v", err) + return err + } + o.Theme.PostLayout, err = os.ReadFile(fmt.Sprintf("%s/post.html", dir)) + if err != nil { + logger.Errorf("read pose.html theme file failed: %v", err) + return err + } + o.Theme.ArchiveLayout, err = os.ReadFile(fmt.Sprintf("%s/archive.html", dir)) + if err != nil { + logger.Errorf("read archive.html theme file failed: %v", err) + return err + } + o.Theme.TagLayout, err = os.ReadFile(fmt.Sprintf("%s/tag.html", dir)) + if err != nil { + logger.Errorf("read tag.html theme file failed: %v", err) + return err + } + o.Theme.CategoryLayout, err = os.ReadFile(fmt.Sprintf("%s/category.html", dir)) + if err != nil { + logger.Errorf("read category.html theme file failed: %v", err) + return err + } + + o.Theme.IndexLayout = bytes.ReplaceAll(o.Theme.BaseLayout, []byte("{{layout_placeholder}}"), o.Theme.IndexLayout) + o.Theme.PageLayout = bytes.ReplaceAll(o.Theme.BaseLayout, []byte("{{layout_placeholder}}"), o.Theme.PageLayout) + o.Theme.PostLayout = bytes.ReplaceAll(o.Theme.BaseLayout, []byte("{{layout_placeholder}}"), o.Theme.PostLayout) + o.Theme.ArchiveLayout = bytes.ReplaceAll(o.Theme.BaseLayout, []byte("{{layout_placeholder}}"), o.Theme.ArchiveLayout) + o.Theme.TagLayout = bytes.ReplaceAll(o.Theme.BaseLayout, []byte("{{layout_placeholder}}"), o.Theme.TagLayout) + o.Theme.CategoryLayout = bytes.ReplaceAll(o.Theme.BaseLayout, []byte("{{layout_placeholder}}"), o.Theme.TagLayout) + return nil +} + +type Outter struct { + SourceVersion string // 资源号 防缓存 + Config *Config // 全局配置 + SourceData map[string]interface{} // 记录source/data下的所有文件 + Posts []*Post // 记录文章 + Pages []*Page // 记录页面 + Theme *Theme // 记录主题模板文件 + Now time.Time // 当前时间 +} + +type Theme struct { + Name string // 主题名 + BaseLayout []byte // 基本布局 + PostLayout []byte // 文章布局 + PageLayout []byte // 页面布局 + IndexLayout []byte // 首页布局 + ArchiveLayout []byte // 文章归档布局 + TagLayout []byte // 标签归档布局 + CategoryLayout []byte // 分类归档布局 +} + +// Post 文章属性 +type Post struct { + Title string // 文章标题 + FileBasename string // 文件名 + Link string // 链接 + Category string // 分类 + CategoryAlias string // 分类别名 + Tags []string // 标签 + CreatedAt time.Time // 创建时间 + CreatedAtFormat string // 创建时间格式化 + UpdatedAtFormat string // 更新时间 + MD string // 文章内容 + TOC string // 文章toc +} + +// Page 页面属性 +type Page struct { + Title string // 展示名 + Link string // 链接名 + MD string // 页面内容 +} + +func newOutter() *Outter { + return &Outter{ + SourceVersion: randString(8), + Config: new(Config), + Now: time.Now(), + } +} + +// loadConfig 加载配置文件 +func (o *Outter) loadConfig() error { + if o.Config == nil { + o.Config = new(Config) + } + return o.Config.load() +} + +// readDataSource 读取资源文件 +func (o *Outter) readDataSource() error { + o.SourceData = make(map[string]interface{}) + const dataDir = "./data" + dirs, err := os.ReadDir(dataDir) + if err != nil { + return nil + } + for _, dir := range dirs { + if dir.IsDir() { + continue + } + // 不是json文件 + if !strings.HasSuffix(dir.Name(), ".json") { + continue + } + var body []byte + body, err = os.ReadFile(fmt.Sprintf("%s/%s", dataDir, dir.Name())) + if err != nil { + logger.Errorf("read data source file failed: %v", err) + return err + } + var obj interface{} + if err := json.Unmarshal(body, &obj); err != nil { + logger.Errorf("unmarshal data source file failed: %v", err) + return err + } + o.SourceData[strings.ReplaceAll(dir.Name(), ".json", "")] = obj + } + return nil +} + +// generate 文件生成 +func (o *Outter) generate() { + // 清理dist目录 + if err := o.clearDistDirectory(); err != nil { + logger.Errorf("clear dist directory failed: %v", err) + return + } + if err := o.sourceCopy(); err != nil { + logger.Errorf("copy theme source to dist failed: %v", err) + return + } + o.generateIndex() + o.generatePost() + o.generatePage() + o.generateArchives() + o.generateTags() + o.generateCategories() +} + +// clearDistDirectory 清理dist目录 +func (o *Outter) clearDistDirectory() error { + if err := os.RemoveAll("./dist"); err != nil { + logger.Errorf("remove old dist directory failed: %v", err) + return err + } + + if err := mkdir("./dist"); err != nil { + logger.Errorf("create dist directory failed: %v", err) + return err + } + return nil +} + +// sourceCopy 资源拷贝 将主题的资源拷贝到目标文件夹中 +func (o *Outter) sourceCopy() error { + sourcePath := slash(fmt.Sprintf("./themes/%s/", o.Config.Site.Theme)) + destPath := slash("./dist/" + o.SourceVersion + "/") + // mkdir + cssDir := fmt.Sprintf("%s/css", destPath) + jsDir := fmt.Sprintf("%s/js", destPath) + imagesDir := fmt.Sprintf("%s/images", destPath) + if err := mkdir(cssDir); err != nil { + logger.Errorf("mkdir css directory failed: %v", err) + return err + } + if err := mkdir(jsDir); err != nil { + logger.Errorf("mkdir js directory failed: %v", err) + } + if err := mkdir(imagesDir); err != nil { + logger.Errorf("mkdir images directory failed: %v", err) + } + cmd := exec.Command("cp", "-r", sourcePath+"css", destPath) + if err := cmd.Run(); err != nil { + logger.Infof(cmd.String()) + logger.Errorf("copy theme css source failed: %v", err) + return err + } + cmd = exec.Command("cp", "-r", sourcePath+"js", destPath) + if err := cmd.Run(); err != nil { + logger.Infof(cmd.String()) + logger.Errorf("copy theme js source failed: %v", err) + return err + } + cmd = exec.Command("cp", "-r", sourcePath+"images", destPath) + if err := cmd.Run(); err != nil { + logger.Infof(cmd.String()) + logger.Errorf("copy theme images source failed: %v", err) + return err + } + return nil +} + +// generateIndex 首页生成 +func (o *Outter) generateIndex() error { + indexTemplate := template.New("index") + + funcMap["title"] = func() string { + return o.Config.Site.Title + } + + indexTemplate.Funcs(funcMap) + + indexTemplate, err := indexTemplate.Parse(string(o.Theme.IndexLayout)) + if err != nil { + logger.Errorf("parse index layout failed: %v", err) + return err + } + + // 首页文件 + var buffer = bytes.Buffer{} + + // 没开分页 + if !o.Config.PageConfig.Paginate { + err = indexTemplate.Execute(&buffer, o) + if err != nil { + logger.Errorf("generate index page failed: %v", err) + return err + } + var filename = "./dist/index.html" + if err := os.WriteFile(filename, buffer.Bytes(), os.ModePerm); err != nil { + logger.Errorf("write index file failed: %v", err) + return err + } + logger.Infof("index generate success...") + return nil + } + + // 分页数据不规范 + if o.Config.PageConfig.Size < 1 { + logger.Errorf("page size must > 1") + return err + } + + var pageSize = int(math.Ceil(float64(len(o.Posts)) / float64(o.Config.PageConfig.Size))) + o.Config.PageConfig.Total = pageSize + var posts = make([]*Post, len(o.Posts)) + copy(posts, o.Posts) + for i := 0; i < pageSize; i++ { + rightBorder := int(o.Config.PageConfig.Size) * (i + 1) + if rightBorder > len(posts) { + rightBorder = len(posts) + } + leftBorder := int(o.Config.PageConfig.Size) * (i) + o.Posts = make([]*Post, rightBorder-leftBorder) + copy(o.Posts, posts[leftBorder:rightBorder]) + o.Config.PageConfig.CurrentSize = i + 1 + + err = indexTemplate.Execute(&buffer, o) + if err != nil { + logger.Errorf("generate index page failed: %v", err) + return err + } + // 第一页 主页 + if i == 0 { + // 第一次的时候创建目录 + dir := "./dist/page" + if err := o.createDir(dir); err != nil { + logger.Errorf("create index page failed: %v", err) + return err + } + var filename = "./dist/index.html" + if err := os.WriteFile(filename, buffer.Bytes(), os.ModePerm); err != nil { + logger.Errorf("write index file failed: %v", err) + return err + } + } + fPage := fmt.Sprintf("./dist/page/%d.html", i+1) + if err := os.WriteFile(fPage, buffer.Bytes(), os.ModePerm); err != nil { + logger.Errorf("write index file failed: %v", err) + return err + } + buffer.Reset() + } + o.Posts = make([]*Post, len(posts)) + copy(o.Posts, posts) + return nil +} + +func (o *Outter) generatePost() { + postTemplate := template.New("post") + var postBuffer = new(bytes.Buffer) + for _, post := range o.Posts { + instance := PostOutter{ + Post: post, + Config: o.Config, + } + instance.Outter = o + funcMap["title"] = func() string { + return instance.Post.Title + } + funcMap["post_name"] = func() string { + return o.Config.Site.Title + } + postTemplate.Funcs(funcMap) + postTemplate, err := postTemplate.Parse(string(o.Theme.PostLayout)) + if err != nil { + logger.Errorf("generate post page failed: %v", err) + return + } + // 入buffer + if err := postTemplate.Execute(postBuffer, instance); err != nil { + logger.Errorf("generate post page failed: %v", err) + return + } + // buffer写文件 + var catDir = fmt.Sprintf("./dist/%s", post.Category) + if !isExist(catDir) { + if err := mkdir(catDir); err != nil { + logger.Errorf("create directory failed: %v", err) + return + } + } + var filename = fmt.Sprintf("%s/%s.html", catDir, post.FileBasename) + if err := os.WriteFile(filename, postBuffer.Bytes(), os.ModePerm); err != nil { + logger.Errorf("write file failed: %v", err) + return + } + postBuffer.Reset() + } +} + +func (o *Outter) generatePage() { + pageTemplate := template.New("page") + var pageBuffer = new(bytes.Buffer) + for _, page := range o.Pages { + instance := PageOutter{ + Page: page, + Config: o.Config, + } + instance.Outter = o + funcMap["title"] = func() string { + return page.Title + } + funcMap["page_name"] = func() string { + return o.Config.Site.Title + } + pageTemplate.Funcs(funcMap) + pageTemplate, err := pageTemplate.Parse(string(o.Theme.PageLayout)) + if err != nil { + logger.Errorf("generate page page failed: %v", err) + return + } + // 入buffer + if err := pageTemplate.Execute(pageBuffer, instance); err != nil { + logger.Errorf("generate page page failed: %v", err) + return + } + // buffer写文件 + var filename = fmt.Sprintf("./dist/%s.html", page.Link) + if err := os.WriteFile(filename, pageBuffer.Bytes(), os.ModePerm); err != nil { + logger.Errorf("write page content failed: %v", err) + return + } + pageBuffer.Reset() + } +} + +func (o *Outter) generateArchives() { + archiveTemplate := template.New("archive") + var archiveBuffer = new(bytes.Buffer) + // 按时间归档 + var m = make(map[string][]*Post) + for _, post := range o.Posts { + newPost := post + newPost.MD = "" + m[int2String(post.CreatedAt.Year())] = append(m[int2String(post.CreatedAt.Year())], newPost) + } + + var postData []*PostData + for year, posts := range m { + postData = append(postData, &PostData{ + Key: year, + Posts: posts, + }) + } + + sort.Slice(postData, func(i, j int) bool { + return postData[i].Key > postData[j].Key + }) + + var instance = PageOutter{ + Page: &Page{ + Title: "Archives", + Link: "archives", + }, + Config: o.Config, + PostData: postData, + } + instance.Outter = o + funcMap["title"] = func() string { + return instance.Page.Title + } + funcMap["page_name"] = func() string { + return o.Config.Site.Title + } + archiveTemplate.Funcs(funcMap) + archiveTemplate, err := archiveTemplate.Parse(string(o.Theme.ArchiveLayout)) + if err != nil { + logger.Errorf("generate archive page failed: %v", err) + return + } + + // 入buffer + if err := archiveTemplate.Execute(archiveBuffer, instance); err != nil { + logger.Errorf("generate archive page failed: %v", err) + return + } + // buffer写文件 + var filename = fmt.Sprintf("./dist/%s.html", instance.Page.Link) + if err := os.WriteFile(filename, archiveBuffer.Bytes(), os.ModePerm); err != nil { + logger.Errorf("write archive page failed: %v", err) + return + } + archiveBuffer.Reset() +} + +func (o *Outter) generateTags() { + tagsTemplate := template.New("tags") + var tagsBuffer = new(bytes.Buffer) + + // 按标签归档 + var mTag = make(map[string][]*Post) + for _, post := range o.Posts { + for _, tag := range post.Tags { + newPost := post + newPost.MD = "" + mTag[tag] = append(mTag[tag], newPost) + } + } + + for tag, posts := range mTag { + // 按时间归档 + var m = make(map[string][]*Post) + for _, post := range posts { + newPost := post + newPost.MD = "" + m[int2String(post.CreatedAt.Year())] = append(m[int2String(post.CreatedAt.Year())], newPost) + } + + var postData []*PostData + for year, _posts := range m { + postData = append(postData, &PostData{ + Key: year, + Posts: _posts, + }) + } + + sort.Slice(postData, func(i, j int) bool { + return postData[i].Key > postData[j].Key + }) + + var instance = PageOutter{ + Page: &Page{ + Title: tag, + Link: tag, + }, + Config: o.Config, + PostData: postData, + } + instance.Outter = o + funcMap["title"] = func() string { + return instance.Page.Title + } + funcMap["page_name"] = func() string { + return o.Config.Site.Title + } + tagsTemplate.Funcs(funcMap) + tagsTemplate, err := tagsTemplate.Parse(string(o.Theme.TagLayout)) + if err != nil { + logger.Errorf("generate tags page failed: %v", err) + return + } + + // 入buffer + if err := tagsTemplate.Execute(tagsBuffer, instance); err != nil { + logger.Errorf("generate tags page failed: %v", err) + return + } + // 创建tag文件夹 + var tagDir = "./dist/tags" + if !isExist(tagDir) { + if err := mkdir(tagDir); err != nil { + logger.Errorf("create tag directory failed: %v", err) + return + } + } + // buffer写文件 + var filename = fmt.Sprintf("%s/%s.html", tagDir, instance.Page.Link) + if err := os.WriteFile(filename, tagsBuffer.Bytes(), os.ModePerm); err != nil { + logger.Errorf("write tag content failed: %v", err) + return + } + tagsBuffer.Reset() + } +} + +func (o *Outter) generateCategories() { + tagsTemplate := template.New("categories") + var tagsBuffer = new(bytes.Buffer) + + // 按标签归档 + var cats = make(map[string][]*Post) + for _, post := range o.Posts { + newPost := post + cats[post.Category] = append(cats[post.Category], newPost) + } + + for tag, posts := range cats { + // 按时间归档 + var m = make(map[string][]*Post) + for _, post := range posts { + newPost := post + newPost.MD = "" + m[int2String(post.CreatedAt.Year())] = append(m[int2String(post.CreatedAt.Year())], newPost) + } + + var postData []*PostData + for year, _posts := range m { + postData = append(postData, &PostData{ + Key: year, + Posts: _posts, + }) + } + + sort.Slice(postData, func(i, j int) bool { + return postData[i].Key > postData[j].Key + }) + + var instance = PageOutter{ + Page: &Page{ + Title: tag, + Link: tag, + }, + Config: o.Config, + PostData: postData, + } + instance.Outter = o + funcMap["title"] = func() string { + return instance.Page.Title + } + funcMap["page_name"] = func() string { + return o.Config.Site.Title + } + tagsTemplate.Funcs(funcMap) + tagsTemplate, err := tagsTemplate.Parse(string(o.Theme.TagLayout)) + if err != nil { + logger.Errorf("generate tags page failed: %v", err) + return + } + + // 入buffer + if err := tagsTemplate.Execute(tagsBuffer, instance); err != nil { + logger.Errorf("generate tags page failed: %v", err) + return + } + // 创建tag文件夹 + var tagDir = "./dist/category" + if !isExist(tagDir) { + if err := mkdir(tagDir); err != nil { + logger.Errorf("create tag directory failed: %v", err) + return + } + } + // buffer写文件 + var filename = fmt.Sprintf("%s/%s.html", tagDir, instance.Page.Link) + if err := os.WriteFile(filename, tagsBuffer.Bytes(), os.ModePerm); err != nil { + logger.Errorf("write tag content failed: %v", err) + return + } + tagsBuffer.Reset() + } +} + +type PostOutter struct { + Post *Post + Config *Config + *Outter +} + +type PageOutter struct { + Page *Page + Config *Config + PostData []*PostData + *Outter +} + +type PostData struct { + Key string + Posts []*Post +} + +func (o *Outter) createDir(fp string) error { + if !isExist(fp) { + if err := mkdir(fp); err != nil { + logger.Errorf("create %s directory failed: %v", fp, err) + return err + } + } + return nil +} + +var funcMap = template.FuncMap{ + "getSource": getSource, + "add": add, + "sum": sum, +} + +func getSource(data interface{}, key string) interface{} { + source, ok := data.(map[string]interface{}) + if !ok { + return nil + } + return source[key] +} + +func add(a, b int) int { + return a + b +} + +func sum(a, b int) int { + return a - b +} diff --git a/go.mod b/go.mod index a89cb74..749ad98 100644 --- a/go.mod +++ b/go.mod @@ -1,39 +1,49 @@ module gitter.top/mder/mder -go 1.18 +go 1.20 require ( - github.com/abhinav/goldmark-toc v0.2.1 - github.com/gin-gonic/gin v1.8.1 - github.com/guonaihong/gout v0.3.1 + github.com/gin-gonic/gin v1.9.1 + github.com/guonaihong/gout v0.3.8 github.com/radovskyb/watcher v1.0.7 - github.com/spf13/cobra v1.5.0 - github.com/yuin/goldmark v1.4.13 - github.com/yuin/goldmark-emoji v1.0.1 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.7.0 + github.com/yuin/goldmark v1.5.6 + github.com/yuin/goldmark-emoji v1.0.2 github.com/yuin/goldmark-meta v1.1.0 + go.abhg.dev/goldmark/mermaid v0.4.0 + go.abhg.dev/goldmark/toc v0.7.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/bytedance/sonic v1.10.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/chenzhuoyu/iasm v0.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-playground/locales v0.14.0 // indirect - github.com/go-playground/universal-translator v0.18.0 // indirect - github.com/go-playground/validator/v10 v10.11.0 // indirect - github.com/goccy/go-json v0.9.10 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.15.4 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/leodido/go-urn v1.2.1 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/ugorji/go/codec v1.2.7 // indirect - golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect - golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect - golang.org/x/text v0.3.7 // indirect - google.golang.org/protobuf v1.28.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.5.0 // indirect + golang.org/x/crypto v0.13.0 // indirect + golang.org/x/net v0.15.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/text v0.13.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/init.go b/init.go deleted file mode 100644 index f1f7d30..0000000 --- a/init.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import ( - "fmt" - "regexp" - - "github.com/spf13/cobra" -) - -func initCmd() *cobra.Command { - var name string - cmd := &cobra.Command{ - Use: "init", - Short: "create a new mder folder", - Run: func(cmd *cobra.Command, args []string) { - if name == "" { - if len(args) != 0 { - name = args[0] - } - } - var rule = fmt.Sprintf("[a-zA-Z0-9_]{%d}", len(name)) - var reg = regexp.MustCompilePOSIX(rule) - if !reg.MatchString(name) { - sfault("folder name must be %s", rule) - } - if err := cloneTemplate(name); err != nil { - sfault("clone template repository failed: %v", err) - } - sout("create folder %s success.", name) - }, - } - - cmd.Flags().StringVar(&name, "name", "", "Name of the folder to create") - return cmd -} diff --git a/init_command.go b/init_command.go new file mode 100644 index 0000000..e29324c --- /dev/null +++ b/init_command.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "regexp" + + "github.com/spf13/cobra" +) + +func initCmd() *cobra.Command { + var name string + cmd := &cobra.Command{ + Use: "init", + Short: "create a new mder folder", + Run: func(cmd *cobra.Command, args []string) { + if name == "" && len(args) == 0 { + logger.Errorf("folder name empty") + return + } + if name == "" && len(args) != 0 { + name = args[0] + } + var rule = fmt.Sprintf(`[A-Za-z0-9_]{%d}`, len([]rune(name))) + var reg = regexp.MustCompilePOSIX(rule) + if !reg.MatchString(name) { + logger.Errorf("folder name rule must be: %s", rule) + return + } + if err := cloneTemplate(name); err != nil { + logger.Errorf("clone template repository failed: %v", err) + return + } + logger.Infof("create folder `%s` success", name) + }, + } + + cmd.Flags().StringVar(&name, "name", "", "name of the folder to create") + return cmd +} diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..0c69771 --- /dev/null +++ b/logger.go @@ -0,0 +1,36 @@ +package main + +import ( + "bytes" + "fmt" + "strings" + + "github.com/sirupsen/logrus" +) + +var logger = logrus.New() + +func init() { + logger.SetReportCaller(true) + logger.SetFormatter(&EasyFormatter{}) +} + +type EasyFormatter struct{} + +func (receiver *EasyFormatter) Format(entry *logrus.Entry) ([]byte, error) { + var output bytes.Buffer + output.WriteString(entry.Level.String()[:4]) + output.WriteString("|") + var filenames = strings.Split(entry.Caller.File, "/") + filename := filenames[len(filenames)-1] + output.WriteString(fmt.Sprintf("%s:%d", filename, entry.Caller.Line)) + output.WriteString("|") + output.WriteString(entry.Caller.Function) + output.WriteString("|") + for k, val := range entry.Data { + output.WriteString(fmt.Sprintf("%s=%v|", k, val)) + } + output.WriteString(" " + entry.Message) + output.WriteRune('\n') + return output.Bytes(), nil +} diff --git a/module_update.sh b/module_update.sh deleted file mode 100644 index 8d6f033..0000000 --- a/module_update.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -go mod tidy - -match_required=$(cat go.mod | grep -zoE "\((.*?)\)" | grep -zoP "(?<=\()[^\)]*(?=\))" | awk -F ' ' '{if(NF==2){print $1}}') - -for i in $match_required;do - echo $i "updating..." - go get -u "$i"; -done - -go mod tidy diff --git a/new.go b/new_command.go similarity index 80% rename from new.go rename to new_command.go index f62fd86..a59d2d1 100644 --- a/new.go +++ b/new_command.go @@ -43,17 +43,20 @@ func newPostCmd() *cobra.Command { dir := filepath.Dir(filename) if !isExist(dir) { if err := mkdir(dir); err != nil { - sfault("make directory failed: %v", err) + logger.Errorf("make directory failed: %v", err) + return } } f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, os.ModePerm) if err != nil { - sfault("create file failed: %v", err) + logger.Errorf("create file failed: %v", err) + return } defer f.Close() - var data = fmt.Sprintf("---\ntitle: %s\ndate: %s\ntags:\n---", pureName, time.Now().Format("2006-01-02 15:04:05")) + var data = fmt.Sprintf("---\ntitle: %s\ndate: %s\ncatagories:\ntags:\n---", pureName, time.Now().Format("2006-01-02 15:04:05")) if _, err := f.Write([]byte(data)); err != nil { - sfault("create file failed: %v", err) + logger.Errorf("create file failed: %v", err) + return } }, } @@ -79,17 +82,20 @@ func newPageCmd() *cobra.Command { dir := filepath.Dir(filename) if !isExist(dir) { if err := mkdir(dir); err != nil { - sfault("make directory failed: %v", err) + logger.Errorf("make directory failed: %v", err) + return } } f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, os.ModePerm) if err != nil { - sfault("create file failed: %v", err) + logger.Errorf("create file failed: %v", err) + return } defer f.Close() var data = fmt.Sprintf("---\ntitle: %s\ndate: %s\n---", pureName, time.Now().Format("2006-01-02 15:04:05")) if _, err := f.Write([]byte(data)); err != nil { - sfault("create file failed: %v", err) + logger.Errorf("create file failed: %v", err) + return } }, } diff --git a/output.go b/output.go deleted file mode 100644 index 5c7c13c..0000000 --- a/output.go +++ /dev/null @@ -1,424 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "math" - "os" - "os/exec" - "runtime" - "sort" - "text/template" -) - -type Outter struct { - MetaData map[string]interface{} // 记录source/data下的所有文件 - Posts []Post // 记录文章 - Pages []Page // 记录页面 - Theme Theme // 记录主题模板文件 - Config Config // 全局配置 - Paginate bool // 是否开启了分页 - PageTotal int // 页面总数 - PageCurrent int // 当前分页 -} - -type PostOutter struct { - Post Post - Config Config -} - -type PageOutter struct { - Page Page - Config Config - PostData []PostData -} - -type PostData struct { - Key string - Posts []Post -} - -// check 检测目标 -func (o *Outter) check() { - var args []string - switch runtime.GOOS { - case "windows": - args = []string{"cmd.exe", "/C", "rmdir", "/S", "/Q", "dist"} - default: - args = []string{"rm", "-rf", "./dist"} - } - - var cmd = exec.Command(args[0], args[1:]...) - - if err := cmd.Run(); err != nil { - sout(cmd.String()) - serr("clear dist directory failed: %v", err) - } - - if !isExist("./dist") { - if err := mkdir("./dist"); err != nil { - sfault("create dist directory failed: %v", err) - } - } -} - -func (o *Outter) createDir(fp string) error { - if !isExist(fp) { - if err := mkdir(fp); err != nil { - sfault("create %s directory failed: %v", fp, err) - } - } - return nil -} - -// sourceCopy 资源拷贝 将主题的资源拷贝到目标文件夹中 -func (o *Outter) sourceCopy() { - sourcePath := slash(fmt.Sprintf("./themes/%s/", o.Config.Theme)) - destPath := slash("./dist/" + o.Config.SourceVersion + "/") - switch runtime.GOOS { - case "windows": - // args = []string{"cmd.exe", "/C", "xcopy", "/e", "/y"} - default: - // mkdir - cssDir := fmt.Sprintf("%s/css", destPath) - jsDir := fmt.Sprintf("%s/js", destPath) - imagesDir := fmt.Sprintf("%s/images", destPath) - if err := mkdir(cssDir); err != nil { - sfault("mkdir css directory failed: %v", err) - } - if err := mkdir(jsDir); err != nil { - sfault("mkdir js directory failed: %v", err) - } - if err := mkdir(imagesDir); err != nil { - sfault("mkdir images directory failed: %v", err) - } - cmd := exec.Command("cp", "-r", sourcePath+"css", destPath) - if err := cmd.Run(); err != nil { - sout(cmd.String()) - sfault("copy theme css source failed: %v", err) - } - cmd = exec.Command("cp", "-r", sourcePath+"js", destPath) - if err := cmd.Run(); err != nil { - sout(cmd.String()) - sfault("copy theme js source failed: %v", err) - } - cmd = exec.Command("cp", "-r", sourcePath+"images", destPath) - if err := cmd.Run(); err != nil { - sout(cmd.String()) - sfault("copy theme images source failed: %v", err) - } - } -} - -// generate 文件生成 -func (o *Outter) generate() { - o.check() - o.sourceCopy() - o.generateIndex() - o.generatePost() - o.generatePage() - o.generateArchives() - o.generateTags() -} - -var funcMap = template.FuncMap{ - "getSource": getSource, - "add": add, - "sum": sum, -} - -// generateIndex 首页生成 -func (o *Outter) generateIndex() { - indexTemplate := template.New("index") - - funcMap["title"] = func() string { - return o.Config.Title - } - - indexTemplate.Funcs(funcMap) - - indexTemplate, err := indexTemplate.Parse(string(o.Theme.IndexLayout)) - if err != nil { - sfault("parse index layout failed: %v", err) - } - - // 首页文件 - var buffer = bytes.Buffer{} - - // 没开分页 - if !o.Config.Site.Paginate { - err = indexTemplate.Execute(&buffer, o) - if err != nil { - sfault("generate index page failed: %v", err) - } - var filename = "./dist/index.html" - if err := os.WriteFile(filename, buffer.Bytes(), os.ModePerm); err != nil { - sfault("write index file failed: %v", err) - } - sout("index generate success...") - return - } - - // 分页数据不规范 - if o.Config.Site.PageSize < 1 { - sfault("page size must > 1") - } - - var pageSize = int(math.Ceil(float64(len(o.Posts)) / float64(o.Config.Site.PageSize))) - o.PageTotal = pageSize - o.Paginate = true - var posts = make([]Post, len(o.Posts)) - copy(posts, o.Posts) - for i := 0; i < pageSize; i++ { - rightBorder := int(o.Config.Site.PageSize) * (i + 1) - if rightBorder > len(posts) { - rightBorder = len(posts) - } - leftBorder := int(o.Config.Site.PageSize) * (i) - o.Posts = make([]Post, rightBorder-leftBorder) - copy(o.Posts, posts[leftBorder:rightBorder]) - o.PageCurrent = i + 1 - - err = indexTemplate.Execute(&buffer, o) - if err != nil { - sfault("generate index page failed: %v", err) - } - // 第一页 主页 - if i == 0 { - // 第一次的时候创建目录 - dir := "./dist/page" - if err := o.createDir(dir); err != nil { - sfault("create index page failed: %v", err) - return - } - var filename = "./dist/index.html" - if err := os.WriteFile(filename, buffer.Bytes(), os.ModePerm); err != nil { - sfault("write index file failed: %v", err) - } - } - fPage := fmt.Sprintf("./dist/page/%d.html", i+1) - if err := os.WriteFile(fPage, buffer.Bytes(), os.ModePerm); err != nil { - sfault("write index file failed: %v", err) - } - buffer.Reset() - } - o.Posts = make([]Post, len(posts)) - copy(o.Posts, posts) -} - -func (o *Outter) generatePost() { - postTemplate := template.New("post") - var postBuffer = new(bytes.Buffer) - for _, post := range o.Posts { - instance := PostOutter{ - Post: post, - Config: o.Config, - } - funcMap["title"] = func() string { - return instance.Post.Title - } - funcMap["post_name"] = func() string { - return o.Config.Title - } - postTemplate.Funcs(funcMap) - postTemplate, err := postTemplate.Parse(string(o.Theme.PostLayout)) - if err != nil { - sfault("generate post page failed: %v", err) - } - // 入buffer - if err := postTemplate.Execute(postBuffer, instance); err != nil { - sfault("generate post page failed: %v", err) - } - // buffer写文件 - var catDir = fmt.Sprintf("./dist/%s", post.Category) - if !isExist(catDir) { - if err := mkdir(catDir); err != nil { - sfault("create directory failed: %v", err) - } - } - var filename = fmt.Sprintf("%s/%s.html", catDir, post.FileBasename) - if err := os.WriteFile(filename, postBuffer.Bytes(), os.ModePerm); err != nil { - sfault("write file failed: %v", err) - } - postBuffer.Reset() - } -} - -func (o *Outter) generatePage() { - pageTemplate := template.New("page") - var pageBuffer = new(bytes.Buffer) - for _, page := range o.Pages { - instance := PageOutter{ - Page: page, - Config: o.Config, - } - funcMap["title"] = func() string { - return page.Title - } - funcMap["page_name"] = func() string { - return o.Config.Title - } - pageTemplate.Funcs(funcMap) - pageTemplate, err := pageTemplate.Parse(string(o.Theme.PageLayout)) - if err != nil { - sfault("generate page page failed: %v", err) - } - // 入buffer - if err := pageTemplate.Execute(pageBuffer, instance); err != nil { - sfault("generate page page failed: %v", err) - } - // buffer写文件 - var filename = fmt.Sprintf("./dist/%s.html", page.Link) - if err := os.WriteFile(filename, pageBuffer.Bytes(), os.ModePerm); err != nil { - sfault("write page content failed: %v", err) - } - pageBuffer.Reset() - } -} - -func (o *Outter) generateArchives() { - archiveTemplate := template.New("archive") - var archiveBuffer = new(bytes.Buffer) - // 按时间归档 - var m = make(map[string][]Post) - for _, post := range o.Posts { - newPost := post - newPost.MD = "" - m[int2String(post.CreatedAt.Year())] = append(m[int2String(post.CreatedAt.Year())], newPost) - } - - var postData []PostData - for year, posts := range m { - postData = append(postData, PostData{ - Key: year, - Posts: posts, - }) - } - - sort.Slice(postData, func(i, j int) bool { - return postData[i].Key > postData[j].Key - }) - - var instance = PageOutter{ - Page: Page{ - Title: "Archives", - Link: "archives", - }, - Config: o.Config, - PostData: postData, - } - funcMap["title"] = func() string { - return instance.Page.Title - } - funcMap["page_name"] = func() string { - return o.Config.Title - } - archiveTemplate.Funcs(funcMap) - archiveTemplate, err := archiveTemplate.Parse(string(o.Theme.ArchiveLayout)) - if err != nil { - sfault("generate archive page failed: %v", err) - } - - // 入buffer - if err := archiveTemplate.Execute(archiveBuffer, instance); err != nil { - sfault("generate archive page failed: %v", err) - } - // buffer写文件 - var filename = fmt.Sprintf("./dist/%s.html", instance.Page.Link) - if err := os.WriteFile(filename, archiveBuffer.Bytes(), os.ModePerm); err != nil { - sfault("write archive page failed: %v", err) - } - archiveBuffer.Reset() -} - -func (o *Outter) generateTags() { - tagsTemplate := template.New("tags") - var tagsBuffer = new(bytes.Buffer) - - // 按标签归档 - var mTag = make(map[string][]Post) - for _, post := range o.Posts { - for _, tag := range post.Tags { - newPost := post - newPost.MD = "" - mTag[tag] = append(mTag[tag], newPost) - } - } - - for tag, posts := range mTag { - // 按时间归档 - var m = make(map[string][]Post) - for _, post := range posts { - newPost := post - newPost.MD = "" - m[int2String(post.CreatedAt.Year())] = append(m[int2String(post.CreatedAt.Year())], newPost) - } - - var postData []PostData - for year, _posts := range m { - postData = append(postData, PostData{ - Key: year, - Posts: _posts, - }) - } - - sort.Slice(postData, func(i, j int) bool { - return postData[i].Key > postData[j].Key - }) - - var instance = PageOutter{ - Page: Page{ - Title: tag, - Link: tag, - }, - Config: o.Config, - PostData: postData, - } - - funcMap["title"] = func() string { - return instance.Page.Title - } - funcMap["page_name"] = func() string { - return o.Config.Title - } - tagsTemplate.Funcs(funcMap) - tagsTemplate, err := tagsTemplate.Parse(string(o.Theme.TagLayout)) - if err != nil { - sfault("generate tags page failed: %v", err) - } - - // 入buffer - if err := tagsTemplate.Execute(tagsBuffer, instance); err != nil { - sfault("generate tags page failed: %v", err) - } - // 创建tag文件夹 - var tagDir = "./dist/tags" - if !isExist(tagDir) { - if err := mkdir(tagDir); err != nil { - sfault("create tag directory failed: %v", err) - } - } - // buffer写文件 - var filename = fmt.Sprintf("%s/%s.html", tagDir, instance.Page.Link) - if err := os.WriteFile(filename, tagsBuffer.Bytes(), os.ModePerm); err != nil { - sfault("write tag content failed: %v", err) - } - tagsBuffer.Reset() - } -} - -func getSource(data interface{}, key string) interface{} { - meta, ok := data.(map[string]interface{}) - if !ok { - return nil - } - return meta[key] -} - -func add(a, b int) int { - return a + b -} - -func sum(a, b int) int { - return a - b -} diff --git a/serve.go b/serve_command.go similarity index 65% rename from serve.go rename to serve_command.go index 05819a6..e19c4c4 100644 --- a/serve.go +++ b/serve_command.go @@ -18,19 +18,24 @@ func serveCmd() *cobra.Command { w.SetMaxEvents(1) w.FilterOps(watcher.Rename, watcher.Move, watcher.Write, watcher.Create) if err := w.AddRecursive("./data"); err != nil { - sfault("watch directory data failed: %v", err) + logger.Errorf("watch directory data failed: %v", err) + return } if err := w.AddRecursive("./posts"); err != nil { - sfault("watch directory posts failed: %v", err) + logger.Errorf("watch directory posts failed: %v", err) + return } if err := w.AddRecursive("./pages"); err != nil { - sfault("watch directory pages failed: %v", err) + logger.Errorf("watch directory pages failed: %v", err) + return } if err := w.AddRecursive("./themes"); err != nil { - sfault("watch directory pages failed: %v", err) + logger.Errorf("watch directory pages failed: %v", err) + return } if err := w.Add("config.yaml"); err != nil { - sfault("watch file failed: %v", err) + logger.Errorf("watch file failed: %v", err) + return } // start http server gin.SetMode(gin.ReleaseMode) @@ -42,7 +47,7 @@ func serveCmd() *cobra.Command { } go func() { if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - sfault("listen server failed: %v", err) + logger.Errorf("listen server failed: %v", err) } }() @@ -52,11 +57,12 @@ func serveCmd() *cobra.Command { case event := <-w.Event: // 文件变更 更新文件 if err := generateCmd().Execute(); err != nil { - sfault("re generate website failed: %v", err) + logger.Errorf("re generate website failed: %v", err) + return } - sout("file change: %v", event.String()) + logger.Infof("file change: %v", event.String()) case err := <-w.Error: - sfault("watch file failed: %v", err) + logger.Errorf("watch file failed: %v", err) case <-w.Closed: return } @@ -64,12 +70,14 @@ func serveCmd() *cobra.Command { }() // 先生成一次 if err := generateCmd().Execute(); err != nil { - sfault("generate website failed: %v", err) + logger.Errorf("generate website failed: %v", err) + return } // 五秒一次 - sout("http://127.0.0.1:8666") + logger.Info("http://127.0.0.1:8666") if err := w.Start(time.Second * 5); err != nil { - sfault("watch file failed: %v", err) + logger.Errorf("watch file failed: %v", err) + return } }, } diff --git a/update.go b/update_command.go similarity index 92% rename from update.go rename to update_command.go index e9ebed4..ac9051f 100644 --- a/update.go +++ b/update_command.go @@ -2,11 +2,11 @@ package main import ( "fmt" - "github.com/spf13/cobra" "os/exec" "time" "github.com/guonaihong/gout" + "github.com/spf13/cobra" ) func updateCmd() *cobra.Command { @@ -17,16 +17,17 @@ func updateCmd() *cobra.Command { Run: func(cmd *cobra.Command, args []string) { commit, err := GetRepoLatestCommit() if err != nil { - sfault("get mder version failed: %v", err) - } - sout("-----\nmder latest version: %s\n更新中...\n", commit.Sha) - var url = fmt.Sprintf("gitter.top/mder/mder@%s", commit.Sha) - genO, err := exec.Command("go", "install", url).CombinedOutput() - if err != nil { - serr("install: go install %s\nupdate failed: %v\n", url, err) + logger.Errorf("get mder version failed: %v", err) return } - sout("%supdate success", string(genO)) + logger.Infof("-----\nmder latest version: %s\n更新中...\n", commit.Sha) + var url = fmt.Sprintf("gitter.top/mder/mder@%s", commit.Sha) + _, err = exec.Command("go", "install", url).CombinedOutput() + if err != nil { + logger.Errorf("install: go install %s\nupdate failed: %v\n", url, err) + return + } + logger.Info("mder %s update success", commit.Sha) }, } diff --git a/utils.go b/utils.go index 0d2b6fb..a724ac6 100644 --- a/utils.go +++ b/utils.go @@ -10,8 +10,6 @@ import ( "strconv" "strings" "time" - - "gopkg.in/yaml.v3" ) func isExist(fp string) bool { @@ -26,34 +24,11 @@ func mkdir(fp string) error { func cloneTemplate(base string) error { _, err := exec.Command("git", "clone", "https://gitter.top/mder/template", base).Output() if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "get latest template failed: %v\n", err) return err } return nil } -func readConfigFile() Config { - configBuffer, err := os.ReadFile("config.yaml") - if err != nil { - panic(err) - } - var config Config - if err := yaml.Unmarshal(configBuffer, &config); err != nil { - sfault("read config file failed: %v", err) - } - config.Now = time.Now() - return config -} - -func readFile(fp string) []byte { - if body, err := os.ReadFile(fp); err != nil { - sfault("read file failed: %v", err) - } else { - return body - } - return nil -} - func datetimeStringToTime(datetime string) (time.Time, error) { if datetime == "" { return time.Now(), nil @@ -94,44 +69,6 @@ func int2String(i int) string { return strconv.FormatInt(int64(i), 10) } -type RunInfo struct { - pc uintptr - line int - filename string - funcName string -} - -// 获取正在运行的函数名 -func runInfo() RunInfo { - pc, file, line, ok := runtime.Caller(2) - - if !ok { - return RunInfo{} - } - funcName := runtime.FuncForPC(pc).Name() - return RunInfo{pc, line, file, funcName} -} - -func sout(format string, args ...interface{}) { - if !strings.HasSuffix(format, "\n") { - format = fmt.Sprintf("%s\n", format) - } - _, _ = fmt.Fprintf(os.Stdout, format, args...) -} - -func serr(format string, args ...interface{}) { - var ri = runInfo() - real := fmt.Sprintf(format, args...) - _, _ = fmt.Fprintf(os.Stderr, "runtime: %+v\n%s\n", ri, real) -} - -func sfault(format string, args ...interface{}) { - var ri = runInfo() - real := fmt.Sprintf(format, args...) - _, _ = fmt.Fprintf(os.Stderr, "runtime: %+v\n%s\n", ri, real) - os.Exit(1) -} - func slash(str string) string { if runtime.GOOS == "windows" { return strings.ReplaceAll(str, "/", "\\")