Full Code of Foleyzhao/lacerate for AI

main 4f3ccd4f804b cached
40 files
82.2 KB
26.3k tokens
86 symbols
1 requests
Download .txt
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
================================================
<div align="center">
  <a href="https://happy.zj.cn/">
    <img src="doc/imgs/Lacerate.png" width="80px" height="80px">
  </a>
  <h1 align="center">
    Lacerate
  </h1>
  <h3 align="center">
    一个Goland编写的简单的静态博客生成器
  </h3>

[下 载](https://github.com/Foleyzhao/lacerate/releases) | [主 页](https://happy.zj.cn/)

  <a href="https://github.com/Foleyzhao/lacerate/releases/latest">
    <img src="https://img.shields.io/github/release/Foleyzhao/lacerate.svg?style=flat-square" alt="">
  </a>

  <a href="https://github.com/Foleyzhao/lacerate/master/LICENSE">
    <img src="https://img.shields.io/github/license/Foleyzhao/lacerate.svg?style=flat-square" alt="">
  </a>

  <a href="https://github.com/Foleyzhao/lacerate/releases/latest">
    <img alt="GitHub All Releases" src="https://img.shields.io/github/downloads/Foleyzhao/lacerate/total.svg?color=%2312b886&style=flat-square">
  </a>

</div>
<br>
<div align="center">
  <img src="doc/imgs/主页.png">
</div>
<br>

👏  欢迎使用 **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

## 示例截图
<div align="center">
  <img src="doc/imgs/主页.png">
</div>
<br>
<div align="center">
  <img src="doc/imgs/分类.png">
</div>
<br>
<div align="center">
  <img src="doc/imgs/归档.png">
</div>
<br>
<div align="center">
  <img src="doc/imgs/文章详情.png">
</div>
<br>
<div align="center">
  <img src="doc/imgs/关于我.png">
</div>

## 贡献
欢迎任何形式的贡献。可以使用 [pull requests](https://github.com/Foleyzhao/lacerate/pulls) 或 [issues](https://github.com/Foleyzhao/lacerate/issues) 的方式提交任何想法。

## 支持
<div>
  <img src="doc/imgs/WeChat.jpg" width="240px">
</div>

## 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 = "<h4>目录:</h4>"

// nav正则
var navRegex = regexp.MustCompile(`(?ismU)<nav>(.*)</nav>`)

// 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 }}
    <div class="archive animated fadeInDown">
        <div class="list-with-title">
            <div class="listing-title">{{ .YearStr }}</div>
            <ul class="listing">
                {{ range .Months }}
                <div class="listing-item">
                    {{ range .Posts }}
                    <div class="listing-post"><a href="{{ .Url }}" title="{{ .Title }}">{{ .Title }}</a>
                        <div class="post-time"><span class="date">{{ .CreatedAt | format }}</span>
                        </div>
                    </div>
                    {{ end }}
                </div>
                {{ end }}
            </ul>
        </div>
    </div>
    {{ end }}
{{ end }}

================================================
FILE: theme/blog/layout/category.tpl
================================================
{{ define "content" }}
<div class="archive animated fadeInDown">
    <div class="list-with-title">
        {{ range .categoryList }}
        <div class="listing-title">
            <a href="{{ .Url }}">
                {{ .Name }}({{ .Count }})
            </a>
        </div>
        {{ end }}
    </div>
</div>
{{ end }}

================================================
FILE: theme/blog/layout/home.tpl
================================================
{{ define "content" }}
    {{ range .postList }}
    <div class="post animated fadeInDown">
        <div class="post-title">
            <h3><a style="color: #0077FF" href="{{ .Url }}">{{ .Title }}</a>
            </h3>
        </div>
        <div class="post-content">
            <div class="p_part">
                {{ .Summary | unescaped }}
            </div>
        </div>
        <div class="post-footer">
            <div class="meta">
                <div class="info">
                    <i class="fa fa-calendar"></i>
                    <span class="date">{{ .CreatedAt | format }}</span>
                    &nbsp;&nbsp;
                    <i class="fa fa-folder-open"></i>
                    {{ range .Category }}
                    <a href="/category/{{ . }}">{{ . }}</a>&nbsp;
                    {{ end }}
                    &nbsp;&nbsp;
                    <i class="fa fa-tags"></i>
                    {{ range .Tags }}
                    <a href="/tag/{{ . }}">{{ . }}</a>&nbsp;
                    {{ end }}
                </div>
            </div>
        </div>
    </div>
    {{ end }}
{{ end }}

================================================
FILE: theme/blog/layout/main.tpl
================================================
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="keywords" content="{{ .keywords }}" />
    <meta name="description" content="{{ .description }}" />
    <link href="https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <link href="https://cdn.bootcss.com/highlight.js/9.15.8/styles/solarized-dark.min.css" rel="stylesheet">
    <link href="/assets/css/basic.css" rel="stylesheet">
    <link href="/assets/css/style.css" rel="stylesheet">
    <title>{{.title}}</title>
</head>
<style type="text/css">
    h1,h2,h3 {
        font-size: 22px;
    }
    ul li {
        list-style: none;
    }
</style>
<body style="zoom: 1;">
<!---left-start--->
<div class="sidebar">
    <div class="logo-title">
        <div class="title animated fadeInDown">
            <img src="{{ .avatar }}" style="width:127px;border-radius: 50%;">
            <hgroup>
                <h1 class="header-author"><a href="https://happy.zj.cn/">{{ .title }}</a></h1>
            </hgroup>
            <div class="description animated fadeInDown">
                <p>{{ .subtitle }}</p>
            </div>
        </div>
    </div>
    <ul class="social-links animated fadeInDown">
        <li><a href="{{ .github }}"><i class="fa fa-github"></i></a>
        </li>
    </ul>
    <div class="cate-list animated fadeInDown">
        分类:
        {{ range .categoryList }}
        <span class="cate-title">
                <a href="{{ .Url }}">
                    {{ .Name }}
                </a>&nbsp;
            </span>
        {{ end }}
    </div>
    <div class="footer">
        <!--footer-->
        <span>© HappyNewYear's Blog 2018 - 2024. &nbsp;&nbsp; 浙ICP备2023011627号-1</span>
        <br>
        <span>Powered by <a href="https://lacerate">Lacerate</a>. </span>
    </div>
</div>
<!---left-end--->
<!---right-start--->
<div class="main">
    <div class="page-top animated fadeInDown">
        <div class="nav">
            <li><a href="/">主页</a>
            </li>
            <li><a href="/archive">归档</a>
            </li>
            <li><a href="/about.html">关于我</a>
            </li>
        </div>
        <div class="information">
            <div class="back_btn">
            </div>
            <div class="avatar"><img src="{{ .avatar }}">
            </div>
        </div>
    </div>
    <div class="autopagerize_page_element">
        <div class="content">
            {{ template "content" . }}
            <div class="pagination">
                <ul class="clearfix">
                </ul>
            </div>
        </div>
    </div>
</div>
<!----right-end---->
<script src="https://cdn.bootcss.com/jquery/2.1.4/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
<script src="https://cdn.bootcss.com/highlight.js/9.15.8/highlight.min.js"></script>
<script src="https://cdn.bootcss.com/highlight.js/9.15.8/languages/go.min.js"></script>
<script type="text/javascript">
    $(document).ready(function() {
        $('pre code').each(function(i, block) {
            hljs.highlightBlock(block);
        });
    });
</script>
</body>
</html>

================================================
FILE: theme/blog/layout/page.tpl
================================================
{{ define "content" }}
<div class="archive animated fadeInDown">
    <div class="list-with-title">
        <div class="listing-title">{{ .pageTitle }}({{ .count }})</div>
        <ul class="listing">
            {{ range .content }}
            <div class="listing-item">
                <div class="listing-post"><a href="{{ .Url }}" title="{{ .Title }}">{{ .Title }}</a>
                    <div class="post-time">
                        <span class="date">{{ .CreatedAt | format }}</span>
                    </div>
                </div>
            </div>
            {{ end }}
        </ul>
    </div>
</div>
{{ end }}


================================================
FILE: theme/blog/layout/post.tpl
================================================
{{ define "content" }}
<div class="post-page">
    <div class="post animated fadeInDown">
        <div class="post-title">
            <h3>{{ .post.Title }}
            </h3>
        </div>
        <div class="post-content" id="content">
            {{ .post.Content | unescaped }}
            <nav class="article-nav" id="state">
                <span class="label label-important">PERMANENT LINK:</span>
                <a href="https://happy.zj.cn{{.post.Url}}">https://happy.zj.cn{{.post.Url}}</a>
            </nav>
        </div>
        <div class="post-footer">
            <div class="meta">
                <div class="info">
                    <i class="fa fa-calendar"></i>
                    <span class="date">{{ .post.CreatedAt | format }}</span>
                    &nbsp;&nbsp;
                    <i class="fa fa-folder-open"></i>
                    {{ range .post.Category }}
                    <a href="/category/{{ . }}">{{ . }}</a>&nbsp;
                    {{ end }}
                    &nbsp;&nbsp;
                    <i class="fa fa-tags"></i>
                    {{ range .post.Tags }}
                    <a href="/tag/{{ . }}">{{ . }}</a>&nbsp;
                    {{ end }}
                </div>
            </div>
        </div>
    </div>
    <!--评论-->
</div>
{{ end }}

================================================
FILE: theme/blog/layout/tag.tpl
================================================
{{ define "content" }}
<div class="archive animated fadeInDown">
    <div class="list-with-title">
        {{ range .tagList }}
        <div class="listing-title">
            <a href="{{ .Url }}">
                {{ .Name }}({{ .Count }})
            </a>
        </div>
        {{ end }}
    </div>
</div>
{{ end }}
Download .txt
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
Download .txt
SYMBOL INDEX (86 symbols across 23 files)

FILE: cmd/cmd.go
  function main (line 20) | func main() {

FILE: core/command/command.go
  constant HELP (line 14) | HELP = `
  function PrintHelp (line 40) | func PrintHelp() {
  function Initialize (line 45) | func Initialize() {
  function ListenHttpServer (line 52) | func ListenHttpServer(port int) {
  function CreateDir (line 65) | func CreateDir() {

FILE: core/config/config.go
  type SystemConfig (line 47) | type SystemConfig struct
  function loadConf (line 72) | func loadConf() ([]byte, error) {
  function CreateConf (line 89) | func CreateConf() {
  function Config (line 106) | func Config() SystemConfig {

FILE: core/log/logger.go
  function init (line 9) | func init() {

FILE: core/model/archive.go
  type PublishedYears (line 6) | type PublishedYears
    method Len (line 25) | func (y PublishedYears) Len() int {
    method Swap (line 29) | func (y PublishedYears) Swap(i, j int) {
    method Less (line 33) | func (y PublishedYears) Less(i, j int) bool {
  type PublishedMonths (line 9) | type PublishedMonths
    method Len (line 37) | func (m PublishedMonths) Len() int {
    method Swap (line 41) | func (m PublishedMonths) Swap(i, j int) {
    method Less (line 45) | func (m PublishedMonths) Less(i, j int) bool {
  type PublishedYear (line 12) | type PublishedYear struct
  type PublishedMonth (line 19) | type PublishedMonth struct

FILE: core/model/category.go
  type Category (line 4) | type Category struct

FILE: core/model/post.go
  type PostList (line 4) | type PostList
    method Len (line 20) | func (p PostList) Len() int {
    method Swap (line 24) | func (p PostList) Swap(i, j int) {
    method Less (line 28) | func (p PostList) Less(i, j int) bool {
  type Post (line 7) | type Post struct

FILE: core/model/tag.go
  type Tag (line 4) | type Tag struct

FILE: core/service/about.go
  function GetAbout (line 13) | func GetAbout() (post *model.Post, err error) {

FILE: core/service/archive.go
  function GetArchive (line 11) | func GetArchive() []*model.PublishedYear {

FILE: core/service/category.go
  function init (line 11) | func init() {
  function GetCategoryList (line 16) | func GetCategoryList() map[string]*model.Category {

FILE: core/service/compile.go
  function Compile (line 38) | func Compile() {
  function storageBlogMap (line 62) | func storageBlogMap() {
  function CompileHome (line 74) | func CompileHome() {
  function copyAssetsFile (line 104) | func copyAssetsFile() {
  function checkThemeFile (line 112) | func checkThemeFile() {
  function CompileCategoryPage (line 119) | func CompileCategoryPage() {
  function CompileCategory (line 149) | func CompileCategory() {
  function CompileTagPage (line 180) | func CompileTagPage() {
  function CompileTag (line 210) | func CompileTag() {
  function CompileAbout (line 242) | func CompileAbout() {
  function CompilePost (line 272) | func CompilePost() {
  function CompileArchive (line 303) | func CompileArchive() {

FILE: core/service/post.go
  type Content (line 26) | type Content struct
  function GetPostList (line 36) | func GetPostList() []*model.Post {
  function CreateMarkdown (line 41) | func CreateMarkdown(filename string) string {
  function MarkdownList (line 73) | func MarkdownList(markdownDir string) (markdownList []string) {
  function LoadPostList (line 97) | func LoadPostList() {
  function loadMarkdownContent (line 138) | func loadMarkdownContent(file string) (post *model.Post, err error) {
  function generateSummary (line 164) | func generateSummary(content string, lines int) (string, error) {
  function ReadMarkdownContent (line 190) | func ReadMarkdownContent(path string) (content *Content, err error) {
  function CreatePostLink (line 237) | func CreatePostLink(art *model.Post) string {
  function GetHomePostList (line 245) | func GetHomePostList() []*model.Post {

FILE: core/service/tag.go
  function init (line 11) | func init() {
  function GetTagList (line 16) | func GetTagList() map[string]*model.Tag {

FILE: core/service/watcher.go
  type Watch (line 19) | type Watch struct
    method Watcher (line 30) | func (w *Watch) Watcher() {
    method checkFileSuffix (line 87) | func (w *Watch) checkFileSuffix(name string) bool {
    method getFileModTime (line 97) | func (w *Watch) getFileModTime(path string) int64 {
  function NewWatch (line 25) | func NewWatch(paths []string, suffix []string) *Watch {

FILE: core/utils/crypto.go
  function Xmd5 (line 9) | func Xmd5(text string) string {

FILE: core/utils/file.go
  function CreateFile (line 13) | func CreateFile(dir string, name string) (string, error) {
  function MkDir (line 33) | func MkDir(filepath string) error {
  function CopyFile (line 48) | func CopyFile(src, des string) (w int64, err error) {
  function CopyDir (line 67) | func CopyDir(source string, dest string) (err error) {
  function WriteFile (line 106) | func WriteFile(file string, text string) error {

FILE: core/utils/markdown.go
  function MarkdownToHtml (line 17) | func MarkdownToHtml(content string) (str string) {

FILE: core/utils/slice.go
  function Count (line 4) | func Count(sl []string) (num int) {
  function Lt (line 15) | func Lt(a, b int) bool { return a < b }
  function Eq (line 18) | func Eq(a, b int) bool { return a == b }
  function Gt (line 21) | func Gt(a, b int) bool { return a > b }

FILE: core/utils/storage.go
  type Storage (line 11) | type Storage struct
    method Get (line 32) | func (s *Storage) Get(value interface{}) error {
    method Store (line 38) | func (s *Storage) Store(value interface{}) error {
    method Del (line 44) | func (s *Storage) Del() error {
  function NewStorage (line 17) | func NewStorage(storagePath, fileName string) (*Storage, error) {
  function storageRead (line 50) | func storageRead(storagePath string, value interface{}) error {
  function storageWrite (line 62) | func storageWrite(storagePath string, value interface{}) error {

FILE: core/utils/string.go
  function Convert (line 8) | func Convert(str string) string {

FILE: core/utils/template.go
  function Unescaped (line 6) | func Unescaped(x string) interface{} {

FILE: core/utils/time.go
  function Format (line 6) | func Format(unix int64) string {
  function Month (line 12) | func Month(unix int64) string {
  function Year (line 18) | func Year(unix int64) string {
  function CMonth (line 24) | func CMonth(unix int64) string {
  function Str2Unix (line 30) | func Str2Unix(layout, timeStr string) int64 {
Condensed preview — 40 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (94K chars).
[
  {
    "path": ".gitignore",
    "chars": 524,
    "preview": "# If you prefer the allow list template instead of the deny list, see community template:\n# https://github.com/github/gi"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 2564,
    "preview": "<div align=\"center\">\n  <a href=\"https://happy.zj.cn/\">\n    <img src=\"doc/imgs/Lacerate.png\" width=\"80px\" height=\"80px\">\n"
  },
  {
    "path": "cmd/cmd.go",
    "chars": 1285,
    "preview": "package main\n\nimport (\n\t\"flag\"\n\t\"github.com/fatih/color\"\n\t\"lacerate/core/command\"\n\t\"lacerate/core/common\"\n\t\"lacerate/cor"
  },
  {
    "path": "core/command/command.go",
    "chars": 1959,
    "preview": "package command\n\nimport (\n\t\"fmt\"\n\t\"lacerate/core/config\"\n\t\"lacerate/core/log\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n)\n\nconst (\n\t/"
  },
  {
    "path": "core/common/banner.go",
    "chars": 499,
    "preview": "package common\n\n// Banner banner\nvar Banner = `\n=========================================================\n ▄            "
  },
  {
    "path": "core/config/config.go",
    "chars": 2530,
    "preview": "package config\n\nimport (\n\t\"gopkg.in/yaml.v3\"\n\t\"io\"\n\t\"os\"\n)\n\nvar GlobalConf = Config()\n\nvar confFileName = \"config.yml\"\n\n"
  },
  {
    "path": "core/log/logger.go",
    "chars": 246,
    "preview": "package log\n\nimport \"github.com/sirupsen/logrus\"\n\n// Log 日志记录器\nvar Log = logrus.WithFields(logrus.Fields{})\n\n// 初始化\nfunc"
  },
  {
    "path": "core/model/archive.go",
    "chars": 940,
    "preview": "package model\n\nimport \"time\"\n\n// PublishedYears 年份归档列表\ntype PublishedYears []*PublishedYear\n\n// PublishedMonths 月份归档列表\nt"
  },
  {
    "path": "core/model/category.go",
    "chars": 172,
    "preview": "package model\n\n// Category 分类\ntype Category struct {\n\tCount int     `json:\"count\"`\n\tName  string  `json:\"name\"`\n\tPosts ["
  },
  {
    "path": "core/model/post.go",
    "chars": 668,
    "preview": "package model\n\n// PostList 文章列表\ntype PostList []*Post\n\n// Post 文章\ntype Post struct {\n\tId          int      `json:\"id\"`\n\t"
  },
  {
    "path": "core/model/tag.go",
    "chars": 162,
    "preview": "package model\n\n// Tag 标签\ntype Tag struct {\n\tCount int     `json:\"count\"`\n\tName  string  `json:\"name\"`\n\tPosts []*Post `js"
  },
  {
    "path": "core/service/about.go",
    "chars": 548,
    "preview": "package service\n\nimport (\n\t\"lacerate/core/config\"\n\t\"lacerate/core/model\"\n\t\"lacerate/core/utils\"\n\t\"os\"\n\t\"path\"\n\t\"time\"\n)\n"
  },
  {
    "path": "core/service/archive.go",
    "chars": 1218,
    "preview": "package service\n\nimport (\n\t\"lacerate/core/model\"\n\t\"lacerate/core/utils\"\n\t\"sort\"\n\t\"time\"\n)\n\n// GetArchive 获取归档信息\nfunc Get"
  },
  {
    "path": "core/service/category.go",
    "chars": 279,
    "preview": "package service\n\nimport (\n\t\"lacerate/core/model\"\n)\n\n// 分类列表\nvar categoryList map[string]*model.Category\n\n// 初始化\nfunc ini"
  },
  {
    "path": "core/service/compile.go",
    "chars": 8251,
    "preview": "package service\n\nimport (\n\t\"html/template\"\n\t\"lacerate/core/config\"\n\t\"lacerate/core/log\"\n\t\"lacerate/core/utils\"\n\t\"os\"\n\t\"p"
  },
  {
    "path": "core/service/post.go",
    "chars": 5569,
    "preview": "package service\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"gopkg.in/yaml.v3\"\n\t\"io\"\n\t\"lacerate/core/config\"\n\t\"lacerate/core/log"
  },
  {
    "path": "core/service/tag.go",
    "chars": 239,
    "preview": "package service\n\nimport (\n\t\"lacerate/core/model\"\n)\n\n// 标签列表\nvar tagList map[string]*model.Tag\n\n// 初始化\nfunc init() {\n\ttag"
  },
  {
    "path": "core/service/watcher.go",
    "chars": 2391,
    "preview": "package service\n\nimport (\n\t\"github.com/fsnotify/fsnotify\"\n\t\"lacerate/core/log\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n)\n\nvar (\n\t// 文件事"
  },
  {
    "path": "core/utils/crypto.go",
    "chars": 189,
    "preview": "package utils\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n)\n\n// Xmd5 md5编码\nfunc Xmd5(text string) string {\n\tctx := md5.New()"
  },
  {
    "path": "core/utils/file.go",
    "chars": 2161,
    "preview": "package utils\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path\"\n)\n\n// CreateFile 创建文件\nfunc CreateFile(dir string, "
  },
  {
    "path": "core/utils/markdown.go",
    "chars": 1463,
    "preview": "package utils\n\nimport (\n\tbf \"github.com/russross/blackfriday\"\n\t\"log\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n// TocTitle 目录标题\nvar TocTit"
  },
  {
    "path": "core/utils/slice.go",
    "chars": 301,
    "preview": "package utils\n\n// Count 统计分片长度\nfunc Count(sl []string) (num int) {\n\tnum = 0\n\tfor _, s := range sl {\n\t\tif s != \"\" {\n\t\t\tnu"
  },
  {
    "path": "core/utils/storage.go",
    "chars": 1428,
    "preview": "package utils\n\nimport (\n\t\"bufio\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path\"\n)\n\n// Storage 文件存储\ntype Storage struct {\n\tstoragePath st"
  },
  {
    "path": "core/utils/string.go",
    "chars": 191,
    "preview": "package utils\n\nimport (\n\t\"strings\"\n)\n\n// Convert 文章标题转换文章链接\nfunc Convert(str string) string {\n\tstr = strings.ToLower(str"
  },
  {
    "path": "core/utils/template.go",
    "chars": 125,
    "preview": "package utils\n\nimport \"html/template\"\n\n// Unescaped 解析html\nfunc Unescaped(x string) interface{} {\n\treturn template.HTML("
  },
  {
    "path": "core/utils/time.go",
    "chars": 565,
    "preview": "package utils\n\nimport \"time\"\n\n// Format 日期格式化\nfunc Format(unix int64) string {\n\tt := time.Unix(unix, 0)\n\treturn t.Format"
  },
  {
    "path": "doc/博客编写指南.md",
    "chars": 108,
    "preview": "博客(markdown)需要以 `---` 开头进行说明:\n\n```md\n\n---\ndate: 日期 xxxx-xx-xx\ntitle: 标题\ncategories:\n- 分类\ntags:\n- 标签\n---\n\n```"
  },
  {
    "path": "doc/配置文件说明.md",
    "chars": 438,
    "preview": "`config.yml`配置说明:\n\n```yaml\n\n# 站点信息\ntitle: 站点标题\nsubtitle: 站点子标题\ndescription: 站点描述\nkeywords: 站点关键字\n# 作者信息\nauthor: 作者名称\nava"
  },
  {
    "path": "go.mod",
    "chars": 464,
    "preview": "module lacerate\n\ngo 1.22\n\nrequire (\n\tgithub.com/fatih/color v1.16.0\n\tgithub.com/fsnotify/fsnotify v1.7.0\n\tgithub.com/rus"
  },
  {
    "path": "go.sum",
    "chars": 3785,
    "preview": "github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go"
  },
  {
    "path": "theme/blog/assets/css/basic.css",
    "chars": 11305,
    "preview": "html {\n    height: 100%;\n    max-height: 100%;\n    padding: 0;\n    margin: 0; }\n\nbody {\n    padding: 0;\n    margin: 0;\n "
  },
  {
    "path": "theme/blog/assets/css/style.css",
    "chars": 12521,
    "preview": "html {\n    background-color: #fff;\n    -webkit-font-smoothing: antialiased; }\n\nbody {\n    color: rgba(0, 0, 0, 0.5);\n   "
  },
  {
    "path": "theme/blog/layout/archive.tpl",
    "chars": 751,
    "preview": "{{ define \"content\" }}\n    {{ range .archive }}\n    <div class=\"archive animated fadeInDown\">\n        <div class=\"list-w"
  },
  {
    "path": "theme/blog/layout/category.tpl",
    "chars": 322,
    "preview": "{{ define \"content\" }}\n<div class=\"archive animated fadeInDown\">\n    <div class=\"list-with-title\">\n        {{ range .cat"
  },
  {
    "path": "theme/blog/layout/home.tpl",
    "chars": 1128,
    "preview": "{{ define \"content\" }}\n    {{ range .postList }}\n    <div class=\"post animated fadeInDown\">\n        <div class=\"post-tit"
  },
  {
    "path": "theme/blog/layout/main.tpl",
    "chars": 3299,
    "preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE"
  },
  {
    "path": "theme/blog/layout/page.tpl",
    "chars": 626,
    "preview": "{{ define \"content\" }}\n<div class=\"archive animated fadeInDown\">\n    <div class=\"list-with-title\">\n        <div class=\"l"
  },
  {
    "path": "theme/blog/layout/post.tpl",
    "chars": 1306,
    "preview": "{{ define \"content\" }}\n<div class=\"post-page\">\n    <div class=\"post animated fadeInDown\">\n        <div class=\"post-title"
  },
  {
    "path": "theme/blog/layout/tag.tpl",
    "chars": 317,
    "preview": "{{ define \"content\" }}\n<div class=\"archive animated fadeInDown\">\n    <div class=\"list-with-title\">\n        {{ range .tag"
  }
]

About this extraction

This page contains the full source code of the Foleyzhao/lacerate GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 40 files (82.2 KB), approximately 26.3k tokens, and a symbol index with 86 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!