djhx-blogger 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of djhx-blogger might be problematic. Click here for more details.
- djhx_blogger-0.1.4/LICENSE +21 -0
- djhx_blogger-0.1.4/PKG-INFO +12 -0
- djhx_blogger-0.1.4/pyproject.toml +21 -0
- djhx_blogger-0.1.4/setup.cfg +4 -0
- djhx_blogger-0.1.4/src/djhx_blogger/__init__.py +0 -0
- djhx_blogger-0.1.4/src/djhx_blogger/__main__.py +4 -0
- djhx_blogger-0.1.4/src/djhx_blogger/cli.py +47 -0
- djhx_blogger-0.1.4/src/djhx_blogger/deploy.py +65 -0
- djhx_blogger-0.1.4/src/djhx_blogger/gen.py +366 -0
- djhx_blogger-0.1.4/src/djhx_blogger/log_config.py +38 -0
- djhx_blogger-0.1.4/src/djhx_blogger/static/css/archive.css +49 -0
- djhx_blogger-0.1.4/src/djhx_blogger/static/css/article.css +147 -0
- djhx_blogger-0.1.4/src/djhx_blogger/static/css/basic.css +53 -0
- djhx_blogger-0.1.4/src/djhx_blogger/static/css/category.css +112 -0
- djhx_blogger-0.1.4/src/djhx_blogger/static/images/favicon.ico +0 -0
- djhx_blogger-0.1.4/src/djhx_blogger/static/template/archive.html +40 -0
- djhx_blogger-0.1.4/src/djhx_blogger/static/template/article.html +39 -0
- djhx_blogger-0.1.4/src/djhx_blogger/static/template/category.html +57 -0
- djhx_blogger-0.1.4/src/djhx_blogger.egg-info/PKG-INFO +12 -0
- djhx_blogger-0.1.4/src/djhx_blogger.egg-info/SOURCES.txt +22 -0
- djhx_blogger-0.1.4/src/djhx_blogger.egg-info/dependency_links.txt +1 -0
- djhx_blogger-0.1.4/src/djhx_blogger.egg-info/entry_points.txt +2 -0
- djhx_blogger-0.1.4/src/djhx_blogger.egg-info/requires.txt +5 -0
- djhx_blogger-0.1.4/src/djhx_blogger.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Koril33
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: djhx-blogger
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: beautifulsoup4>=4.14.2
|
|
8
|
+
Requires-Dist: fabric>=3.2.2
|
|
9
|
+
Requires-Dist: jinja2>=3.1.6
|
|
10
|
+
Requires-Dist: markdown>=3.9
|
|
11
|
+
Requires-Dist: typer>=0.20.0
|
|
12
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "djhx-blogger"
|
|
3
|
+
version = "0.1.4"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
requires-python = ">=3.9"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"beautifulsoup4>=4.14.2",
|
|
8
|
+
"fabric>=3.2.2",
|
|
9
|
+
"jinja2>=3.1.6",
|
|
10
|
+
"markdown>=3.9",
|
|
11
|
+
"typer>=0.20.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
blg = "djhx_blogger.cli:app"
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
where = ["src"]
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.package-data]
|
|
21
|
+
"djhx_blogger" = ["static/**/*"]
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from .deploy import compress_dir, deploy_blog
|
|
6
|
+
from .gen import generate_blog
|
|
7
|
+
from .log_config import log_init
|
|
8
|
+
log_init()
|
|
9
|
+
|
|
10
|
+
app = typer.Typer()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command()
|
|
14
|
+
def run(
|
|
15
|
+
origin: Path = typer.Argument(..., exists=True, readable=True, help="原始博客内容目录(必填)"),
|
|
16
|
+
target: Path = typer.Option(
|
|
17
|
+
Path.cwd(),
|
|
18
|
+
"--target", "-t",
|
|
19
|
+
help="生成的静态博客输出目录,默认为当前目录。",
|
|
20
|
+
),
|
|
21
|
+
server: str = typer.Option(
|
|
22
|
+
None,
|
|
23
|
+
"--server", "-s",
|
|
24
|
+
help="目标服务器(域名或 IP 地址),可选。",
|
|
25
|
+
),
|
|
26
|
+
server_target: str = typer.Option(
|
|
27
|
+
None,
|
|
28
|
+
"--server-target", "-T",
|
|
29
|
+
help="目标服务器的部署路径"
|
|
30
|
+
),
|
|
31
|
+
deploy: bool = typer.Option(
|
|
32
|
+
False,
|
|
33
|
+
"--deploy/--no-deploy",
|
|
34
|
+
help="是否将静态博客部署到远程服务器。",
|
|
35
|
+
),
|
|
36
|
+
):
|
|
37
|
+
typer.echo(f"原始目录: {origin}")
|
|
38
|
+
typer.echo(f"输出目录: {target}")
|
|
39
|
+
typer.echo(f"目标服务器: {server or '(未指定)'}")
|
|
40
|
+
typer.echo(f"目标服务器部署地址: {server_target}")
|
|
41
|
+
typer.echo(f"是否部署: {'是' if deploy else '否'}")
|
|
42
|
+
|
|
43
|
+
root_node = generate_blog(str(origin), str(target))
|
|
44
|
+
|
|
45
|
+
if deploy and server and server_target:
|
|
46
|
+
tar_path = compress_dir(root_node.destination_path)
|
|
47
|
+
deploy_blog(server, tar_path, server_target)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import tarfile
|
|
2
|
+
from getpass import getpass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from fabric import Connection, Config
|
|
6
|
+
|
|
7
|
+
from .log_config import app_logger
|
|
8
|
+
|
|
9
|
+
logger = app_logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def compress_dir(blog_path: Path) -> Path:
|
|
13
|
+
"""
|
|
14
|
+
将指定目录压缩为 public.tar.gz
|
|
15
|
+
"""
|
|
16
|
+
logger.info(f'压缩目录: {blog_path}')
|
|
17
|
+
output_tar = blog_path.parent / 'public.tar.gz'
|
|
18
|
+
|
|
19
|
+
with tarfile.open(output_tar, "w:gz") as tar:
|
|
20
|
+
tar.add(str(blog_path), arcname="public")
|
|
21
|
+
|
|
22
|
+
logger.info(f'压缩完成: {output_tar}')
|
|
23
|
+
return output_tar
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def deploy_blog(server_name: str, local_tar_path: Path, remote_web_root: str):
|
|
27
|
+
"""
|
|
28
|
+
将 tar.gz 文件部署到远程服务器
|
|
29
|
+
"""
|
|
30
|
+
logger.info(f'开始部署 -> 服务器: {server_name},文件: {local_tar_path}')
|
|
31
|
+
|
|
32
|
+
sudo_pass = getpass("[sudo]: ")
|
|
33
|
+
config = Config(overrides={'sudo': {'password': sudo_pass}})
|
|
34
|
+
c = Connection(host=server_name, user='koril', config=config, connect_kwargs={'password': sudo_pass})
|
|
35
|
+
|
|
36
|
+
remote_home_path = f'/home/{c.user}'
|
|
37
|
+
remote_tar_path = f'{remote_home_path}/{local_tar_path.name}'
|
|
38
|
+
remote_target_path = f'{remote_web_root}blog'
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
# 上传
|
|
42
|
+
c.put(str(local_tar_path), remote=remote_home_path)
|
|
43
|
+
logger.info('上传完成')
|
|
44
|
+
|
|
45
|
+
# 删除旧备份
|
|
46
|
+
c.sudo(f'rm -rf {remote_web_root}blog.bak')
|
|
47
|
+
logger.info('旧 blog.bak 删除')
|
|
48
|
+
|
|
49
|
+
# 备份 blog
|
|
50
|
+
c.sudo(f'mv {remote_target_path} {remote_target_path}.bak')
|
|
51
|
+
logger.info('blog -> blog.bak')
|
|
52
|
+
|
|
53
|
+
# 移动 tar.gz 并解压
|
|
54
|
+
c.sudo(f'mv {remote_tar_path} {remote_web_root}')
|
|
55
|
+
c.sudo(f'tar -xzf {remote_web_root}{local_tar_path.name} -C {remote_web_root}')
|
|
56
|
+
logger.info('解压完成')
|
|
57
|
+
|
|
58
|
+
# 清理
|
|
59
|
+
c.sudo(f'rm {remote_web_root}{local_tar_path.name}')
|
|
60
|
+
c.sudo(f'mv {remote_web_root}public {remote_target_path}')
|
|
61
|
+
logger.info('部署完成')
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.exception(f"部署失败")
|
|
65
|
+
raise
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import time
|
|
3
|
+
from collections import deque, OrderedDict
|
|
4
|
+
from importlib import resources
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import markdown
|
|
8
|
+
from bs4 import BeautifulSoup
|
|
9
|
+
from jinja2 import Template
|
|
10
|
+
|
|
11
|
+
from .log_config import app_logger
|
|
12
|
+
|
|
13
|
+
logger = app_logger
|
|
14
|
+
|
|
15
|
+
ignore_item = ['.git', 'LICENSE']
|
|
16
|
+
|
|
17
|
+
def load_template(name: str) -> str:
|
|
18
|
+
"""读取 static/template/ 下的模板文件"""
|
|
19
|
+
file_path = resources.files("djhx_blogger.static.template").joinpath(name)
|
|
20
|
+
return file_path.read_text(encoding="utf-8")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Node:
|
|
24
|
+
cache_map = {}
|
|
25
|
+
|
|
26
|
+
def __init__(self, source_path, destination_path, node_type):
|
|
27
|
+
# 该节点的源目录路径
|
|
28
|
+
self.source_path = source_path
|
|
29
|
+
# 该节点生成的结果目录路径
|
|
30
|
+
self.destination_path = destination_path
|
|
31
|
+
# 子节点
|
|
32
|
+
self.children = []
|
|
33
|
+
# 节点类型:
|
|
34
|
+
# 1. category 包含多个子目录
|
|
35
|
+
# 2. article 包含一个 index.md 文件和 images 目录
|
|
36
|
+
# 3. leaf index.md 或者 images 目录
|
|
37
|
+
self.node_type = node_type
|
|
38
|
+
# 描述分类或者文章的元信息(比如:文章的标题,简介和日期)
|
|
39
|
+
self.metadata = None
|
|
40
|
+
|
|
41
|
+
Node.cache_map[source_path] = self
|
|
42
|
+
|
|
43
|
+
def __str__(self):
|
|
44
|
+
return f'path={self.source_path}'
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def walk_dir(dir_path_str: str, destination_blog_dir_path_str: str, target_name: str='public') -> Node:
|
|
48
|
+
"""
|
|
49
|
+
遍历目录,构造树结构
|
|
50
|
+
:param dir_path_str: 存放博客 md 文件的目录的字符串
|
|
51
|
+
:param destination_blog_dir_path_str: 生成博客目录的地址
|
|
52
|
+
:param target_name: 生成博客的目录名称
|
|
53
|
+
:return: 树结构的根节点
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
start = int(time.time() * 1000)
|
|
57
|
+
q = deque()
|
|
58
|
+
dir_path = Path(dir_path_str)
|
|
59
|
+
q.append(dir_path)
|
|
60
|
+
|
|
61
|
+
# 生成目录的根路径
|
|
62
|
+
destination_root_dir = Path(destination_blog_dir_path_str).joinpath(target_name)
|
|
63
|
+
logger.info(f'源路经: {dir_path}, 目标路径: {destination_root_dir}')
|
|
64
|
+
|
|
65
|
+
root = None
|
|
66
|
+
|
|
67
|
+
# 层次遍历
|
|
68
|
+
while q:
|
|
69
|
+
item = q.popleft()
|
|
70
|
+
if item.name in ignore_item:
|
|
71
|
+
logger.info(f'略过: {item.name}')
|
|
72
|
+
continue
|
|
73
|
+
if Path.is_dir(item):
|
|
74
|
+
[q.append(e) for e in item.iterdir()]
|
|
75
|
+
|
|
76
|
+
# node 类型判定
|
|
77
|
+
node_type = 'leaf'
|
|
78
|
+
if Path.is_dir(item):
|
|
79
|
+
node_type = 'category'
|
|
80
|
+
# 如果目录包含 index.md 则是文章目录节点
|
|
81
|
+
for e in item.iterdir():
|
|
82
|
+
if e.name == 'index.md':
|
|
83
|
+
node_type = 'article'
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
if not root:
|
|
87
|
+
root = Node(item, destination_root_dir, node_type)
|
|
88
|
+
else:
|
|
89
|
+
cur_node = Node.cache_map[item.parent]
|
|
90
|
+
# 计算相对路径
|
|
91
|
+
relative_path = item.relative_to(dir_path)
|
|
92
|
+
# 构造目标路径
|
|
93
|
+
destination_path = destination_root_dir / relative_path
|
|
94
|
+
if destination_path.name == 'index.md':
|
|
95
|
+
destination_path = destination_path.parent / Path('index.html')
|
|
96
|
+
n = Node(item, destination_path, node_type)
|
|
97
|
+
cur_node.children.append(n)
|
|
98
|
+
end = int(time.time() * 1000)
|
|
99
|
+
logger.info(f'构造树耗时: {end - start} ms')
|
|
100
|
+
|
|
101
|
+
return root
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def md_to_html(md_file_path: Path) -> str:
|
|
105
|
+
"""
|
|
106
|
+
markdown -> html
|
|
107
|
+
:param md_file_path: markdown 文件的路径对象
|
|
108
|
+
:return: html str
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def remove_metadata(content: str) -> str:
|
|
112
|
+
"""
|
|
113
|
+
删除文章开头的 YAML 元信息
|
|
114
|
+
:param content: markdown 内容
|
|
115
|
+
"""
|
|
116
|
+
lines = content.splitlines()
|
|
117
|
+
if lines and lines[0] == '---':
|
|
118
|
+
for i in range(1, len(lines)):
|
|
119
|
+
if lines[i] == '---':
|
|
120
|
+
return '\n'.join(lines[i + 1:])
|
|
121
|
+
return md_content
|
|
122
|
+
|
|
123
|
+
with open(md_file_path, mode='r', encoding='utf-8') as md_file:
|
|
124
|
+
md_content = md_file.read()
|
|
125
|
+
md_content = remove_metadata(md_content)
|
|
126
|
+
return markdown.markdown(
|
|
127
|
+
md_content,
|
|
128
|
+
extensions=[
|
|
129
|
+
'markdown.extensions.toc',
|
|
130
|
+
'markdown.extensions.tables',
|
|
131
|
+
'markdown.extensions.sane_lists',
|
|
132
|
+
'markdown.extensions.fenced_code'
|
|
133
|
+
]
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def gen_article_index(md_file_path: Path, article_name):
|
|
138
|
+
|
|
139
|
+
bs1 = BeautifulSoup(load_template('article.html'), "html.parser")
|
|
140
|
+
bs2 = BeautifulSoup(md_to_html(md_file_path), "html.parser")
|
|
141
|
+
|
|
142
|
+
article_metadata = read_metadata(md_file_path)
|
|
143
|
+
|
|
144
|
+
article_tag = bs1.find('article')
|
|
145
|
+
# 添加 h1 标题
|
|
146
|
+
h1_tag = bs1.new_tag('h1')
|
|
147
|
+
h1_tag.string = article_name
|
|
148
|
+
article_tag.insert(0, h1_tag)
|
|
149
|
+
|
|
150
|
+
# 添加日期信息
|
|
151
|
+
time_tag = bs1.new_tag('time', datetime=article_metadata["date"])
|
|
152
|
+
time_tag.string = '时间: ' + article_metadata["date"]
|
|
153
|
+
|
|
154
|
+
# 添加摘要信息
|
|
155
|
+
summary_tag = bs1.new_tag('p')
|
|
156
|
+
summary_tag.string = '摘要: ' + article_metadata["summary"]
|
|
157
|
+
|
|
158
|
+
# 包裹元信息
|
|
159
|
+
meta_wrapper = bs1.new_tag('div', **{"class": "article-meta"})
|
|
160
|
+
meta_wrapper.append(time_tag)
|
|
161
|
+
meta_wrapper.append(bs1.new_tag('br'))
|
|
162
|
+
meta_wrapper.append(summary_tag)
|
|
163
|
+
|
|
164
|
+
# 插入到 h1 之后
|
|
165
|
+
h1_tag.insert_after(meta_wrapper)
|
|
166
|
+
|
|
167
|
+
# 添加标题和正文之间的换行符
|
|
168
|
+
article_tag.append(bs1.new_tag('hr'))
|
|
169
|
+
# 添加正文内容
|
|
170
|
+
article_tag.append(bs2)
|
|
171
|
+
# 修改页面标题
|
|
172
|
+
bs1.find('title').string = f'文章 | {article_name}'
|
|
173
|
+
|
|
174
|
+
return bs1.prettify()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def gen_category_index(categories: list, category_name) -> str:
|
|
178
|
+
template = Template(load_template('category.html'))
|
|
179
|
+
html = template.render(categories=categories, category_name=category_name)
|
|
180
|
+
return html
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def sort_categories(item):
|
|
184
|
+
"""
|
|
185
|
+
对 categories 排序,type = category 排在所有 type = article 前
|
|
186
|
+
category 按照 name 字典顺序 a-z 排序
|
|
187
|
+
article 按照 metadata 的 date 字段(格式:2024-02-03T14:44:42+08:00)降序排列。
|
|
188
|
+
:param item:
|
|
189
|
+
:return:
|
|
190
|
+
"""
|
|
191
|
+
from datetime import datetime
|
|
192
|
+
if item['type'] == 'category':
|
|
193
|
+
# 分类优先,按 name 排序
|
|
194
|
+
return 0, item['name'].lower()
|
|
195
|
+
elif item['type'] == 'article':
|
|
196
|
+
# 文章按日期降序排序,优先级次于 category
|
|
197
|
+
# 将日期解析为 datetime 对象,若无日期则排在最后
|
|
198
|
+
date = item['metadata'].get('date')
|
|
199
|
+
parsed_date = datetime.fromisoformat(date) if date else datetime(year=1970, month=1, day=1)
|
|
200
|
+
return 1, -parsed_date.timestamp()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def gen_blog_dir(root: Node):
|
|
204
|
+
"""
|
|
205
|
+
根据目录树构造博客目录
|
|
206
|
+
:param root: 树结构根节点
|
|
207
|
+
:return:
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
start = int(time.time() * 1000)
|
|
211
|
+
|
|
212
|
+
q = deque()
|
|
213
|
+
q.append(root)
|
|
214
|
+
|
|
215
|
+
# 清理之前生成的 root destination
|
|
216
|
+
if Path.exists(root.destination_path):
|
|
217
|
+
logger.info(f'存在目标目录: {root.destination_path},进行删除')
|
|
218
|
+
shutil.rmtree(root.destination_path)
|
|
219
|
+
|
|
220
|
+
while q:
|
|
221
|
+
node = q.popleft()
|
|
222
|
+
[q.append(child) for child in node.children]
|
|
223
|
+
|
|
224
|
+
# 对三种不同类型的节点分别进行处理
|
|
225
|
+
|
|
226
|
+
if node.node_type == 'category' and node.source_path.name != 'images':
|
|
227
|
+
Path.mkdir(node.destination_path, parents=True, exist_ok=True)
|
|
228
|
+
category_index = node.destination_path / Path('index.html')
|
|
229
|
+
categories = []
|
|
230
|
+
for child in node.children:
|
|
231
|
+
if child:
|
|
232
|
+
if child.node_type == 'article':
|
|
233
|
+
child.metadata = read_metadata(child.source_path / Path('index.md'))
|
|
234
|
+
relative_path = child.destination_path.name / Path('index.html')
|
|
235
|
+
categories.append({
|
|
236
|
+
'type': child.node_type,
|
|
237
|
+
'name': child.destination_path.name,
|
|
238
|
+
'href': relative_path,
|
|
239
|
+
'metadata': child.metadata,
|
|
240
|
+
})
|
|
241
|
+
categories.sort(key=sort_categories)
|
|
242
|
+
with open(category_index, mode='w', encoding='utf-8') as f:
|
|
243
|
+
f.write(gen_category_index(categories, node.source_path.name))
|
|
244
|
+
|
|
245
|
+
if node.node_type == 'category' and node.source_path.name == 'images':
|
|
246
|
+
Path.mkdir(node.destination_path, parents=True, exist_ok=True)
|
|
247
|
+
|
|
248
|
+
if node.node_type == 'article':
|
|
249
|
+
Path.mkdir(node.destination_path, parents=True, exist_ok=True)
|
|
250
|
+
|
|
251
|
+
if node.node_type == 'leaf':
|
|
252
|
+
Path.mkdir(node.destination_path.parent, parents=True, exist_ok=True)
|
|
253
|
+
if node.source_path.name == 'index.md':
|
|
254
|
+
with open(node.destination_path, mode='w', encoding='utf-8') as f:
|
|
255
|
+
f.write(gen_article_index(node.source_path, node.source_path.parent.name))
|
|
256
|
+
else:
|
|
257
|
+
shutil.copy(node.source_path, node.destination_path)
|
|
258
|
+
|
|
259
|
+
end = int(time.time() * 1000)
|
|
260
|
+
logger.info(f'生成目标目录耗时: {end - start} ms')
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def gen_blog_archive(blog_dir_str: str, blog_target_dir_str: str, root: Node, target_name: str='public'):
|
|
264
|
+
"""
|
|
265
|
+
生成博客 archive 页面
|
|
266
|
+
按照年份分栏,日期排序,展示所有的博客文章
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
root_node_path = root.destination_path
|
|
270
|
+
blog_dir = Path(blog_dir_str)
|
|
271
|
+
|
|
272
|
+
q = deque()
|
|
273
|
+
q.append(root)
|
|
274
|
+
articles = []
|
|
275
|
+
while q:
|
|
276
|
+
node = q.popleft()
|
|
277
|
+
[q.append(child) for child in node.children]
|
|
278
|
+
if node.node_type == 'article':
|
|
279
|
+
articles.append(node)
|
|
280
|
+
|
|
281
|
+
archives = OrderedDict()
|
|
282
|
+
# 先将所有文章按日期降序排列
|
|
283
|
+
articles_sorted = sorted(articles, key=lambda a: a.metadata['date'], reverse=True)
|
|
284
|
+
|
|
285
|
+
for article in articles_sorted:
|
|
286
|
+
article_name = article.source_path.name
|
|
287
|
+
full_path = article.destination_path / Path('index.html')
|
|
288
|
+
base_path = Path(blog_target_dir_str) / Path(target_name)
|
|
289
|
+
url = full_path.relative_to(base_path)
|
|
290
|
+
|
|
291
|
+
article_datetime = article.metadata.get('date')
|
|
292
|
+
article_year = article_datetime[:4]
|
|
293
|
+
article_date = article_datetime[:10]
|
|
294
|
+
if article_year not in archives:
|
|
295
|
+
archives[article_year] = {
|
|
296
|
+
'articles': [],
|
|
297
|
+
'total': 0,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
archives[article_year]['articles'].append({
|
|
301
|
+
'date': article_date,
|
|
302
|
+
'title': article_name,
|
|
303
|
+
'url': url
|
|
304
|
+
})
|
|
305
|
+
archives[article_year]['total'] += 1
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
template = Template(load_template('archive.html'))
|
|
309
|
+
html = template.render(archives=archives)
|
|
310
|
+
|
|
311
|
+
root_node_path.joinpath('archive.html').write_text(data=html, encoding='utf-8')
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def cp_resource(blog_target_path_str: str):
|
|
315
|
+
"""将包内 static 资源复制到目标目录下的 public/"""
|
|
316
|
+
public_dir = Path(blog_target_path_str) / "public"
|
|
317
|
+
|
|
318
|
+
# 1. 复制 css/
|
|
319
|
+
css_src = str(resources.files("djhx_blogger.static").joinpath("css"))
|
|
320
|
+
css_dst = public_dir / "css"
|
|
321
|
+
shutil.copytree(css_src, css_dst, dirs_exist_ok=True)
|
|
322
|
+
|
|
323
|
+
# 2. 复制 images/
|
|
324
|
+
images_src = str(resources.files("djhx_blogger.static").joinpath("images"))
|
|
325
|
+
images_dst = public_dir / "images"
|
|
326
|
+
shutil.copytree(images_src, images_dst, dirs_exist_ok=True)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def read_metadata(md_file_path):
|
|
330
|
+
import re
|
|
331
|
+
with open(md_file_path, 'r', encoding='utf-8') as file:
|
|
332
|
+
content = file.read()
|
|
333
|
+
|
|
334
|
+
# 正则提取元数据
|
|
335
|
+
match = re.match(r'^---\n([\s\S]*?)\n---\n', content)
|
|
336
|
+
if match:
|
|
337
|
+
metadata = match.group(1)
|
|
338
|
+
return parse_metadata(metadata)
|
|
339
|
+
return {}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def parse_metadata(metadata):
|
|
343
|
+
"""
|
|
344
|
+
将元数据解析为字典
|
|
345
|
+
title, date, summary
|
|
346
|
+
"""
|
|
347
|
+
meta_dict = {}
|
|
348
|
+
for line in metadata.split('\n'):
|
|
349
|
+
if ':' in line:
|
|
350
|
+
key, value = map(str.strip, line.split(':', 1))
|
|
351
|
+
meta_dict[key] = value
|
|
352
|
+
return meta_dict
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def generate_blog(blog_dir: str, blog_target: str):
|
|
356
|
+
start = time.time()
|
|
357
|
+
|
|
358
|
+
logger.info("开始生成博客文件结构...")
|
|
359
|
+
root_node = walk_dir(blog_dir, blog_target)
|
|
360
|
+
gen_blog_dir(root_node)
|
|
361
|
+
gen_blog_archive(blog_dir, blog_target, root_node)
|
|
362
|
+
cp_resource(blog_target)
|
|
363
|
+
|
|
364
|
+
end = time.time()
|
|
365
|
+
logger.info(f'生成静态博客 {blog_dir}, 任务完成, 总耗时: {int((end-start)*1000)} ms')
|
|
366
|
+
return root_node
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import logging.config
|
|
2
|
+
|
|
3
|
+
log_name = 'djhx_blogger'
|
|
4
|
+
|
|
5
|
+
log_config_dict = {
|
|
6
|
+
'version': 1,
|
|
7
|
+
'disable_existing_loggers': False,
|
|
8
|
+
'formatters': {
|
|
9
|
+
'default': {
|
|
10
|
+
'format': '[%(asctime)s] - %(levelname)-8s :: %(message)s',
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
'handlers': {
|
|
14
|
+
'console_handler': {
|
|
15
|
+
'class': 'logging.StreamHandler',
|
|
16
|
+
'stream': 'ext://sys.stdout',
|
|
17
|
+
'formatter': 'default',
|
|
18
|
+
'level': 'DEBUG',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
'loggers': {
|
|
22
|
+
log_name: {
|
|
23
|
+
'handlers': ['console_handler'],
|
|
24
|
+
'level': 'DEBUG',
|
|
25
|
+
'propagate': False,
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
'root': {
|
|
29
|
+
'handlers': ['console_handler'],
|
|
30
|
+
'level': 'WARNING',
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def log_init():
|
|
35
|
+
logging.config.dictConfig(log_config_dict)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
app_logger = logging.getLogger(log_name)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
body {
|
|
2
|
+
background-color: #121212;
|
|
3
|
+
color: #e0e0e0;
|
|
4
|
+
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
5
|
+
margin: 0;
|
|
6
|
+
padding: 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.archive-container {
|
|
10
|
+
max-width: 800px;
|
|
11
|
+
margin: 2rem auto;
|
|
12
|
+
padding: 0 1rem;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.archive-title {
|
|
16
|
+
font-size: 2rem;
|
|
17
|
+
margin-bottom: 2rem;
|
|
18
|
+
text-align: center;
|
|
19
|
+
border-bottom: 1px solid #333;
|
|
20
|
+
padding-bottom: 0.5rem;
|
|
21
|
+
color: #ffffff;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.archive-year {
|
|
25
|
+
margin-bottom: 2rem;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.archive-year h2 {
|
|
29
|
+
font-size: 1.5rem;
|
|
30
|
+
color: #bb86fc;
|
|
31
|
+
border-left: 4px solid #bb86fc;
|
|
32
|
+
padding-left: 0.5rem;
|
|
33
|
+
margin-bottom: 0.5rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.archive-year ul {
|
|
37
|
+
list-style: none;
|
|
38
|
+
padding-left: 1rem;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.archive-item {
|
|
42
|
+
margin: 0.3rem 0;
|
|
43
|
+
font-size: 1rem;
|
|
44
|
+
color: #cccccc;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.archive-item a {
|
|
48
|
+
color: wheat;
|
|
49
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
body {
|
|
2
|
+
background-color: #121212;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
article {
|
|
6
|
+
margin: 50px auto;
|
|
7
|
+
width: 50%;
|
|
8
|
+
background-color: #303030;
|
|
9
|
+
padding-left: 40px;
|
|
10
|
+
padding-right: 40px;
|
|
11
|
+
padding-bottom: 80px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
article h1 {
|
|
15
|
+
font-size: 3em;
|
|
16
|
+
text-align: center;
|
|
17
|
+
margin-top: 30px;
|
|
18
|
+
margin-bottom: 30px;
|
|
19
|
+
color: aliceblue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
article h2 {
|
|
23
|
+
font-size: 2.2em;
|
|
24
|
+
margin-top: 30px;
|
|
25
|
+
margin-bottom: 20px;
|
|
26
|
+
color: cadetblue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
article h3, article h4, article h5, article h6 {
|
|
30
|
+
font-size: 1.8em;
|
|
31
|
+
margin-top: 20px;
|
|
32
|
+
margin-bottom: 20px;
|
|
33
|
+
color: darkcyan;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
article p {
|
|
37
|
+
font-size: 1.2em;
|
|
38
|
+
line-height: 2.2em;
|
|
39
|
+
margin-bottom: 20px;
|
|
40
|
+
color: wheat;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
article img {
|
|
44
|
+
max-width: 100%; /* 限制图片宽度为容器宽度 */
|
|
45
|
+
height: auto; /* 保持图片比例 */
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
code {
|
|
49
|
+
border: 1px solid #ddd; /* 添加边框 */
|
|
50
|
+
border-radius: 4px; /* 圆角边框 */
|
|
51
|
+
font-family: monospace; /* 使用等宽字体 */
|
|
52
|
+
font-size: 1.2em; /* 稍微缩小字体 */
|
|
53
|
+
margin-top: 20px;
|
|
54
|
+
margin-bottom: 20px;
|
|
55
|
+
color: white;
|
|
56
|
+
padding: 3px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
blockquote {
|
|
60
|
+
background-color: dimgrey;
|
|
61
|
+
margin: 1em 0; /* 设置上下间距 */
|
|
62
|
+
padding: 0.5em 1em; /* 内边距 */
|
|
63
|
+
border-left: 4px solid #0074d9; /* 左侧蓝色边框 */
|
|
64
|
+
color: #555; /* 设置文本颜色 */
|
|
65
|
+
font-style: italic; /* 倾斜字体 */
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
table {
|
|
69
|
+
width: 100%;
|
|
70
|
+
border-collapse: collapse;
|
|
71
|
+
margin: 1em 0;
|
|
72
|
+
background-color: #1e1e1e; /* 表格整体背景 */
|
|
73
|
+
color: #ccc; /* 默认文字颜色 */
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
table th, table td {
|
|
77
|
+
border: 1px solid #444; /* 深色边框 */
|
|
78
|
+
padding: 8px;
|
|
79
|
+
text-align: left;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
table th {
|
|
83
|
+
background-color: #2e2e2e; /* 表头背景色 */
|
|
84
|
+
color: #fff; /* 表头文字颜色 */
|
|
85
|
+
font-weight: bold;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
table tr:nth-child(even) {
|
|
89
|
+
background-color: #2a2a2a; /* 偶数行背景色 */
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
table tr:nth-child(odd) {
|
|
93
|
+
background-color: #242424; /* 奇数行背景色 */
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
table tr:hover {
|
|
97
|
+
background-color: #555522; /* 鼠标悬停高亮色 */
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ul, ol {
|
|
101
|
+
margin: 1em 0; /* 上下外边距 */
|
|
102
|
+
padding-left: 2em; /* 左内边距(缩进) */
|
|
103
|
+
line-height: 1.6; /* 设置行高 */
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
ul {
|
|
107
|
+
list-style-type: disc; /* 使用圆点作为项目符号 */
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
ol {
|
|
111
|
+
list-style-type: decimal; /* 使用数字作为编号 */
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ul li, ol li {
|
|
115
|
+
margin-bottom: 0.5em; /* 列表项的下间距 */
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
ul li::marker {
|
|
119
|
+
color: #0074d9; /* 修改圆点颜色 */
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
ol li::marker {
|
|
123
|
+
color: #e74c3c; /* 修改数字颜色 */
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
li {
|
|
127
|
+
font-size: 1rem; /* 设置字体大小 */
|
|
128
|
+
color: aquamarine; /* 设置文字颜色 */
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
li:hover {
|
|
132
|
+
color: antiquewhite; /* 鼠标悬停时改变文字颜色 */
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
hr {
|
|
136
|
+
margin-top: 20px;
|
|
137
|
+
margin-bottom: 20px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.article-meta time, .article-meta p {
|
|
141
|
+
font-size: 0.9em;
|
|
142
|
+
color: aquamarine;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
a {
|
|
146
|
+
color: deeppink;
|
|
147
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
* {
|
|
2
|
+
margin: 0;
|
|
3
|
+
padding: 0;
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
nav {
|
|
8
|
+
display: flex;
|
|
9
|
+
justify-content: space-between;
|
|
10
|
+
align-items: center;
|
|
11
|
+
padding: 10px 20px;
|
|
12
|
+
background-color: #333;
|
|
13
|
+
color: #fff;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
nav .brand {
|
|
17
|
+
font-size: 1.6em;
|
|
18
|
+
font-weight: bold;
|
|
19
|
+
text-decoration: none;
|
|
20
|
+
color: #4caf50;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
nav .nav-links a {
|
|
24
|
+
margin-left: 15px;
|
|
25
|
+
text-decoration: none;
|
|
26
|
+
color: #fff;
|
|
27
|
+
transition: color 0.3s;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
nav .nav-links a:hover {
|
|
31
|
+
color: #4caf50;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.site-footer {
|
|
35
|
+
background: #141414; /* 深色背景 */
|
|
36
|
+
padding: 0.8rem;
|
|
37
|
+
margin-top: 4rem; /* 可以移除或调整,因为使用了 fixed 定位 */
|
|
38
|
+
border-top: 2px solid #444;
|
|
39
|
+
text-align: center;
|
|
40
|
+
position: fixed; /* 将 footer 固定定位 */
|
|
41
|
+
bottom: 0; /* 固定在底部 */
|
|
42
|
+
left: 0; /* 从左侧开始 */
|
|
43
|
+
width: 100%; /* 宽度撑满整个视口 */
|
|
44
|
+
z-index: 10; /* 确保不被其他元素遮挡,可以根据实际情况调整 */
|
|
45
|
+
color: wheat;
|
|
46
|
+
font-size: 0.7rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.site-footer a {
|
|
50
|
+
color: #ffd700; /* 高亮备案链接 */
|
|
51
|
+
text-decoration: none;
|
|
52
|
+
font-weight: 500;
|
|
53
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
body {
|
|
2
|
+
background-color: #202020;
|
|
3
|
+
|
|
4
|
+
min-height: 100vh;
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
h1 {
|
|
11
|
+
text-align: center;
|
|
12
|
+
color: moccasin;
|
|
13
|
+
margin-top: 3%;
|
|
14
|
+
margin-bottom: 3%;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
h2 {
|
|
18
|
+
text-align: center;
|
|
19
|
+
color: rgb(121, 149, 146);
|
|
20
|
+
margin-top: 3%;
|
|
21
|
+
margin-bottom: 3%;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
hr {
|
|
25
|
+
border: 0;
|
|
26
|
+
border-top: 1px dashed #a2a9b6;
|
|
27
|
+
margin-top: 3%;
|
|
28
|
+
margin-bottom: 3%;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* 分类容器优化 */
|
|
32
|
+
.category-container {
|
|
33
|
+
display: grid;
|
|
34
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
35
|
+
gap: 2rem;
|
|
36
|
+
padding: 2rem 1rem;
|
|
37
|
+
max-width: 1200px;
|
|
38
|
+
margin: 2rem auto;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.category {
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
align-items: center;
|
|
45
|
+
background: linear-gradient(145deg, #2d2d2d, #383838);
|
|
46
|
+
border-radius: 16px;
|
|
47
|
+
padding: 2rem;
|
|
48
|
+
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
49
|
+
box-shadow: 0 8px 20px rgba(0,0,0,0.2);
|
|
50
|
+
backdrop-filter: blur(8px);
|
|
51
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.category:hover {
|
|
55
|
+
transform: translateY(-8px);
|
|
56
|
+
box-shadow: 0 12px 30px rgba(76,175,80,0.15);
|
|
57
|
+
background: linear-gradient(145deg, #383838, #4d4d4d);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* 文章卡片优化 */
|
|
61
|
+
.article {
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
align-items: center;
|
|
65
|
+
background: linear-gradient(145deg, #2d2d2d, #383838);
|
|
66
|
+
border-radius: 16px;
|
|
67
|
+
padding: 2rem;
|
|
68
|
+
margin: 1.5rem 0;
|
|
69
|
+
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
70
|
+
box-shadow: 0 8px 20px rgba(0,0,0,0.2);
|
|
71
|
+
backdrop-filter: blur(8px);
|
|
72
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.article:hover {
|
|
76
|
+
transform: translateY(-5px);
|
|
77
|
+
box-shadow: 0 10px 25px rgba(76,175,80,0.1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.article h2 {
|
|
81
|
+
color: rgb(255, 149, 146);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* 链接样式优化 */
|
|
85
|
+
.item-link {
|
|
86
|
+
margin-top: 1.5rem;
|
|
87
|
+
display: flex;
|
|
88
|
+
gap: 1rem;
|
|
89
|
+
justify-content: center;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
a {
|
|
93
|
+
font-size: 1.1em;
|
|
94
|
+
text-decoration: none;
|
|
95
|
+
color: #8b8ea3;
|
|
96
|
+
transition: all 0.3s ease;
|
|
97
|
+
padding: 0.5rem 1rem;
|
|
98
|
+
border-radius: 8px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
a:hover {
|
|
102
|
+
color: #4caf50;
|
|
103
|
+
background: rgba(76,175,80,0.1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* 元数据样式 */
|
|
107
|
+
.article-metadata {
|
|
108
|
+
color: #b0b0b0;
|
|
109
|
+
font-size: 0.9rem;
|
|
110
|
+
margin-top: 1rem;
|
|
111
|
+
text-align: center;
|
|
112
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>归档</title>
|
|
6
|
+
<link rel="icon" type="image/x-icon" href="/blog/images/favicon.ico">
|
|
7
|
+
<link href="/blog/css/basic.css" rel="stylesheet">
|
|
8
|
+
<link href="/blog/css/archive.css" rel="stylesheet">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<nav>
|
|
12
|
+
<a href="/" class="brand">阿辉的小站</a>
|
|
13
|
+
<div class="nav-links">
|
|
14
|
+
<a href="/blog/index.html">blog</a>
|
|
15
|
+
<a href="/blog/archive.html">archive</a>
|
|
16
|
+
<a href="/blog/about/index.html">about</a>
|
|
17
|
+
<a href="/blog/hardware/index.html">hardware</a>
|
|
18
|
+
<a href="/blog/software/index.html">software</a>
|
|
19
|
+
<a href="/blog/cook/index.html">cook</a>
|
|
20
|
+
</div>
|
|
21
|
+
</nav>
|
|
22
|
+
|
|
23
|
+
<main class="archive-container">
|
|
24
|
+
<h1 class="archive-title">归档</h1>
|
|
25
|
+
{% for year, info in archives.items() %}
|
|
26
|
+
<section class="archive-year">
|
|
27
|
+
<h2>{{ year }} - 共计 {{ info.get('total') }} 篇</h2>
|
|
28
|
+
<ul>
|
|
29
|
+
{% for entry in info.get('articles') %}
|
|
30
|
+
<li class="archive-item">
|
|
31
|
+
<span class="date">{{ entry.date }}</span>:<a href="{{ entry.url }}">{{ entry.title }}</a>
|
|
32
|
+
</li>
|
|
33
|
+
{% endfor %}
|
|
34
|
+
</ul>
|
|
35
|
+
</section>
|
|
36
|
+
{% endfor %}
|
|
37
|
+
</main>
|
|
38
|
+
|
|
39
|
+
</body>
|
|
40
|
+
</html>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title></title>
|
|
6
|
+
<link rel="icon" type="image/x-icon" href="/blog/images/favicon.ico">
|
|
7
|
+
<link href="/blog/css/basic.css" rel="stylesheet">
|
|
8
|
+
<link href="/blog/css/article.css" rel="stylesheet">
|
|
9
|
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css"
|
|
10
|
+
rel="stylesheet"/>
|
|
11
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
|
|
12
|
+
<script>
|
|
13
|
+
hljs.highlightAll();
|
|
14
|
+
</script>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<nav>
|
|
18
|
+
<a href="/" class="brand">阿辉的小站</a>
|
|
19
|
+
<div class="nav-links">
|
|
20
|
+
<a href="/blog/index.html">blog</a>
|
|
21
|
+
<a href="/blog/archive.html">archive</a>
|
|
22
|
+
<a href="/blog/about/index.html">about</a>
|
|
23
|
+
<a href="/blog/hardware/index.html">hardware</a>
|
|
24
|
+
<a href="/blog/software/index.html">software</a>
|
|
25
|
+
<a href="/blog/cook/index.html">cook</a>
|
|
26
|
+
</div>
|
|
27
|
+
</nav>
|
|
28
|
+
|
|
29
|
+
<article></article>
|
|
30
|
+
<footer>
|
|
31
|
+
<p class="site-footer">
|
|
32
|
+
备案号:
|
|
33
|
+
<a href="https://beian.miit.gov.cn/" target="_blank">
|
|
34
|
+
浙ICP备19051268号
|
|
35
|
+
</a>
|
|
36
|
+
</p>
|
|
37
|
+
</footer>
|
|
38
|
+
</body>
|
|
39
|
+
</html>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>分类 | {{ category_name }}</title>
|
|
6
|
+
<link rel="icon" type="image/x-icon" href="/blog/images/favicon.ico">
|
|
7
|
+
<link href="/blog/css/basic.css" rel="stylesheet">
|
|
8
|
+
<link href="/blog/css/category.css" rel="stylesheet">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<nav>
|
|
12
|
+
<a href="/" class="brand">阿辉的小站</a>
|
|
13
|
+
<div class="nav-links">
|
|
14
|
+
<a href="/blog/index.html">blog</a>
|
|
15
|
+
<a href="/blog/archive.html">archive</a>
|
|
16
|
+
<a href="/blog/about/index.html">about</a>
|
|
17
|
+
<a href="/blog/hardware/index.html">hardware</a>
|
|
18
|
+
<a href="/blog/software/index.html">software</a>
|
|
19
|
+
<a href="/blog/cook/index.html">cook</a>
|
|
20
|
+
</div>
|
|
21
|
+
</nav>
|
|
22
|
+
|
|
23
|
+
<div class="category-container">
|
|
24
|
+
{% if not categories %}
|
|
25
|
+
<h1>暂无内容</h1>
|
|
26
|
+
{% endif %}
|
|
27
|
+
|
|
28
|
+
{% for item in categories %}
|
|
29
|
+
|
|
30
|
+
<div class="{{ 'article' if item['type'] == 'article' else 'category' }}">
|
|
31
|
+
<a href="{{ item['href'] }}">
|
|
32
|
+
<h2>
|
|
33
|
+
{{ item['name'] }}
|
|
34
|
+
</h2>
|
|
35
|
+
</a>
|
|
36
|
+
{% if item['metadata'] is not none %}
|
|
37
|
+
<div class="article-metadata">
|
|
38
|
+
<strong>Date:</strong> {{ item.metadata.date }}<br>
|
|
39
|
+
<strong>Summary:</strong> {{ item.metadata.summary }}
|
|
40
|
+
</div>
|
|
41
|
+
{% endif %}
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{% endfor %}
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<footer>
|
|
48
|
+
<p class="site-footer">
|
|
49
|
+
备案号:
|
|
50
|
+
<a href="https://beian.miit.gov.cn/" target="_blank">
|
|
51
|
+
浙ICP备19051268号
|
|
52
|
+
</a>
|
|
53
|
+
</p>
|
|
54
|
+
</footer>
|
|
55
|
+
|
|
56
|
+
</body>
|
|
57
|
+
</html>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: djhx-blogger
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: beautifulsoup4>=4.14.2
|
|
8
|
+
Requires-Dist: fabric>=3.2.2
|
|
9
|
+
Requires-Dist: jinja2>=3.1.6
|
|
10
|
+
Requires-Dist: markdown>=3.9
|
|
11
|
+
Requires-Dist: typer>=0.20.0
|
|
12
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/djhx_blogger/__init__.py
|
|
4
|
+
src/djhx_blogger/__main__.py
|
|
5
|
+
src/djhx_blogger/cli.py
|
|
6
|
+
src/djhx_blogger/deploy.py
|
|
7
|
+
src/djhx_blogger/gen.py
|
|
8
|
+
src/djhx_blogger/log_config.py
|
|
9
|
+
src/djhx_blogger.egg-info/PKG-INFO
|
|
10
|
+
src/djhx_blogger.egg-info/SOURCES.txt
|
|
11
|
+
src/djhx_blogger.egg-info/dependency_links.txt
|
|
12
|
+
src/djhx_blogger.egg-info/entry_points.txt
|
|
13
|
+
src/djhx_blogger.egg-info/requires.txt
|
|
14
|
+
src/djhx_blogger.egg-info/top_level.txt
|
|
15
|
+
src/djhx_blogger/static/css/archive.css
|
|
16
|
+
src/djhx_blogger/static/css/article.css
|
|
17
|
+
src/djhx_blogger/static/css/basic.css
|
|
18
|
+
src/djhx_blogger/static/css/category.css
|
|
19
|
+
src/djhx_blogger/static/images/favicon.ico
|
|
20
|
+
src/djhx_blogger/static/template/archive.html
|
|
21
|
+
src/djhx_blogger/static/template/article.html
|
|
22
|
+
src/djhx_blogger/static/template/category.html
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
djhx_blogger
|