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 cmd := &cobra.Command{ Use: "generate", Aliases: []string{"g"}, Short: "generate project to dist folder", Run: func(cmd *cobra.Command, args []string) { startAt := time.Now() // 读取配置文件 outter.Config = readConfigFile() // 读取资源文件 outter.MetaData = readDataSource() // 读取文章列表 outter.Posts = readAllPosts() // 读取页面列表 outter.Pages = readAllPages() // 读取主题模板文件 outter.Theme = readTheme(outter.Config.Theme) // 数据写入模板文件 outter.generate() endAt := time.Since(startAt) fmt.Printf("generate used: %s\n", 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 }