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.

@@ -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/**/*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,4 @@
1
+ from .cli import app
2
+
3
+ if __name__ == '__main__':
4
+ app()
@@ -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
+ }
@@ -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,2 @@
1
+ [console_scripts]
2
+ blg = djhx_blogger.cli:app
@@ -0,0 +1,5 @@
1
+ beautifulsoup4>=4.14.2
2
+ fabric>=3.2.2
3
+ jinja2>=3.1.6
4
+ markdown>=3.9
5
+ typer>=0.20.0
@@ -0,0 +1 @@
1
+ djhx_blogger