From eea0c51b262dc375c8d227ab0d11c9e497c64353 Mon Sep 17 00:00:00 2001 From: xuthus5 Date: Sun, 24 Jul 2022 14:26:30 +0800 Subject: [PATCH] release: 0.0.1 version. --- README.md | 22 +++ cmd.go | 21 ++- config.go | 73 +++++++++ deploy.go | 231 +++++++++++++++++++++++++++ go.mod | 31 +++- init.go | 20 ++- module_update.sh | 12 ++ new.go | 98 ++++++++++++ output.go | 400 +++++++++++++++++++++++++++++++++++++++++++++++ page.go | 1 - post.go | 1 - serve.go | 68 ++++++++ utils.go | 115 ++++++++++++++ 13 files changed, 1083 insertions(+), 10 deletions(-) create mode 100644 README.md create mode 100644 config.go create mode 100644 module_update.sh create mode 100644 new.go create mode 100644 output.go delete mode 100644 page.go delete mode 100644 post.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..41b837c --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# mder + +> 一个极速静态网站生成工具 + +快速开始 + +```shell +# 执行安装 +go install gitter.top/mder/mder@latest +# 创建第一个项目 +mder init --name "my_first_blog" +# 本地预览 +mder serve +# 本地部署 +mder deploy +``` + +## changelog + +| 时间节点 | 描述 | +|------------|-----------| +| 2022-07-25 | 服务端基本功能完成 | diff --git a/cmd.go b/cmd.go index c372b5c..8a7c0bc 100644 --- a/cmd.go +++ b/cmd.go @@ -2,6 +2,11 @@ package main import ( "github.com/spf13/cobra" + "github.com/yuin/goldmark" + emoji "github.com/yuin/goldmark-emoji" + meta "github.com/yuin/goldmark-meta" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" ) var ( @@ -9,13 +14,27 @@ var ( Use: "mder", Short: "mder is a very fast static site generator", } + + markdown = goldmark.New( + goldmark.WithParserOptions(parser.WithAutoHeadingID()), + goldmark.WithExtensions(extension.GFM, meta.Meta, emoji.Emoji), + ) ) func init() { // create a new mder folder rootCmd.AddCommand(initCmd()) + // generate static website + rootCmd.AddCommand(deployCmd()) + // new post or page + rootCmd.AddCommand(newCmd()) + // run serve locally + rootCmd.AddCommand(serveCmd()) } func main() { - rootCmd.Execute() + err := rootCmd.Execute() + if err != nil { + panic(err) + } } diff --git a/config.go b/config.go new file mode 100644 index 0000000..03c7756 --- /dev/null +++ b/config.go @@ -0,0 +1,73 @@ +package main + +import "time" + +// Config 配置文件 +type Config struct { + Title string `yaml:"title"` // 网站标题 + Seo SEO `yaml:"seo"` // seo相关信息 + Person Person `yaml:"person"` // 博主个人信息 + Theme string `yaml:"theme"` // 主题 + Site Site `yaml:"site"` // 站点配置信息 + Now time.Time // 当前时间 +} + +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 { + Subtitle string `yaml:"subtitle"` // 副标题 + Description string `yaml:"description"` // 描述 + Keywords string `yaml:"keywords"` // 关键字 +} + +// 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.go index 06ab7d0..18658a3 100644 --- a/deploy.go +++ b/deploy.go @@ -1 +1,232 @@ 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 deployCmd() *cobra.Command { + var outter Outter + + cmd := &cobra.Command{ + Use: "deploy", + Short: "create a new mder 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 +} diff --git a/go.mod b/go.mod index c36bc83..2aa8315 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,37 @@ -module mder +module gitter.top/mder/mder go 1.18 -require github.com/spf13/cobra v1.5.0 +require ( + github.com/abhinav/goldmark-toc v0.2.1 + github.com/gin-gonic/gin v1.8.1 + 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/yuin/goldmark-meta v1.1.0 + gopkg.in/yaml.v3 v3.0.1 +) require ( + 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/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/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/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 + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/init.go b/init.go index 703d355..f1f7d30 100644 --- a/init.go +++ b/init.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "os" + "regexp" "github.com/spf13/cobra" ) @@ -13,13 +13,23 @@ func initCmd() *cobra.Command { Use: "init", Short: "create a new mder folder", Run: func(cmd *cobra.Command, args []string) { - if err := cloneTemplate(name); err != nil { - panic(err) + if name == "" { + if len(args) != 0 { + name = args[0] + } } - _, _ = fmt.Fprintf(os.Stdout, "create folder %s success.\n", name) + 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", "mder", "Name of the folder to create") + cmd.Flags().StringVar(&name, "name", "", "Name of the folder to create") return cmd } diff --git a/module_update.sh b/module_update.sh new file mode 100644 index 0000000..8d6f033 --- /dev/null +++ b/module_update.sh @@ -0,0 +1,12 @@ +#!/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.go new file mode 100644 index 0000000..f62fd86 --- /dev/null +++ b/new.go @@ -0,0 +1,98 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/spf13/cobra" +) + +func newCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "new", + Short: "new a post or page", + } + + cmd.AddCommand(newPostCmd()) + cmd.AddCommand(newPageCmd()) + + return cmd +} + +func newPostCmd() *cobra.Command { + var name string + cmd := &cobra.Command{ + Use: "post", + Short: "new a post", + Run: func(cmd *cobra.Command, args []string) { + if name == "" && len(args) != 0 { + name = args[0] + } + var pureName = strings.ReplaceAll(name, ".md", "") + pureName = strings.ReplaceAll(name, "-", " ") + // 空格处理 + name = strings.ReplaceAll(name, " ", "-") + if !strings.HasSuffix(name, ".md") { + name = name + ".md" + } + filename := fmt.Sprintf("posts/%s", name) + // 检测文件夹是否存在 + dir := filepath.Dir(filename) + if !isExist(dir) { + if err := mkdir(dir); err != nil { + sfault("make directory failed: %v", err) + } + } + f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, os.ModePerm) + if err != nil { + sfault("create file failed: %v", err) + } + defer f.Close() + var data = fmt.Sprintf("---\ntitle: %s\ndate: %s\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) + } + }, + } + cmd.Flags().StringVar(&name, "name", "uname.md", "Name of the post file to create") + return cmd +} + +func newPageCmd() *cobra.Command { + var name string + cmd := &cobra.Command{ + Use: "page", + Short: "new a page", + Run: func(cmd *cobra.Command, args []string) { + if name == "" && len(args) != 0 { + name = args[0] + } + var pureName = strings.ReplaceAll(name, ".md", "") + if !strings.HasSuffix(name, ".md") { + name = name + ".md" + } + filename := fmt.Sprintf("pages/%s", name) + // 检测文件夹是否存在 + dir := filepath.Dir(filename) + if !isExist(dir) { + if err := mkdir(dir); err != nil { + sfault("make directory failed: %v", err) + } + } + f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, os.ModePerm) + if err != nil { + sfault("create file failed: %v", err) + } + 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) + } + }, + } + cmd.Flags().StringVar(&name, "name", "uname.md", "Name of the page file to create") + return cmd +} diff --git a/output.go b/output.go new file mode 100644 index 0000000..08e75d2 --- /dev/null +++ b/output.go @@ -0,0 +1,400 @@ +package main + +import ( + "bytes" + "fmt" + "math" + "os" + "os/exec" + "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() { + if !isExist("./dist") { + if err := mkdir("./dist"); err != nil { + sfault("create dist directory failed: %v", err) + } + } + + if err := exec.Command("rm", "-rf", "./dist/*").Run(); err != nil { + sfault("clear 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() { + themePath := fmt.Sprintf("./themes/%s/", o.Config.Theme) + destPath := "./dist/" + cmd := exec.Command("cp", "-r", themePath+"css", destPath) + if err := cmd.Run(); err != nil { + sfault("copy theme css source failed: %v", err) + } + cmd = exec.Command("cp", "-r", themePath+"js", destPath) + if err := cmd.Run(); err != nil { + sfault("copy theme js source failed: %v", err) + } + cmd = exec.Command("cp", "-r", themePath+"images", destPath) + if err := cmd.Run(); err != nil { + 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() + } + sout("index generate success...") + 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() + } + + sout("post generate success...") +} + +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() + } + + sout("page generate success...") +} + +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() + + sout("archives generate success...") +} + +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() + } + + sout("tags generate success...") +} + +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/page.go b/page.go deleted file mode 100644 index 06ab7d0..0000000 --- a/page.go +++ /dev/null @@ -1 +0,0 @@ -package main diff --git a/post.go b/post.go deleted file mode 100644 index 06ab7d0..0000000 --- a/post.go +++ /dev/null @@ -1 +0,0 @@ -package main diff --git a/serve.go b/serve.go index 06ab7d0..2219ab5 100644 --- a/serve.go +++ b/serve.go @@ -1 +1,69 @@ package main + +import ( + "fmt" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/radovskyb/watcher" + "github.com/spf13/cobra" +) + +func serveCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "serve", + Short: "run a serve locally", + Run: func(cmd *cobra.Command, args []string) { + w := watcher.New() + w.SetMaxEvents(1) + w.FilterOps(watcher.Rename, watcher.Move, watcher.Write, watcher.Create) + if err := w.Ignore("./dist"); err != nil { + sfault("watch file failed: %v", err) + } + if err := w.AddRecursive("."); err != nil { + sfault("watch file failed: %v", err) + } + // start http server + gin.SetMode(gin.ReleaseMode) + router := gin.New() + router.Static("/", "./dist") + server := http.Server{ + Addr: ":8666", + Handler: router, + } + go func() { + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + sfault("listen server failed: %v", err) + } + }() + + go func() { + for { + select { + case <-w.Event: + // 文件变更 更新文件 + if err := deployCmd().Execute(); err != nil { + sfault("re generate website failed: %v", err) + } + sout("file change") + case err := <-w.Error: + sfault("watch file failed: %v", err) + case <-w.Closed: + return + } + } + }() + // 先生成一次 + if err := deployCmd().Execute(); err != nil { + sfault("generate website failed: %v", err) + } + // 五秒一次 + fmt.Println("http://127.0.0.1:8666") + if err := w.Start(time.Second * 5); err != nil { + sfault("watch file failed: %v", err) + } + }, + } + return cmd +} diff --git a/utils.go b/utils.go index 9b4279d..9c3425e 100644 --- a/utils.go +++ b/utils.go @@ -4,8 +4,23 @@ import ( "fmt" "os" "os/exec" + "runtime" + "strconv" + "strings" + "time" + + "gopkg.in/yaml.v3" ) +func isExist(fp string) bool { + _, err := os.Stat(fp) + return !os.IsNotExist(err) +} + +func mkdir(fp string) error { + return os.MkdirAll(fp, os.ModePerm) +} + func cloneTemplate(base string) error { _, err := exec.Command("git", "clone", "https://gitter.top/mder/template", base).Output() if err != nil { @@ -14,3 +29,103 @@ func cloneTemplate(base string) error { } 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 + } + var tpl = "2006-01-02 15:04:05" + t, err := time.ParseInLocation(tpl, datetime, time.Local) + if err != nil { + return time.Now(), err + } + return t, nil +} + +func mustString(i interface{}) string { + s, ok := i.(string) + if !ok { + return "" + } + return s +} + +func mustStringSlice(i interface{}) []string { + var arr []string + iarr, ok := i.([]interface{}) + if !ok { + return []string{} + } + for _, str := range iarr { + r, ok := str.(string) + if !ok { + continue + } + arr = append(arr, r) + } + return arr +} + +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) +}