Repository: Foleyzhao/lacerate
Branch: main
Commit: 4f3ccd4f804b
Files: 40
Total size: 82.2 KB
Directory structure:
gitextract__84n7yyp/
├── .gitignore
├── LICENSE
├── README.md
├── cmd/
│ └── cmd.go
├── core/
│ ├── command/
│ │ └── command.go
│ ├── common/
│ │ └── banner.go
│ ├── config/
│ │ └── config.go
│ ├── log/
│ │ └── logger.go
│ ├── model/
│ │ ├── archive.go
│ │ ├── category.go
│ │ ├── post.go
│ │ └── tag.go
│ ├── service/
│ │ ├── about.go
│ │ ├── archive.go
│ │ ├── category.go
│ │ ├── compile.go
│ │ ├── post.go
│ │ ├── tag.go
│ │ └── watcher.go
│ └── utils/
│ ├── crypto.go
│ ├── file.go
│ ├── markdown.go
│ ├── slice.go
│ ├── storage.go
│ ├── string.go
│ ├── template.go
│ └── time.go
├── doc/
│ ├── 博客编写指南.md
│ └── 配置文件说明.md
├── go.mod
├── go.sum
└── theme/
└── blog/
├── assets/
│ └── css/
│ ├── basic.css
│ └── style.css
└── layout/
├── archive.tpl
├── category.tpl
├── home.tpl
├── main.tpl
├── page.tpl
├── post.tpl
└── tag.tpl
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
# Customize
markdown
storage
config.yml
.idea
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
👏 欢迎使用 **Lacerate** !
✍️ **Lacerate** 一个简单的静态博客生成器。
## 特性👇
📝 使用 **Markdown** 语法,进行快速创作
🌉 对文章进行分类
🏷️ 对文章进行标签分组
📋 根据年月进行文章归档
🌁 自定义关于我页面
💻 支持多客户端: **𝖶𝗂𝗇𝖽𝗈𝗐𝗌** / **𝖬𝖺𝖼𝖮𝖲** / **Linux**
## 教程
[配置文件说明](https://github.com/Foleyzhao/lacerate/blob/main/doc/%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E8%AF%B4%E6%98%8E.md) |
[博客编写指南](https://github.com/Foleyzhao/lacerate/blob/main/doc/%E5%8D%9A%E5%AE%A2%E7%BC%96%E5%86%99%E6%8C%87%E5%8D%97.md)
### 快速启动
```bash
git clone https://github.com/Foleyzhao/lacerate.git
go build -o lacerate ./cmd/cmd.go
nohup ./lacerate run > lacerate.log 2>&1 &
```
访问: http://localhost:8090/
### 详细指令
```bash
# lacerate command [args...]
# 初始化博客文件夹
lacerate init
# 新建 markdown 文件
lacerate new filename
# 编译博客
lacerate compile/c
# 打开文件监听器
lacerate watch/w
# 运行http服务,默认端口8090
lacerate http [port]
# 运行lacerate,默认端口8090
lacerate run [port]
```
## 联系
[主页](https://happy.zj.cn/) | 邮箱: foleyzhao@163.com
## 示例截图
## 贡献
欢迎任何形式的贡献。可以使用 [pull requests](https://github.com/Foleyzhao/lacerate/pulls) 或 [issues](https://github.com/Foleyzhao/lacerate/issues) 的方式提交任何想法。
## 支持
## License
[Apache-2.0](https://github.com/Foleyzhao/lacerate/blob/main/LICENSE). Copyright (c) 2024 Lacerate
================================================
FILE: cmd/cmd.go
================================================
package main
import (
"flag"
"github.com/fatih/color"
"lacerate/core/command"
"lacerate/core/common"
"lacerate/core/config"
"lacerate/core/service"
"os"
"strconv"
)
var (
// 命令行参数
args []string
)
// 入口函数
func main() {
_, _ = color.New(color.FgGreen).Fprintln(os.Stdout, common.Banner)
flag.Parse()
args = flag.Args()
if len(args) == 0 || len(args) > 3 {
command.PrintHelp()
os.Exit(1)
}
switch args[0] {
default:
command.PrintHelp()
os.Exit(1)
case "init":
command.Initialize()
case "new":
if len(args) == 2 {
name := args[1]
service.CreateMarkdown(name)
} else {
panic("the file name is missing.")
}
case "compile", "c":
service.Compile()
case "watch", "w":
service.NewWatch(config.Config().Paths, config.Config().Suffix).Watcher()
done := make(chan bool)
<-done
case "run":
service.Compile()
service.NewWatch(config.Config().Paths, config.Config().Suffix).Watcher()
var port = 8090
if len(args) == 2 {
p, err := strconv.Atoi(args[1])
if err != nil {
panic(err)
}
port = p
}
command.ListenHttpServer(port)
case "http", "web":
var port = 8090
if len(args) == 2 {
p, err := strconv.Atoi(args[1])
if err != nil {
panic(err)
}
port = p
}
command.ListenHttpServer(port)
}
}
================================================
FILE: core/command/command.go
================================================
package command
import (
"fmt"
"lacerate/core/config"
"lacerate/core/log"
"net/http"
"os"
"strconv"
)
const (
// HELP 帮助信息
HELP = `
Usage:
lacerate command [args...]
Initialize the blog folder
lacerate init
Create a new markdown file
lacerate new filename
Compile the blog
lacerate compile/c
Open the file listener
lacerate watch/w
Open the file server
lacerate http/web [port]
Run all Lacerate services
lacerate run [port]
`
)
// PrintHelp 打印帮助信息
func PrintHelp() {
fmt.Println(HELP)
}
// Initialize 初始化操作
func Initialize() {
config.CreateConf()
CreateDir()
log.Log.Debug("the initialization is successful!")
}
// ListenHttpServer 启动http服务
func ListenHttpServer(port int) {
log.Log.Info("open the built-in web server...")
p := strconv.Itoa(port)
http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir(config.GlobalConf.Html+"/assets/"))))
http.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir(config.GlobalConf.Html))))
log.Log.Debugf("the built-in web server is successfully turned on and the port is monitored: %d...", port)
err := http.ListenAndServe(":"+p, nil)
if err != nil {
log.Log.Errorf("listen http serve error: %s", err)
}
}
// CreateDir 创建博客目录
func CreateDir() {
_, err := os.Stat(config.GlobalConf.Html)
if os.IsNotExist(err) {
if err := os.MkdirAll(config.GlobalConf.Html, os.ModePerm); err != nil {
panic(err)
}
}
_, err = os.Stat(config.GlobalConf.Markdown)
if os.IsNotExist(err) {
if err := os.MkdirAll(config.GlobalConf.Markdown, os.ModePerm); err != nil {
panic(err)
}
}
_, err = os.Stat(config.GlobalConf.Storage)
if os.IsNotExist(err) {
if err := os.MkdirAll(config.GlobalConf.Storage, os.ModePerm); err != nil {
panic(err)
}
}
_, err = os.Stat(config.GlobalConf.Theme)
if os.IsNotExist(err) {
if err := os.MkdirAll(config.GlobalConf.Theme, os.ModePerm); err != nil {
panic(err)
}
}
}
================================================
FILE: core/common/banner.go
================================================
package common
// Banner banner
var Banner = `
=========================================================
▄ ▄
█ ▄▄▄ ▄▄▄ ▄▄▄ ▄ ▄▄ ▄▄▄ ▄▄█▄▄ ▄▄▄
█ ▀ █ █▀ ▀ █▀ █ █▀ ▀ ▀ █ █ █▀ █
█ ▄▀▀▀█ █ █▀▀▀▀ █ ▄▀▀▀█ █ █▀▀▀▀
█▄▄▄▄▄ ▀▄▄▀█ ▀█▄▄▀ ▀█▄▄▀ █ ▀▄▄▀█ ▀▄▄ ▀█▄▄▀
Author: HappyNewYear
=========================================================
`
================================================
FILE: core/config/config.go
================================================
package config
import (
"gopkg.in/yaml.v3"
"io"
"os"
)
var GlobalConf = Config()
var confFileName = "config.yml"
var confContent = `
# 站点信息
title: xxx's Blog
subtitle: 主页
description: xxx's Blog
keywords: blog
# 作者信息
author: HappyNewYear
avatar: /assets/avatar.jpg
github: https://github.com/Foleyzhao
email: foleyzhao@163.com
# 配置信息
summary_line: 6
home_post_num: 10
# 文件存储
theme: theme/blog
markdown: markdown
html: /data/www/html
storage: storage
# 文件监听
paths:
- markdown
suffix:
- md
- yml
# 自定义信息
home_title: xxx's Blog
archive_title: 归档
tag_title: 标签
category_title: 分类
about_title: 关于我
`
// SystemConfig 系统配置
type SystemConfig struct {
Title string `yaml:"title"`
SubTitle string `yaml:"subtitle"`
Description string `yaml:"description"`
Keywords string `yaml:"keywords"`
Author string `yaml:"name"`
Avatar string `yaml:"avatar"`
Github string `yaml:"github"`
Email string `yaml:"email"`
SummaryLine int `yaml:"summary_line"`
HomePostNum int `yaml:"home_post_num"`
Theme string `yaml:"theme"`
Markdown string `yaml:"markdown"`
Html string `yaml:"html"`
Storage string `yaml:"storage"`
Paths []string `yaml:"paths"`
Suffix []string `yaml:"suffix"`
HomeTitle string `yaml:"home_title,omitempty"`
ArchiveTitle string `yaml:"archive_title,omitempty"`
TagTitle string `yaml:"tag_title,omitempty"`
CategoryTitle string `yaml:"category_title,omitempty"`
AboutTitle string `yaml:"about_title,omitempty"`
}
// 加载系统配置
func loadConf() ([]byte, error) {
_, err := os.Stat(confFileName)
if os.IsNotExist(err) {
CreateConf()
}
file, err := os.Open(confFileName)
if err != nil {
return nil, err
}
defer func(file *os.File) {
_ = file.Close()
}(file)
return io.ReadAll(file)
}
// CreateConf 创建系统配置文件
func CreateConf() {
_, err := os.Stat(confFileName)
if os.IsNotExist(err) {
_, err := os.OpenFile(confFileName, os.O_WRONLY|os.O_CREATE, 0755)
if err != nil {
panic(err)
}
var confWrite = []byte(confContent)
err = os.WriteFile(confFileName, confWrite, 0666)
if err != nil {
panic(err)
}
}
}
// Config 系统配置
func Config() SystemConfig {
confContent, err := loadConf()
if err != nil {
panic("failed to load configuration file: " + err.Error())
}
c := SystemConfig{}
err = yaml.Unmarshal(confContent, &c)
if err != nil {
panic("failed to parse the configuration file: " + err.Error())
}
return c
}
================================================
FILE: core/log/logger.go
================================================
package log
import "github.com/sirupsen/logrus"
// Log 日志记录器
var Log = logrus.WithFields(logrus.Fields{})
// 初始化
func init() {
Log.Logger.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
})
Log.Logger.SetLevel(logrus.DebugLevel)
}
================================================
FILE: core/model/archive.go
================================================
package model
import "time"
// PublishedYears 年份归档列表
type PublishedYears []*PublishedYear
// PublishedMonths 月份归档列表
type PublishedMonths []*PublishedMonth
// PublishedYear 年份归档
type PublishedYear struct {
YearStr string `json:"year"`
Months []*PublishedMonth `json:"months"`
MonthDict map[string]*PublishedMonth `json:"-"`
}
// PublishedMonth 月份归档
type PublishedMonth struct {
MonthStr string `json:"month"`
Posts []*Post `json:"posts"`
Month time.Month `json:"-"`
}
func (y PublishedYears) Len() int {
return len(y)
}
func (y PublishedYears) Swap(i, j int) {
y[i], y[j] = y[j], y[i]
}
func (y PublishedYears) Less(i, j int) bool {
return y[i].YearStr > y[j].YearStr
}
func (m PublishedMonths) Len() int {
return len(m)
}
func (m PublishedMonths) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}
func (m PublishedMonths) Less(i, j int) bool {
return m[i].Month > m[j].Month
}
================================================
FILE: core/model/category.go
================================================
package model
// Category 分类
type Category struct {
Count int `json:"count"`
Name string `json:"name"`
Posts []*Post `json:"posts"`
Url string `json:"url"`
}
================================================
FILE: core/model/post.go
================================================
package model
// PostList 文章列表
type PostList []*Post
// Post 文章
type Post struct {
Id int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Summary string `json:"summary"`
Content string `json:"content"`
Tags []string `json:"tags"`
Category []string `json:"category"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
Url string `json:"url"`
}
func (p PostList) Len() int {
return len(p)
}
func (p PostList) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}
func (p PostList) Less(i, j int) bool {
return p[i].CreatedAt > p[j].CreatedAt
}
================================================
FILE: core/model/tag.go
================================================
package model
// Tag 标签
type Tag struct {
Count int `json:"count"`
Name string `json:"name"`
Posts []*Post `json:"posts"`
Url string `json:"url"`
}
================================================
FILE: core/service/about.go
================================================
package service
import (
"lacerate/core/config"
"lacerate/core/model"
"lacerate/core/utils"
"os"
"path"
"time"
)
// GetAbout 获取about内容
func GetAbout() (post *model.Post, err error) {
post = &model.Post{}
about := path.Join(config.GlobalConf.Markdown, "/about.md")
if _, err := os.Stat(about); os.IsNotExist(err) {
return post, nil
}
content, err := os.ReadFile(about)
if err != nil {
return nil, err
}
post.Title = ""
post.Content = utils.MarkdownToHtml(string(content))
post.CreatedAt = time.Now().Unix()
return post, nil
}
================================================
FILE: core/service/archive.go
================================================
package service
import (
"lacerate/core/model"
"lacerate/core/utils"
"sort"
"time"
)
// GetArchive 获取归档信息
func GetArchive() []*model.PublishedYear {
archiveYear := make(model.PublishedYears, 0)
_archiveYear := make(map[string]*model.PublishedYear)
for _, post := range postList {
yearStr := utils.Year(post.CreatedAt)
monthStr := utils.Month(post.CreatedAt)
_month := time.Unix(post.CreatedAt, 0).Month()
year := _archiveYear[yearStr]
if year == nil {
year = &model.PublishedYear{YearStr: yearStr, Months: make([]*model.PublishedMonth, 0), MonthDict: make(map[string]*model.PublishedMonth)}
_archiveYear[yearStr] = year
}
month := year.MonthDict[monthStr]
if month == nil {
month = &model.PublishedMonth{MonthStr: monthStr, Posts: []*model.Post{}, Month: _month}
year.MonthDict[monthStr] = month
}
month.Posts = append(month.Posts, post)
}
for _, year := range _archiveYear {
monthArray := make(model.PublishedMonths, 0)
for _, month := range year.MonthDict {
monthArray = append(monthArray, month)
}
sort.Sort(monthArray)
year.MonthDict = nil
year.Months = monthArray
archiveYear = append(archiveYear, year)
}
sort.Sort(archiveYear)
return archiveYear
}
================================================
FILE: core/service/category.go
================================================
package service
import (
"lacerate/core/model"
)
// 分类列表
var categoryList map[string]*model.Category
// 初始化
func init() {
categoryList = make(map[string]*model.Category)
}
// GetCategoryList 获取菜单列表
func GetCategoryList() map[string]*model.Category {
return categoryList
}
================================================
FILE: core/service/compile.go
================================================
package service
import (
"html/template"
"lacerate/core/config"
"lacerate/core/log"
"lacerate/core/utils"
"os"
"path"
"strings"
)
// 全局数据
var data = map[string]interface{}{
"title": config.GlobalConf.Title,
"subtitle": config.GlobalConf.SubTitle,
"description": config.GlobalConf.Description,
"keywords": config.GlobalConf.Keywords,
"author": config.GlobalConf.Author,
"avatar": config.GlobalConf.Avatar,
"github": config.GlobalConf.Github,
"email": config.GlobalConf.Email,
}
// html模板函数字典
var funcMaps = template.FuncMap{
"unescaped": utils.Unescaped,
"cmonth": utils.CMonth,
"format": utils.Format,
"count": utils.Count,
"lt": utils.Lt,
"gt": utils.Gt,
"eq": utils.Eq,
"md5": utils.Xmd5,
}
// Compile 编译博客
func Compile() {
defer func() {
if r := recover(); r != nil {
log.Log.Errorf("panic recovered from: %v", r)
}
}()
log.Log.Info("start compiling your blog...")
checkThemeFile()
copyAssetsFile()
LoadPostList()
// 创建页面
CompileHome()
CompilePost()
CompileArchive()
CompileTagPage()
CompileTag()
CompileCategoryPage()
CompileCategory()
CompileAbout()
storageBlogMap()
log.Log.Debug("compilation complete...")
}
// 存储文章
func storageBlogMap() {
storage, err := utils.NewStorage(config.GlobalConf.Storage, "storage.json")
if err != nil {
panic(err)
}
err = storage.Store(GetPostList())
if err != nil {
panic(err)
}
}
// CompileHome 编译主页
func CompileHome() {
title := config.GlobalConf.HomeTitle
if len(strings.TrimSpace(title)) == 0 {
data["title"] = "home page"
} else {
data["title"] = title
}
data["postList"] = GetHomePostList()
data["categoryList"] = GetCategoryList()
data["tagList"] = GetTagList()
err := utils.MkDir(config.GlobalConf.Html)
if err != nil {
panic(err)
}
homePath := path.Join(config.GlobalConf.Html, "index.html")
htmlFile, err := os.Create(homePath)
if err != nil {
panic(err)
}
t, err := template.New("main.tpl").Funcs(funcMaps).ParseFiles(config.GlobalConf.Theme+"/layout/main.tpl", config.GlobalConf.Theme+"/layout/home.tpl")
if err != nil {
panic(err)
}
err = t.Execute(htmlFile, data)
if err != nil {
panic(err)
}
}
// 拷贝静态文件
func copyAssetsFile() {
err := utils.CopyDir(path.Join(config.GlobalConf.Theme, "assets"), path.Join(config.GlobalConf.Html, "assets"))
if err != nil {
panic(err)
}
}
// 校验模板文件
func checkThemeFile() {
if _, err := os.Stat(config.GlobalConf.Theme); os.IsNotExist(err) {
panic("you need to initialize and add the template file first.")
}
}
// CompileCategoryPage 编译分类导航页
func CompileCategoryPage() {
subTitle := config.GlobalConf.CategoryTitle
if len(strings.TrimSpace(subTitle)) == 0 {
data["subtitle"] = "article category"
} else {
data["subtitle"] = subTitle
}
data["categoryList"] = GetCategoryList()
data["tagList"] = GetTagList()
filepath := path.Join(config.GlobalConf.Html, "category")
err := utils.MkDir(filepath)
if err != nil {
panic(err)
}
filename := path.Join(filepath, "index.html")
htmlFile, err := os.Create(filename)
if err != nil {
panic(err)
}
t, err := template.New("main.tpl").Funcs(funcMaps).ParseFiles(config.GlobalConf.Theme+"/layout/category.tpl", config.GlobalConf.Theme+"/layout/main.tpl")
if err != nil {
panic(err)
}
err = t.Execute(htmlFile, data)
if err != nil {
panic(err)
}
}
// CompileCategory 编译分类页
func CompileCategory() {
cateList := GetCategoryList()
data["categoryList"] = cateList
data["tagList"] = GetTagList()
for _, cate := range cateList {
data["subtitle"] = cate.Name
data["pageTitle"] = cate.Name
data["content"] = cate.Posts
data["count"] = cate.Count
filepath := path.Join(config.GlobalConf.Html, "category", cate.Name)
err := utils.MkDir(filepath)
if err != nil {
panic(err)
}
filename := path.Join(filepath, "index.html")
htmlFile, err := os.Create(filename)
if err != nil {
panic(err)
}
t, err := template.New("main.tpl").Funcs(funcMaps).ParseFiles(config.GlobalConf.Theme+"/layout/page.tpl", config.GlobalConf.Theme+"/layout/main.tpl")
if err != nil {
panic(err)
}
err = t.Execute(htmlFile, data)
if err != nil {
panic(err)
}
}
}
// CompileTagPage 编译标签导航页
func CompileTagPage() {
subTitle := config.GlobalConf.TagTitle
if len(strings.TrimSpace(subTitle)) == 0 {
data["subtitle"] = "article tags"
} else {
data["subtitle"] = subTitle
}
data["categoryList"] = GetCategoryList()
data["tagList"] = GetTagList()
filePath := path.Join(config.GlobalConf.Html, "tag")
err := utils.MkDir(filePath)
if err != nil {
panic(err)
}
fileName := path.Join(filePath, "index.html")
htmlFile, err := os.Create(fileName)
if err != nil {
panic(err)
}
t, err := template.New("main.tpl").Funcs(funcMaps).ParseFiles(config.GlobalConf.Theme+"/layout/tag.tpl", config.GlobalConf.Theme+"/layout/main.tpl")
if err != nil {
panic(err)
}
err = t.Execute(htmlFile, data)
if err != nil {
panic(err)
}
}
// CompileTag 编译标签页
func CompileTag() {
tags := GetTagList()
data["categoryList"] = GetCategoryList()
data["tagList"] = GetTagList()
for _, tag := range tags {
data["subtitle"] = tag.Name
data["pageTitle"] = tag.Name
data["content"] = tag.Posts
data["count"] = tag.Count
data["tpl"] = config.GlobalConf.Theme + "/layout/page.html"
filepath := path.Join(config.GlobalConf.Html, "tag", tag.Name)
err := utils.MkDir(filepath)
if err != nil {
panic(err)
}
filename := path.Join(filepath, "index.html")
htmlFile, err := os.Create(filename)
if err != nil {
panic(err)
}
t, err := template.New("main.tpl").Funcs(funcMaps).ParseFiles(config.GlobalConf.Theme+"/layout/page.tpl", config.GlobalConf.Theme+"/layout/main.tpl")
if err != nil {
panic(err)
}
err = t.Execute(htmlFile, data)
if err != nil {
panic(err)
}
}
}
// CompileAbout 编译关于我页
func CompileAbout() {
about, err := GetAbout()
if err != nil {
panic(err)
}
subTitle := config.GlobalConf.AboutTitle
if len(strings.TrimSpace(subTitle)) == 0 {
data["subtitle"] = "about me"
} else {
data["subtitle"] = subTitle
}
data["post"] = about
data["categoryList"] = GetCategoryList()
data["tagList"] = GetTagList()
filePath := path.Join(config.GlobalConf.Html, "about.html")
htmlFile, err := os.Create(filePath)
if err != nil {
panic(err)
}
t, err := template.New("main.tpl").Funcs(funcMaps).ParseFiles(config.GlobalConf.Theme+"/layout/post.tpl", config.GlobalConf.Theme+"/layout/main.tpl")
if err != nil {
panic(err)
}
err = t.Execute(htmlFile, data)
if err != nil {
panic(err)
}
}
// CompilePost 编译文章页
func CompilePost() {
data["categoryList"] = GetCategoryList()
data["tagList"] = GetTagList()
for _, post := range postList {
data["subtitle"] = post.Title
data["description"] = strings.TrimSpace(post.Summary)
data["keywords"] = strings.Join(post.Tags, ",")
data["post"] = post
url := CreatePostLink(post)
filePath := path.Join(config.GlobalConf.Html, url)
err := utils.MkDir(filePath)
if err != nil {
panic(err)
}
fileName := path.Join(filePath, "index.html")
htmlFile, err := os.Create(fileName)
if err != nil {
panic(err)
}
t, err := template.New("main.tpl").Funcs(funcMaps).ParseFiles(config.GlobalConf.Theme+"/layout/post.tpl", config.GlobalConf.Theme+"/layout/main.tpl")
if err != nil {
panic(err)
}
err = t.Execute(htmlFile, data)
if err != nil {
panic(err)
}
}
}
// CompileArchive 编译归档页
func CompileArchive() {
subTitle := config.GlobalConf.ArchiveTitle
if len(strings.TrimSpace(subTitle)) == 0 {
data["subtitle"] = "article archiving"
} else {
data["subtitle"] = subTitle
}
data["archive"] = GetArchive()
data["categoryList"] = GetCategoryList()
data["tagList"] = GetTagList()
filePath := path.Join(config.GlobalConf.Html, "archive")
err := utils.MkDir(filePath)
if err != nil {
panic(err)
}
fileName := path.Join(filePath, "index.html")
htmlFile, err := os.Create(fileName)
if err != nil {
panic(err)
}
t, err := template.New("main.tpl").Funcs(funcMaps).ParseFiles(config.GlobalConf.Theme+"/layout/archive.tpl", config.GlobalConf.Theme+"/layout/main.tpl")
if err != nil {
panic(err)
}
err = t.Execute(htmlFile, data)
if err != nil {
panic(err)
}
}
================================================
FILE: core/service/post.go
================================================
package service
import (
"bufio"
"bytes"
"fmt"
"gopkg.in/yaml.v3"
"io"
"lacerate/core/config"
"lacerate/core/log"
"lacerate/core/model"
"lacerate/core/utils"
"os"
"path"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
)
// 文章列表
var postList []*model.Post
// Content markdown内容
type Content struct {
Title string
Description string
Date string
Categories []string
Tags []string
Content string
}
// GetPostList 获取文章列表
func GetPostList() []*model.Post {
return postList
}
// CreateMarkdown 创建markdown文件
func CreateMarkdown(filename string) string {
file := path.Join(config.GlobalConf.Markdown, filename+".md")
_, err := os.Stat(file)
if !os.IsNotExist(err) {
log.Log.Errorf("The file already exists.")
os.Exit(1)
}
src, err := utils.CreateFile(config.GlobalConf.Markdown, filename+".md")
if err != nil {
panic(err)
}
date := time.Now().Format("2006-01-02")
now := time.Now().Format("15:04:05")
informationContent := `---
date: ` + date + `
time: ` + now + `
title: ` + filename + `
categories:
-
tagList:
-
-
---`
err = utils.WriteFile(src, informationContent)
if err != nil {
panic(err)
}
return src
}
// MarkdownList 获取markdown文件夹下所有文件
func MarkdownList(markdownDir string) (markdownList []string) {
_ = filepath.Walk(markdownDir, func(path string, f os.FileInfo, err error) error {
if err != nil { //忽略错误
return err
}
if f.IsDir() {
return nil
}
//if strings.ToLower(f.Name()) == "readme.md" {
// return nil
//}
if f.Name() == "about.md" {
return nil
}
if strings.HasSuffix(f.Name(), ".md") {
markdownList = append(markdownList, path)
}
return nil
})
return markdownList
}
// LoadPostList 加载文章列表
func LoadPostList() {
postList = make([]*model.Post, 0)
markdownList := MarkdownList(config.GlobalConf.Markdown)
for _, markdown := range markdownList {
post, err := loadMarkdownContent(markdown)
if err == nil {
post.Url = CreatePostLink(post)
postList = append(postList, post)
for _, _cate := range post.Category {
if len(_cate) <= 0 {
continue
}
category := categoryList[_cate]
if category == nil {
category = &model.Category{Count: 0, Name: _cate, Posts: make([]*model.Post, 0), Url: "/category/" + _cate}
categoryList[_cate] = category
}
category.Count += 1
category.Posts = append(category.Posts, post)
}
for _, _tag := range post.Tags {
if len(_tag) <= 0 {
continue
}
tag := tagList[_tag]
if tag == nil {
tag = &model.Tag{Count: 0, Name: _tag, Posts: make([]*model.Post, 0), Url: "/tag/" + _tag}
tagList[_tag] = tag
}
tag.Count += 1
tag.Posts = append(tag.Posts, post)
}
} else {
panic(err)
}
}
sort.Sort(model.PostList(postList))
}
// 加载markdown内容生成文章
func loadMarkdownContent(file string) (post *model.Post, err error) {
post = &model.Post{}
content, err := ReadMarkdownContent(file)
if err != nil {
return nil, err
}
if post.Summary == "" {
summaryLine := config.GlobalConf.SummaryLine
post.Summary, err = generateSummary(content.Content, summaryLine)
if err != nil {
return nil, err
}
}
post.Title = content.Title
post.Description = content.Description
post.Category = content.Categories
post.Tags = content.Tags
post.Content = utils.MarkdownToHtml(content.Content)
post.CreatedAt = utils.Str2Unix("2006-01-02", content.Date)
return post, nil
}
// 生成摘要
func generateSummary(content string, lines int) (string, error) {
buff := bufio.NewReader(bytes.NewBufferString(content))
dst := ""
for lines > 0 {
line, err := buff.ReadString('\n')
if err != nil || io.EOF == err {
break
}
if strings.Contains(strings.ToLower(line), "[toc]") {
continue
}
reg := regexp.MustCompile(`!\[(.*)\]\((.*)\)`)
if reg.MatchString(line) {
continue
}
if strings.Trim(line, "\r\n\t ") == "```" {
continue
}
dst += line
lines--
}
return utils.MarkdownToHtml(dst), nil
}
// ReadMarkdownContent 读取markdown内容
func ReadMarkdownContent(path string) (content *Content, err error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer func(f *os.File) {
_ = f.Close()
}(f)
br := bufio.NewReader(f)
line, err := br.ReadString('\n')
if err != nil {
return nil, err
}
if !strings.HasPrefix(line, "---") {
err = fmt.Errorf("markdown file format error, the file header must start with '---': " + path)
return nil, err
}
buf := bytes.NewBuffer(nil)
for {
line, err = br.ReadString('\n')
if err != nil {
if err != io.EOF {
return nil, err
}
}
if strings.HasPrefix(line, "---") {
break
}
buf.WriteString(line)
}
err = yaml.Unmarshal(buf.Bytes(), &content)
contentByte, err := io.ReadAll(br)
if err != nil {
return nil, err
}
fi, _ := f.Stat()
if content.Title == "" {
content.Title = strings.Replace(strings.TrimRight(fi.Name(), ".md"), config.GlobalConf.Markdown+"/", "", 1)
}
if content.Date == "" {
content.Date = utils.Format(fi.ModTime().Unix())
}
content.Content = string(contentByte)
return
}
// CreatePostLink 创建文章链接
func CreatePostLink(art *model.Post) string {
t := time.Unix(art.CreatedAt, 0)
year, month, day := t.Date()
link := fmt.Sprintf("/%s/%d/%d/%d/%s/", "post", year, month, day, utils.Convert(art.Title))
return link
}
// GetHomePostList 获取首页文章列表
func GetHomePostList() []*model.Post {
num := config.GlobalConf.HomePostNum
if num == 0 || len(postList) <= num {
num = len(postList)
}
homePostList := make([]*model.Post, num)
copy(homePostList, postList)
return homePostList
}
================================================
FILE: core/service/tag.go
================================================
package service
import (
"lacerate/core/model"
)
// 标签列表
var tagList map[string]*model.Tag
// 初始化
func init() {
tagList = make(map[string]*model.Tag)
}
// GetTagList 获取标签列表
func GetTagList() map[string]*model.Tag {
return tagList
}
================================================
FILE: core/service/watcher.go
================================================
package service
import (
"github.com/fsnotify/fsnotify"
"lacerate/core/log"
"os"
"strings"
"time"
)
var (
// 文件事件与事件时间字典
eventTime = make(map[string]int64)
// 触发编译时间
scheduleTime time.Time
)
// Watch 文件监控
type Watch struct {
Paths []string // 监控文件路径
Suffix []string // 监控文件后缀
}
// NewWatch 新建文件监控
func NewWatch(paths []string, suffix []string) *Watch {
return &Watch{paths, suffix}
}
// Watcher 文件监控
func (w *Watch) Watcher() {
// 初始化监听器
log.Log.Info("initialize the file listener...")
watcher, err := fsnotify.NewWatcher()
if err != nil {
panic("failed to initialize the file listener: " + err.Error())
}
go func() {
for {
select {
case event := <-watcher.Events:
build := true
if !w.checkFileSuffix(event.Name) {
continue
}
if event.Op&fsnotify.Chmod == fsnotify.Chmod {
log.Log.Infof(" skin %s ", event)
continue
}
mt := w.getFileModTime(event.Name)
if t := eventTime[event.Name]; mt == t {
log.Log.Infof(" skin %s ", event.String())
build = false
}
eventTime[event.Name] = mt
if build {
go func() {
scheduleTime = time.Now().Add(1 * time.Second)
for {
time.Sleep(scheduleTime.Sub(time.Now()))
if time.Now().After(scheduleTime) {
break
}
return
}
log.Log.Infof("triggers a compilation event: %s ", event)
Compile()
}()
}
case err := <-watcher.Errors:
log.Log.Errorf("monitoring failed %s ", err)
}
}
}()
for _, path := range w.Paths {
log.Log.Infof("listen to folders: [%s] ", path)
err = watcher.Add(path)
if err != nil {
log.Log.Errorf("failed to monitor folder: [%s] ", err)
os.Exit(2)
}
}
log.Log.Debug("the monitoring is successfully initialized...")
}
// 校验文件后缀名
func (w *Watch) checkFileSuffix(name string) bool {
for _, s := range w.Suffix {
if strings.HasSuffix(name, "."+s) {
return true
}
}
return false
}
// 获取文件最后更新时间
func (w *Watch) getFileModTime(path string) int64 {
path = strings.Replace(path, "\\", "/", -1)
f, err := os.Open(path)
if err != nil {
log.Log.Errorf("the file failed to open [ %s ]", err)
return time.Now().Unix()
}
defer func(f *os.File) {
_ = f.Close()
}(f)
fi, err := f.Stat()
if err != nil {
log.Log.Errorf("unable to get file information [ %s ]", err)
return time.Now().Unix()
}
return fi.ModTime().Unix()
}
================================================
FILE: core/utils/crypto.go
================================================
package utils
import (
"crypto/md5"
"encoding/hex"
)
// Xmd5 md5编码
func Xmd5(text string) string {
ctx := md5.New()
ctx.Write([]byte(text))
return hex.EncodeToString(ctx.Sum(nil))
}
================================================
FILE: core/utils/file.go
================================================
package utils
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"path"
)
// CreateFile 创建文件
func CreateFile(dir string, name string) (string, error) {
src := path.Join(dir, name)
_, err := os.Stat(src)
if os.IsExist(err) {
return src, nil
}
if err := os.MkdirAll(dir, 0777); err != nil {
if os.IsPermission(err) {
panic("insufficient permissions")
}
return "", err
}
_, err = os.Create(src)
if err != nil {
return "", err
}
return src, nil
}
// MkDir 创建路径
func MkDir(filepath string) error {
if _, err := os.Stat(filepath); err != nil {
if os.IsNotExist(err) {
err = os.MkdirAll(filepath, os.ModePerm)
if err != nil {
return err
}
} else {
return err
}
}
return nil
}
// CopyFile 复制文件
func CopyFile(src, des string) (w int64, err error) {
srcFile, err := os.Open(src)
if err != nil {
return 0, err
}
defer func(srcFile *os.File) {
_ = srcFile.Close()
}(srcFile)
desFile, err := os.Create(des)
if err != nil {
return 0, err
}
defer func(desFile *os.File) {
_ = desFile.Close()
}(desFile)
return io.Copy(desFile, srcFile)
}
// CopyDir 复制路径
func CopyDir(source string, dest string) (err error) {
fi, err := os.Stat(source)
if err != nil {
return err
}
if !fi.IsDir() {
return errors.New("source is not a directory")
}
_, err = os.Open(dest)
if os.IsExist(err) {
err = os.RemoveAll(dest)
if err != nil {
return err
}
}
err = os.MkdirAll(dest, fi.Mode())
if err != nil {
return err
}
entries, err := os.ReadDir(source)
for _, entry := range entries {
sfp := source + "/" + entry.Name()
dfp := dest + "/" + entry.Name()
if entry.IsDir() {
err = CopyDir(sfp, dfp)
if err != nil {
panic(err)
}
} else {
_, err = CopyFile(sfp, dfp)
if err != nil {
panic(err)
}
}
}
return
}
// WriteFile 写文件
func WriteFile(file string, text string) error {
f, err := os.OpenFile(file, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0660)
if err != nil {
_ = fmt.Errorf("open file error: %s", err)
}
defer func(f *os.File) {
_ = f.Close()
}(f)
w := bufio.NewWriter(f)
_, err = w.Write([]byte(text))
if err != nil {
return err
}
return w.Flush()
}
================================================
FILE: core/utils/markdown.go
================================================
package utils
import (
bf "github.com/russross/blackfriday"
"log"
"regexp"
"strings"
)
// TocTitle 目录标题
var TocTitle = "目录: "
// nav正则
var navRegex = regexp.MustCompile(`(?ismU)(.*) `)
// MarkdownToHtml markdown转html
func MarkdownToHtml(content string) (str string) {
defer func() {
e := recover()
if e != nil {
str = content
log.Println("Render markdown err:", e)
}
}()
htmlFlags := 0
if strings.Contains(strings.ToLower(content), "[toc]") {
htmlFlags |= bf.HTML_TOC
}
htmlFlags |= bf.HTML_USE_XHTML
htmlFlags |= bf.HTML_USE_SMARTYPANTS
htmlFlags |= bf.HTML_SMARTYPANTS_FRACTIONS
htmlFlags |= bf.HTML_SMARTYPANTS_LATEX_DASHES
htmlFlags |= bf.HTML_FOOTNOTE_RETURN_LINKS
renderer := bf.HtmlRenderer(htmlFlags, "", "")
extensions := 0
extensions |= bf.EXTENSION_NO_INTRA_EMPHASIS
extensions |= bf.EXTENSION_TABLES
extensions |= bf.EXTENSION_FENCED_CODE
extensions |= bf.EXTENSION_AUTOLINK
extensions |= bf.EXTENSION_STRIKETHROUGH
extensions |= bf.EXTENSION_SPACE_HEADERS
extensions |= bf.EXTENSION_HARD_LINE_BREAK
extensions |= bf.EXTENSION_FOOTNOTES
str = string(bf.Markdown([]byte(content), renderer, extensions))
if htmlFlags&bf.HTML_TOC != 0 {
found := navRegex.FindIndex([]byte(str))
if len(found) > 0 {
toc := str[found[0]:found[1]]
toc = TocTitle + toc
str = str[found[1]:]
reg := regexp.MustCompile(`\[toc\]|\[TOC\]`)
str = reg.ReplaceAllString(str, toc)
}
}
return str
}
================================================
FILE: core/utils/slice.go
================================================
package utils
// Count 统计分片长度
func Count(sl []string) (num int) {
num = 0
for _, s := range sl {
if s != "" {
num += 1
}
}
return
}
// Lt 判断小于
func Lt(a, b int) bool { return a < b }
// Eq 判断等于
func Eq(a, b int) bool { return a == b }
// Gt 判断大于
func Gt(a, b int) bool { return a > b }
================================================
FILE: core/utils/storage.go
================================================
package utils
import (
"bufio"
"encoding/json"
"os"
"path"
)
// Storage 文件存储
type Storage struct {
storagePath string // 文件存储路径
name string // 文件名
}
// NewStorage 新建文件存储
func NewStorage(storagePath, fileName string) (*Storage, error) {
if _, err := os.Stat(storagePath); err != nil {
if os.IsNotExist(err) {
err = os.MkdirAll(storagePath, os.ModePerm)
if err != nil {
return nil, err
}
} else {
return nil, err
}
}
return &Storage{storagePath: storagePath, name: fileName}, nil
}
// Get 解析文件存储
func (s *Storage) Get(value interface{}) error {
var filepath = path.Join(s.storagePath, s.name)
return storageRead(filepath, value)
}
// Store 缓存文件存储
func (s *Storage) Store(value interface{}) error {
var filepath = path.Join(s.storagePath, s.name)
return storageWrite(filepath, value)
}
// Del 删除文件存储
func (s *Storage) Del() error {
var filepath = path.Join(s.storagePath, s.name)
return os.Remove(filepath)
}
// 读文件存储
func storageRead(storagePath string, value interface{}) error {
f, err := os.OpenFile(storagePath, os.O_RDWR, 0666)
defer func(f *os.File) {
_ = f.Close()
}(f)
if err != nil {
return err
}
return json.NewDecoder(bufio.NewReader(f)).Decode(&value)
}
// 写文件存储
func storageWrite(storagePath string, value interface{}) error {
content, err := json.Marshal(value)
if err != nil {
return err
}
return os.WriteFile(storagePath, content, os.ModePerm)
}
================================================
FILE: core/utils/string.go
================================================
package utils
import (
"strings"
)
// Convert 文章标题转换文章链接
func Convert(str string) string {
str = strings.ToLower(str)
ss := strings.SplitN(str, " ", -1)
return strings.Join(ss, "-")
}
================================================
FILE: core/utils/template.go
================================================
package utils
import "html/template"
// Unescaped 解析html
func Unescaped(x string) interface{} {
return template.HTML(x)
}
================================================
FILE: core/utils/time.go
================================================
package utils
import "time"
// Format 日期格式化
func Format(unix int64) string {
t := time.Unix(unix, 0)
return t.Format("2006-01-02")
}
// Month 月份格式化
func Month(unix int64) string {
t := time.Unix(unix, 0)
return t.Format("1")
}
// Year 年份格式化
func Year(unix int64) string {
t := time.Unix(unix, 0)
return t.Format("2006")
}
// CMonth 日期格式化(不带年份)
func CMonth(unix int64) string {
t := time.Unix(unix, 0)
return t.Format("01-02")
}
// Str2Unix 字符串转时间
func Str2Unix(layout, timeStr string) int64 {
tm, _ := time.Parse(layout, timeStr)
return tm.Unix()
}
================================================
FILE: doc/博客编写指南.md
================================================
博客(markdown)需要以 `---` 开头进行说明:
```md
---
date: 日期 xxxx-xx-xx
title: 标题
categories:
- 分类
tags:
- 标签
---
```
================================================
FILE: doc/配置文件说明.md
================================================
`config.yml`配置说明:
```yaml
# 站点信息
title: 站点标题
subtitle: 站点子标题
description: 站点描述
keywords: 站点关键字
# 作者信息
author: 作者名称
avatar: 头像图标
github: github地址
email: 邮箱
# 配置信息
summary_line: 首页文章行数
home_post_num: 首页文章数量
# 文件存储
theme: 模板文件夹
markdown: markdown文件夹
html: html文件夹
storage: 存储文件夹
# 文件监听
paths:
- 监听文件
suffix:
- 监听文件后缀
# 自定义信息
home_title: 主页子标题
archive_title: 归档页面子标题
tag_title: 标签页面子标题
category_title: 分类页面子标题
about_title: 关于我页面子标题
```
================================================
FILE: go.mod
================================================
module lacerate
go 1.22
require (
github.com/fatih/color v1.16.0
github.com/fsnotify/fsnotify v1.7.0
github.com/russross/blackfriday v1.6.0
github.com/sirupsen/logrus v1.9.3
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.18.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
================================================
FILE: go.sum
================================================
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: theme/blog/assets/css/basic.css
================================================
html {
height: 100%;
max-height: 100%;
padding: 0;
margin: 0; }
body {
padding: 0;
margin: 0;
line-height: 1.6em; }
.clear {
clear: both;
display: block;
overflow: hidden;
visibility: hidden;
width: 0;
height: 0; }
h1, h2, h3, h4, h5, h6 {
text-rendering: optimizeLegibility;
line-height: 1;
margin: 2rem 0; }
h1 {
font-size: 2.1rem;
line-height: 1.2em; }
h2 {
font-size: 1.9rem;
line-height: 1.2em; }
h3 {
font-size: 1.75rem; }
h4 {
font-size: 1.3rem; }
h5 {
font-size: 1.3rem; }
h6 {
font-size: 1.3rem; }
img {
max-width: 100%;
height: auto; }
p, ul, ol, dl {
margin: 1em 0; }
ol ol, ul ul, ul ol, ol ul {
margin: 0.4em 0; }
ul p, ol p, li p, .content li p, blockquote p, .content blockquote p,
.post blockquote p, .post li p {
margin: 0;
overflow: visible; }
a img {
border: none; }
dl dt {
float: left;
width: 180px;
overflow: hidden;
clear: left;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: bold;
margin-bottom: 1em; }
dl dd {
margin-left: 200px;
margin-bottom: 1em; }
hr {
display: block;
height: 1px;
border: 0;
border-top: 1px solid #efefef;
margin: 3.2em 0;
padding: 0; }
blockquote {
-moz-box-sizing: border-box;
box-sizing: border-box;
margin: 1.6em 0 1.6em -2.3em;
padding: 0 0 0 1.6em;
border-radius: 0.4em;
border-left: #FF6600 0.4em solid; }
blockquote p {
margin: 0.8em 0; }
blockquote small {
display: inline-block;
margin: 0.8em 0 0.8em 1.5em;
font-size: 0.9em;
color: #ccc; }
blockquote small:before {
content: '\2014 \00A0'; }
blockquote cite {
font-weight: bold; }
blockquote cite a {
font-weight: normal; }
mark {
background-color: #ffc336; }
code, tt {
padding: 1px 3px;
font-family: Inconsolata, monospace, sans-serif;
font-size: 0.85em;
white-space: pre-wrap;
border: 1px solid #E3EDF3;
background: #f7f7f9;
color: #d14;
border-radius: 2px; }
.label {
padding: 1px 5px 1px;
font-size: 11.25px;
font-weight: bold;
font-family: 'Lato', "Open Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
color: #fff;
text-transform: uppercase;
background-color: #999;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
margin-left: 1mm;
}
.label-important {
background-color: #b94a48;
margin-right: 2mm;
}
pre {
-moz-box-sizing: border-box;
box-sizing: border-box;
margin: 1.6em 0;
border: 1px solid #E3EDF3;
width: 100%;
padding: 10px;
font-family: Inconsolata, monospace, sans-serif;
font-size: 0.9em;
white-space: pre;
overflow: auto;
background: #F7FAFB;
border-radius: 3px; }
pre code, tt {
font-size: inherit;
white-space: -moz-pre-wrap;
white-space: pre-wrap;
background: transparent;
border: none;
color: #333;
padding: 0; }
kbd {
display: inline-block;
margin-bottom: 0.4em;
padding: 1px 8px;
border: #ccc 1px solid;
color: #666;
text-shadow: #fff 0 1px 0;
font-size: 0.9em;
font-weight: bold;
background: #f4f4f4;
border-radius: 4px;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 1px 0 0 white inset; }
table {
-moz-box-sizing: border-box;
box-sizing: border-box;
margin: 1em 0;
width: 100%;
max-width: 100%;
border-width: 1px;
border-style: solid;
background-color: transparent; }
table, table tr, table tr td, table tr th {
border-color: #e5e5e5; }
table th {
color: #666666;
background-color: #fdfdfd; }
tr th {
border-bottom-width: 1px;
border-bottom-style: solid;
text-align: left; }
tr th, tr td {
padding: 5px 20px;
border-right: 1px solid;
font-size: 1rem; }
tr th:last-child, tr td:last-child {
border-right: 0px; }
table th {
font-weight: bold; }
table tbody > tr:nth-child(odd) > td, table tbody > tr:nth-child(odd) > th {
background-color: #f9f9f9; }
.gist {
font-size: 12px; }
.gist table {
margin: 0;
width: auto; }
.gist table pre {
font-size: 12px; }
.gist table .line-numbers {
font-size: 12px; }
.codehilitetable {
margin: 0;
width: auto; }
.codehilitetable tr th {
border: none; }
.codehilitetable tr th, .codehilitetable tr td {
padding: 0;
border: none; }
.codehilitetable .linenos pre {
background: transparent;
border: none; }
.codehilitetable pre {
margin: 0; }
.toc {
border: 1px solid #f0f0f0;
margin-bottom: 20px;
padding: 10px 30px; }
#fb_comments_container {
overflow: hidden;
margin: 0 auto; }
#fb_comments_container #fb_comments {
list-style-type: none;
padding: 0; }
#fb_comments_container #fb_comments h1 {
font-size: 1.3em; }
#fb_comments_container #fb_comments h2 {
font-size: 1.2em; }
#fb_comments_container #fb_comments h3 {
font-size: 1.1em; }
#fb_comments_container #fb_comments h4, #fb_comments_container #fb_comments h5,
#fb_comments_container #fb_comments h6 {
font-size: 1.05em; }
#fb_comments_container #fb_comments .comment {
position: relative;
padding: 25px 0;
border-bottom: 1px solid rgba(150, 150, 150, 0.2);
*border-bottom: 1px solid #f0f0f0; }
#fb_comments_container #fb_comments .comment .avatar {
position: absolute;
top: 25px;
left: 0;
width: 50px;
float: left; }
#fb_comments_container #fb_comments .comment .avatar img {
width: 48px;
border: none;
border-radius: 5px;
margin: 0; }
#fb_comments_container #fb_comments .comment .comment_body,
#fb_comments_container #fb_comments .comment .c_content {
margin-left: 70px;
display: block; }
#fb_comments_container #fb_comments .comment .comment_body p,
#fb_comments_container #fb_comments .comment .c_content p {
margin: 5px 0 15px 0;
padding: 0;
line-height: 1.8; }
#fb_comments_container #fb_comments .comment .comment_body .author,
#fb_comments_container #fb_comments .comment .c_content .author {
line-height: 1.5em;
margin: 0;
padding: 0; }
#fb_comments_container #fb_comments .comment .comment_body .author b,
#fb_comments_container #fb_comments .comment .c_content .author b {
color: #555; }
#fb_comments_container #fb_comments .comment .comment_body .author small,
#fb_comments_container #fb_comments .comment .c_content .author small {
font-weight: normal;
padding-left: 10px;
font-size: 0.7em;
color: #666; }
#fb_new_comment {
padding-bottom: 50px; }
#fb_new_comment textarea {
border-radius: 5px;
height: 80px;
width: 98%;
padding: 5px;
font-size: 1em;
border: 1px solid rgba(150, 150, 150, 0.5);
*border: 1px solid #a8a8a8;
line-height: 1.5; }
#fb_new_comment .comment_error {
color: red;
text-align: center;
display: block;
font-size: 0.8em;
padding-top: 1em; }
#fb_new_comment .c_button:hover {
background: #E60900;
color: #fff;
text-decoration: none; }
#fb_new_comment .c_button, #fb_new_comment #c_submit {
cursor: pointer;
font-family: "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
font-size: 1em;
line-height: 1.3em;
letter-spacing: 1px;
border-radius: 5px;
padding: 5px 5px 2px 5px; }
#fb_new_comment .input_body {
margin-top: 10px; }
#fb_new_comment .input_body ul {
list-style: none;
padding: 5px 0;
margin: auto 0; }
#fb_new_comment .input_body ul li {
float: left;
margin-right: 2.2%;
*margin-right: 22px; }
#fb_new_comment .input_body ul li label {
line-height: 1em; }
#fb_new_comment .input_body ul li input {
border-radius: 5px;
border: 1px solid #ddd;
padding: 5px;
background: rgba(255, 255, 255, 0.5);
margin: 0 0 10px 0; }
#SwfStore_farbox_0 {
height: 0;
overflow: hidden; }
@media screen and (max-width: 320px) {
#fb_comments .c_content, #fb_comments .comment_body {
margin-left: 57px; }
}
.codehilite code, .codehilite pre {
word-break: break-word;
color: #fdce93;
background-color: #3f3f3f;
padding: 10px;
border-radius: 3px; }
.codehilite .hll {
background-color: #222; }
.codehilite .c {
color: #7f9f7f; }
.codehilite .err {
color: #e37170;
background-color: #3d3535; }
.codehilite .g {
color: #7f9f7f; }
.codehilite .k {
color: #f0dfaf; }
.codehilite .l {
color: #ccc; }
.codehilite .n {
color: #dcdccc; }
.codehilite .o {
color: #f0efd0; }
.codehilite .x {
color: #ccc; }
.codehilite .p {
color: #41706f; }
.codehilite .cm {
color: #7f9f7f; }
.codehilite .cp {
color: #7f9f7f; }
.codehilite .c1 {
color: #7f9f7f; }
.codehilite .cs {
color: #cd0000;
font-weight: bold; }
.codehilite .gd {
color: #cd0000; }
.codehilite .ge {
color: #ccc;
font-style: italic; }
.codehilite .gr {
color: red; }
.codehilite .gh {
color: #dcdccc;
font-weight: bold; }
.codehilite .gi {
color: #00cd00; }
.codehilite .go {
color: gray; }
.codehilite .gp {
color: #dcdccc;
font-weight: bold; }
.codehilite .gs {
color: #ccc;
font-weight: bold; }
.codehilite .gu {
color: purple;
font-weight: bold; }
.codehilite .gt {
color: #0040D0; }
.codehilite .kc {
color: #dca3a3; }
.codehilite .kd {
color: #ffff86; }
.codehilite .kn {
color: #dfaf8f;
font-weight: bold; }
.codehilite .kp {
color: #cdcf99; }
.codehilite .kr {
color: #cdcd00; }
.codehilite .kt {
color: #00cd00; }
.codehilite .ld {
color: #cc9393; }
.codehilite .m {
color: #8cd0d3; }
.codehilite .s {
color: #cc9393; }
.codehilite .na {
color: #9ac39f; }
.codehilite .nb {
color: #efef8f; }
.codehilite .nc {
color: #efef8f; }
.codehilite .no {
color: #ccc; }
.codehilite .nd {
color: #ccc; }
.codehilite .ni {
color: #c28182; }
.codehilite .ne {
color: #c3bf9f;
font-weight: bold; }
.codehilite .nf {
color: #efef8f; }
.codehilite .nl {
color: #ccc; }
.codehilite .nn {
color: #8fbede; }
.codehilite .nx {
color: #ccc; }
.codehilite .py {
color: #ccc; }
.codehilite .nt {
color: #9ac39f; }
.codehilite .nv {
color: #dcdccc; }
.codehilite .ow {
color: #f0efd0; }
.codehilite .w {
color: #ccc; }
.codehilite .mf {
color: #8cd0d3; }
.codehilite .mh {
color: #8cd0d3; }
.codehilite .mi {
color: #8cd0d3; }
.codehilite .mo {
color: #8cd0d3; }
.codehilite .sb {
color: #cc9393; }
.codehilite .sc {
color: #cc9393; }
.codehilite .sd {
color: #cc9393; }
.codehilite .s2 {
color: #cc9393; }
.codehilite .se {
color: #cc9393; }
.codehilite .sh {
color: #cc9393; }
.codehilite .si {
color: #cc9393; }
.codehilite .sx {
color: #cc9393; }
.codehilite .sr {
color: #cc9393; }
.codehilite .s1 {
color: #cc9393; }
.codehilite .ss {
color: #cc9393; }
.codehilite .bp {
color: #efef8f; }
.codehilite .vc {
color: #efef8f; }
.codehilite .vg {
color: #dcdccc; }
.codehilite .vi {
color: #ffffc7; }
.codehilite .il {
color: #8cd0d3; }
@media (max-width: 480px) {
code {
padding: 0;
margin: 0; }
}
================================================
FILE: theme/blog/assets/css/style.css
================================================
html {
background-color: #fff;
-webkit-font-smoothing: antialiased; }
body {
color: rgba(0, 0, 0, 0.5);
font-family: georgia,palatino,"Helvetica Neue", Helvetica, "Hiragino Sans GB", "STHeitiSC-Light",
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
font-size: 15px;
width: 100%;
margin: 0 auto 30px auto;
background-color: #fff; }
::selection{
background: #ec6200;
color:#FFF;
}
::-moz-selection{
background:#ec6200;
color:#FFF;
}
p {
line-height: 1.9em;
font-weight: 400;
font-size: 16px; }
a {
text-decoration: none;
}
a:link, a:visited {
opacity: 1;
-webkit-transition: all 0.15s linear;
-moz-transition: all 0.15s linear;
-o-transition: all 0.15s linear;
-ms-transition: all 0.15s linear;
transition: all 0.15s linear;
color: #424242; }
a:hover, a:active {
color: #555; }
.animated {
-webkit-animation-fill-mode: both;
-moz-animation-fill-mode: both;
-ms-animation-fill-mode: both;
-o-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation-duration: 1s;
-moz-animation-duration: 1s;
-ms-animation-duration: 1s;
-o-animation-duration: 1s;
animation-duration: 1s; }
.animated.hinge {
-webkit-animation-duration: 1s;
-moz-animation-duration: 1s;
-ms-animation-duration: 1s;
-o-animation-duration: 1s;
animation-duration: 1s; }
@-webkit-keyframes fadeInDown {
0% {
opacity: 0;
-webkit-transform: translateY(-20px); }
100% {
opacity: 1;
-webkit-transform: translateY(0); }
}
@-moz-keyframes fadeInDown {
0% {
opacity: 0;
-moz-transform: translateY(-20px); }
100% {
opacity: 1;
-moz-transform: translateY(0); }
}
@-o-keyframes fadeInDown {
0% {
opacity: 0;
-o-transform: translateY(-20px); }
100% {
opacity: 1;
-o-transform: translateY(0); }
}
@keyframes fadeInDown {
0% {
opacity: 0;
transform: translateY(-20px); }
100% {
opacity: 1;
transform: translateY(0); }
}
.fadeInDown {
-webkit-animation-name: fadeInDown;
-moz-animation-name: fadeInDown;
-o-animation-name: fadeInDown;
animation-name: fadeInDown; }
.content {
height: auto;
float: right;
width: 70%;
margin-top: 60px; }
.page-top {
width: 69.9%;
position: fixed;
right: 0;
z-index: 9999;
background-color: #fff;
height: 60px;
border-bottom: 1px solid #f2f2f2; }
.page-top .nav {
list-style: none;
padding: 18px 30px;
float: left;
font-size: 12px; }
.page-top .nav li {
position: relative;
display: initial;
padding-right: 20px; }
.page-top .nav a {
color: #5A5A5A; }
.page-top .nav a:hover {
color: #464545; }
.page-top .nav a.current {
color: #5A5A5A;
padding-bottom: 22px;
border-bottom: 1px solid #5A5A5A; }
.page-top .information {
float: right;
padding-top: 12px;
padding-right: 20px; }
.page-top .information .avatar {
float: right; }
.page-top .information .avatar img {
width: 32px;
height: 32px;
border-radius: 300px; }
.page-top .information .back_btn {
float: left;
padding-top: 5px;
margin-right: -10px; }
.page-top .information .back_btn li {
display: initial;
padding-right: 40px; }
.sidebar {
width: 30%;
-webkit-background-size: cover;
background-size: cover;
background-color: #fff;
height: 100%;
transition: 0.8s;
top: 0;
left: 0;
position: fixed;
border-right: 1px solid #f2f2f2; }
.sidebar .logo-title {
text-align: center;
padding-top: 240px; }
.sidebar .logo-title .description {
font-size: 14px;
color: #565654; }
.sidebar .logo-title .logo {
margin: 0 auto; }
.sidebar .logo-title .title h3 {
text-transform: uppercase;
font-size: 2rem;
font-weight: bold;
letter-spacing: 2px;
line-height: 1;
margin: 0; }
.sidebar .logo-title .title a {
text-decoration: none;
color: #464646;
font-weight: bold; }
.sidebar .social-links {
list-style: none;
padding: 0;
font-size: 14px;
text-align: center; }
.sidebar .social-links i {
margin-right: 3px; }
.sidebar .social-links li {
display: inline;
padding: 0 4px;
line-height: 0; }
.sidebar .social-links a {
color: #565654; }
.post {
background-color: #FFF;
margin: 30px; }
.post .post-title h1 {
text-transform: uppercase;
font-size: 30px;
letter-spacing: 5px;
line-height: 1; }
.post .post-title h2 {
text-transform: uppercase;
letter-spacing: 1px;
font-size: 28px;
line-height: 1;
font-weight: 600;
color: #5f5f5f; }
.post .post-title h3 {
text-transform: uppercase;
letter-spacing: 1px;
line-height: 1;
font-weight: 600;
color: #464646;
font-size: 22px;
margin: 0; }
.post .post-title a {
text-decoration: none;
letter-spacing: 1px;
color: #5f5f5f;
font-family: Open Sans; }
.post .post-content a {
text-decoration: none;
letter-spacing: 1px;
color: #4786D6; }
.post .post-content h3 {
color: #5F5F5F;
font-size: 22px;
font-weight: 600;
font-family: Open Sans; }
.post .post-content h4 {
color: #5F5F5F;
font-size: 16px;
font-family: Open Sans; }
.post .post-content img {
max-width: 100%;
min-width: 700px;
}
.post .post-footer {
padding: 0 0 30px 0;
border-bottom: 1px solid #FFD8C9; }
.post .post-footer .meta {
max-width: 100%;
height: 25px;
color: #bbbbbb; }
.post .post-footer .meta .info {
float: left;
font-size: 12px; }
.post .post-footer .meta .info .date {
margin-right: 5px; }
.post .post-footer .meta a {
text-decoration: none;
color: #bbbbbb; }
.post .post-footer .meta i {
margin-right: 3px; }
.post .post-footer .tags {
padding-bottom: 15px;
font-size: 13px; }
.post .post-footer .tags ul {
list-style-type: none;
display: inline;
margin: 0;
padding: 0; }
.post .post-footer .tags ul li {
list-style-type: none;
margin: 0;
padding-right: 5px;
display: inline; }
.post .post-footer .tags a {
text-decoration: none;
color: rgba(0, 0, 0, 0.44);
font-weight: 400; }
.post .post-footer .tags a:hover {
text-decoration: none; }
.pagination {
margin: 30px;
padding: 0px 0 56px 0;
border-bottom: 1px solid #f2f2f2; }
.pagination ul {
list-style: none;
margin: 0;
padding: 0;
height: 13px; }
.pagination ul li {
margin: 0 2px 0 2px;
display: inline;
line-height: 1; }
.pagination ul li a {
text-decoration: none; }
.pagination .pre {
float: left; }
.pagination .next {
float: right; }
.like-reblog-buttons {
float: right; }
.like-button {
float: right;
padding: 0 0 0 10px; }
.reblog-button {
float: right;
padding: 0; }
#install-btn {
position: fixed;
bottom: 0px;
right: 6px; }
#disqus_thread {
margin: 30px;
border-bottom: 1px solid #f2f2f2; }
.footer {
clear: both;
text-align: center;
font-size: 10px;
margin: 0 auto;
bottom: 0;
position: absolute;
width: 100%; }
.footer a {
color: #A6A6A6; }
.footer span {
color: #888;
}
.archive {
width: 100%; }
.list-with-title {
font-size: 14px;
margin: 30px;
padding: 0; }
.list-with-title li {
list-style-type: none;
padding: 0; }
.list-with-title .listing-title {
font-size: 24px;
color: #666666;
font-weight: 600;
line-height: 2.2em; }
.list-with-title .listing {
padding: 0; }
.list-with-title .listing .listing-post {
padding-bottom: 5px; }
.list-with-title .listing .listing-post .post-time {
float: right;
color: #C5C5C5; }
.list-with-title .listing .listing-post a {
color: #8F8F8F; }
.list-with-title .listing .listing-post a:hover {
color: #464545; }
.share {
padding-left: 30px;
display: flex;
width: 100%;
height: 60px;
display: -webkit-box; }
.evernote {
width: 32px;
height: 32px;
border-radius: 300px;
background-color: #3E3E3E;
margin-right: 5px; }
.evernote a {
color: #fff;
padding: 11px;
font-size: 12px; }
.evernote a:hover {
color: #ED6243;
padding: 11px; }
.weibo {
width: 32px;
height: 32px;
border-radius: 300px;
background-color: #ED6243;
margin-right: 5px; }
.weibo a {
color: #fff;
padding: 9px; }
.weibo a:hover {
color: #BD4226; }
.twitter {
width: 32px;
height: 32px;
border-radius: 300px;
background-color: #59C0FD;
margin-right: 5px; }
.twitter a {
color: #fff;
padding: 9px; }
.twitter a:hover {
color: #4B9ECE; }
.about {
margin: 30px; }
.about h3 {
font-size: 22px; }
.comment-count {
color: #666; }
.tab-community {
color: #666; }
.read_more {
font-size: 14px; }
.back-button {
padding-top: 30px;
max-width: 100px;
padding-left: 40px;
float: left; }
a.btn {
color: #868686;
font-weight: 400; }
.btn {
display: inline-block;
position: relative;
outline: 0;
color: rgba(0, 0, 0, 0.44);
background: transparent;
font-size: 14px;
text-align: center;
text-decoration: none;
cursor: pointer;
border: 1px solid rgba(0, 0, 0, 0.15);
white-space: nowrap;
font-weight: 400;
font-style: normal;
border-radius: 999em; }
.btn:hover {
display: inline-block;
position: relative;
outline: 0px;
color: #464545;
background: transparent;
font-size: 14px;
text-align: center;
text-decoration: none;
cursor: pointer;
border: 1px solid #464545;
white-space: nowrap;
font-weight: 400;
font-style: normal;
border-radius: 999em; }
[role="back"] {
padding: 0.5em 1.25em;
line-height: 1.666em; }
[role="home"] {
padding: 0.5em 1.25em;
line-height: 1.666em; }
[role="navigation"] {
padding: 0.5em 1.25em;
line-height: 1.666em; }
[role="tags"] {
padding: 6px 12px; }
.menu {
float: right;
padding-top: 30px; }
.menu .btn-down {
margin: 0px; }
.menu .btn-down li {
list-style: none;
width: 100px; }
.menu .btn-down li a {
display: inline-block;
position: relative;
padding: 0.5em 1.25em;
outline: 0;
color: rgba(0, 0, 0, 0.44);
background: transparent;
font-size: 14px;
text-align: center;
text-decoration: none;
cursor: pointer;
border: 1px solid rgba(0, 0, 0, 0.15);
white-space: nowrap;
font-weight: 400;
font-style: normal;
border-radius: 999em;
margin-top: 5px; }
.menu .btn-down li a:hover {
position: relative;
padding: 0.5em 1.25em;
outline: 0;
color: #fff;
background: #3CBD10;
font-size: 14px;
text-align: center;
text-decoration: none;
cursor: pointer;
border: 1px solid rgba(0, 0, 0, 0.15);
white-space: nowrap;
font-weight: 400;
font-style: normal;
border-radius: 999em;
margin-top: 5px; }
.menu .btn-down div {
position: absolute;
visibility: hidden;
width: 100px;
float: right; }
@media screen and (max-width: 414px) {
.sidebar {
width: 100%;
position: absolute;
border-right: none; }
.sidebar .logo-title {
padding-top: 100px; }
.sidebar .logo-title .title img {
width: 100px; }
.sidebar .logo-title .title h3 {
font-size: 20px; }
.page-top {
display: none; }
.content {
margin-top: 360px;
width: 100%;
overflow: hidden; }
.footer {
display: none; }
.share {
display: flex;
display: -webkit-box; }
}
.profile h2 {
margin: 20px 0;
font-family: 'Montserrat', sans-serif;
font-size: 16px;
text-transform: uppercase;
letter-spacing: 5px;
color: #109289;
}
.cate-list {
margin: 20px auto;
text-transform: uppercase;
text-align: center;
color: #109289;
width: 80%;
font-size: 0.85em;
font-family: 'Lato', "Open Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.cate-list .cate-title {
width: 48%;
}
.cate-list .cate-title a {
display: inline-block;
margin: 5px 0;
text-decoration: none;
color: dimgrey;
}
.meta .info a:hover {
color: #FF7838;
}
.post-nav {
margin: 30px 0;
}
.comment_container {
margin: 30px;
}
================================================
FILE: theme/blog/layout/archive.tpl
================================================
{{ define "content" }}
{{ range .archive }}
{{ .YearStr }}
{{ range .Months }}
{{ range .Posts }}
{{ .Title }}
{{ .CreatedAt | format }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
================================================
FILE: theme/blog/layout/category.tpl
================================================
{{ define "content" }}
{{ range .categoryList }}
{{ end }}
{{ end }}
================================================
FILE: theme/blog/layout/home.tpl
================================================
{{ define "content" }}
{{ range .postList }}
{{ .Summary | unescaped }}
{{ end }}
{{ end }}
================================================
FILE: theme/blog/layout/main.tpl
================================================
{{.title}}
================================================
FILE: theme/blog/layout/page.tpl
================================================
{{ define "content" }}
{{ .pageTitle }}({{ .count }})
{{ range .content }}
{{ end }}
{{ end }}
================================================
FILE: theme/blog/layout/post.tpl
================================================
{{ define "content" }}
{{ .post.Title }}
{{ .post.Content | unescaped }}
PERMANENT LINK:
https://happy.zj.cn{{.post.Url}}
{{ end }}
================================================
FILE: theme/blog/layout/tag.tpl
================================================
{{ define "content" }}
{{ range .tagList }}
{{ end }}
{{ end }}