重新捡起来 Pelican 写博客已经将近一年,见 Hello Pelican Again。由于开始用到大量公式,重新梳理一下Markdown的一些概念,并使用了katex和mermaid扩展,但是体验并不好,见Markdown小记与Python Markdown使用小记。另外,想看看Sass相关以及Jinja2相关内容以便于调整一下模板,也没看懂。
Pelican 是一个基于 Python 的静态网站生成器,广泛用于创建博客、个人网站、文档网站和项目展示页面。它支持 Markdown 和 reStructuredText 格式,允许用户通过模板引擎(如 Jinja2)定制页面布局
Pelican源码结构
既然不准备换掉它,那就走马观花看看,顺便温习python知识点
pyproject.toml
pelican 从多年以前就开始使用 pyproject.toml 文件。
pyproject.toml 是PEP 518 提出的标准配置文件,用于定义 Python 项目的构建工具和元数据。旨在为 Python 项目提供一种统一的、简洁的方式来配置构建工具链。
在它之前,经常可见的文件
- setup.py
- setup.cfg
- requirements.txt (不是完全取代)
文件由各个部分,主要的:
部分 |
描述 |
[project] |
定义项目的元数据,如名称、版本、描述、作者等。 |
[project.dependencies] |
声明项目的运行时依赖项,类似于 requirements.txt 。 |
[project.scripts] |
定义可以暴露为命令行的脚本,类似于 entry_points 中的 console_scripts 。 |
[build-system] |
定义项目的构建工具和构建依赖(如 setuptools 或 poetry )。 |
[tool] |
各种工具(如 black , pytest 等)的配置区域。 |
pyproject.toml 参考链接:
- https://packaging.python.org/en/latest/specifications/pyproject-toml/#pyproject-toml-spec
- https://peps.python.org/pep-0518/
对于当前penlican项目,它的scripts部分如下:
| [project.scripts]
pelican = "pelican.__main__:main"
pelican-import = "pelican.tools.pelican_import:main"
pelican-plugins = "pelican.plugins._utils:list_plugins"
pelican-quickstart = "pelican.tools.pelican_quickstart:main"
pelican-themes = "pelican.tools.pelican_themes:main"
|
对应我们在命令行下可用的命令,特别是第一个。对应主命令 pelican。
另外,我们执行如下命令时,调用的也是它:
python -m
的模块搜索路径遵循 sys.path,顺序为当前工作目录、PYTHONPATH 环境变量、标准库路径和第三方库路径。它运行模块和包中的代码,包的入口文件为 __main__.py
。
main.py 文件
脚本入口文件,真正代码位于别处:
| from . import main
if __name__ == "__main__":
main()
|
__init__py 文件
真正的入口文件
1
2
3
4
5
6
7
8
9
10
11
12 | class Pelican:
def __init__(self, settings):
...
def init_plugins(self):
...
def run(self):
...
def main(argv=None):
...
|
- main() 是命令行入口,解析命令行并决定执行转换操作还是listen操作。
- Pelican.run() 函数,函数内生成各个Generator,并按特定顺序执行。
generators.py文件
生成Generator后,创建一个上下文(context)。该上下文包含来自命令行的设置,以及如果提供了设置文件,则也包含其内容。
- 调用每个生成器的
generate_context()
方法,更新上下文。
- 创建一个写入器(writer)对象,并将其传递给每个生成器的
generate_output()
方法。
- 注意:Generator的
get_template()
方法
classDiagram
class Generator {
+get_template(self, name)
}
class CachingGenerator {
}
class TemplatePagesGenerator {
+generate_output(writer)
}
class StaticGenerator {
+generate_context()
+generate_output(writer)
}
class SourceFileGenerator {
+generate_context()
+generate_output(writer)
}
class ArticleGenerator {
+generate_context()
+generate_output(writer)
+generate_feeds(writer)
+generate_pages(writer)
}
class PagesGenerator {
+generate_context()
+generate_output(writer)
}
Generator <|-- CachingGenerator
Generator <|-- TemplatePagesGenerator
Generator <|-- StaticGenerator
Generator <|-- SourceFileGenerator
CachingGenerator <|-- ArticleGenerator
CachingGenerator <|-- PagesGenerator
以简单的PagesGenerator为例:
在 generate_context
阶段:
- 读取指定路径,搜索其中特定的文件(比如 .markdown文件等)
- 使用Reader,将文件加载到特定对象中
在 generate_output
阶段:
- 使用上面的context 和 Writer,写入生成的内容。
- 对于ArticleGenerator、PagesGenerator以及TemplatePageGenerator,都会调用Writer的
write_file()
函数。
jinja2模板加载
模板加载也在这个文件内,通过这个文件,对接jinja2知识
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67 | from jinja2 import (
BaseLoader,
ChoiceLoader,
Environment,
FileSystemLoader,
PrefixLoader,
TemplateNotFound,
)
class Generator:
def __init__(self, context, settigns,...):
self.env = Environment(
loader=ChoiceLoader(
[
FileSystemLoader(self._templates_path),
simple_loader, # implicit inheritance
PrefixLoader(
{"!simple": simple_loader, "!theme": theme_loader}
), # explicit ones
]
),
**self.settings["JINJA_ENVIRONMENT"],
)
logger.debug("Template list: %s", self.env.list_templates())
# provide utils.strftime as a jinja filter
self.env.filters.update({"strftime": DateFormatter()})
# get custom Jinja filters from user settings
custom_filters = self.settings["JINJA_FILTERS"]
self.env.filters.update(custom_filters)
# get custom Jinja globals from user settings
custom_globals = self.settings["JINJA_GLOBALS"]
self.env.globals.update(custom_globals)
# get custom Jinja tests from user settings
custom_tests = self.settings["JINJA_TESTS"]
self.env.tests["plugin_enabled"] = partial(
plugin_enabled, plugin_list=self.settings["PLUGINS"]
)
self.env.tests.update(custom_tests)
def get_template(self, name):
"""Return the template by name.
Use self.theme to get the templates to use, and return a list of
templates ready to use with Jinja2.
"""
if name not in self._templates:
for ext in self.settings["TEMPLATE_EXTENSIONS"]:
try:
self._templates[name] = self.env.get_template(name + ext)
break
except TemplateNotFound:
continue
if name not in self._templates:
raise PelicanTemplateNotFound(
"[templates] unable to load {}[{}] from {}".format(
name,
", ".join(self.settings["TEMPLATE_EXTENSIONS"]),
self._templates_path,
)
)
return self._templates[name]
|
readers.py文件
这是用于解析各种源文件的地方。主要关注MarkdownReader。
classDiagram
class BaseReader {
+process_metadata(name, value)
+read(source_path)
}
class RstReader {
+_parse_metadata(document, source_path) dict
+read(source_path) :(content, metadata)
}
class MarkdownReader {
-_parse_metadata(meta) dict
+read(source_path) :(content, metadata)
}
class HTMLReader {
+read(filename) :(content, metadata)
}
BaseReader <|-- RstReader
BaseReader <|-- MarkdownReader
BaseReader <|-- HTMLReader
对MarkdownReader,可以看到基本上没有特殊处理:
- 处理配置文件中的参数,使用参数直接创建python的Markdown对象
- 使用Makrdown对象的
convert()
方法执行转换操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 | class MarkdownReader(BaseReader):
"""Reader for Markdown files"""
enabled = bool(Markdown)
file_extensions = ["md", "markdown", "mkd", "mdown"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
settings = self.settings["MARKDOWN"]
settings.setdefault("extension_configs", {})
settings.setdefault("extensions", [])
for extension in settings["extension_configs"].keys():
if extension not in settings["extensions"]:
settings["extensions"].append(extension)
if "markdown.extensions.meta" not in settings["extensions"]:
settings["extensions"].append("markdown.extensions.meta")
self._source_path = None
def read(self, source_path):
"""Parse content and metadata of markdown files"""
self._source_path = source_path
self._md = Markdown(**self.settings["MARKDOWN"])
with pelican_open(source_path) as text:
content = self._md.convert(text)
if hasattr(self._md, "Meta"):
metadata = self._parse_metadata(self._md.Meta)
else:
metadata = {}
return content, metadata
|
writers.py文件
Wrriter类主要两个函数:
classDiagram
class Writer {
+write_feed(elements, context, path=None, url=None, feed_type="atom", ...)
+write_file(name, template, context, relative_urls=False, paginated=None, template_name=None, ...)
}
Writer --> Paginator
主要的函数 write_file()
中有一个辅助函数_write_file()
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | def _write_file(template, localcontext, output_path, name, override):
"""Render the template write the file."""
# set localsiteurl for context so that Contents can adjust links
if localcontext["localsiteurl"]:
context["localsiteurl"] = localcontext["localsiteurl"]
output = template.render(localcontext)
path = sanitised_join(output_path, name)
try:
os.makedirs(os.path.dirname(path))
except Exception:
pass
with self._open_w(path, "utf-8", override=override) as f:
f.write(output)
|
从这儿可以看到,模板的 render() 方法被调用。
插件(plugins)
- 插件接口?
- 插件的组织结构怎么样?
- Pelican如何搜索插件?
插件接口
Pelican的插件文件的定义很简单,只需要提供 register()
文件作为插件入口!
| from pelican import signals
def test(sender):
pass
def register():
signals.initialized.connect(test)
|
所有的插件是通过订阅-监听机制来工作的,Pelican中称之为signals。所以register()中主要为需要监听的信号注册响应函数。
插件结构
对于一个名为myplugin插件,其文件组织结构大致如下:
| ├── pelican
│ └── plugins
│ └── myplugin
│ ├── __init__.py
│ └── ...
├── ...
└── pyproject.toml
|
注意:pelican,pelican/plugins 文件夹下不包含其他文件。
核心文件就一个 __init__.py
用途?
Python写的这东西,尽管简陋,确实灵活
- https://docs.getpelican.com/en/latest/plugins.html
比如:添加一个新的Reader,以支持新的格式
- 从 BaseReader 派生,声明支持的文件后缀并实现特定的read()函数
- 对
readers_init
信号进行注册相应,以便于添加新的reader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 | from pelican import signals
from pelican.readers import BaseReader
# Create a new reader class, inheriting from the pelican.reader.BaseReader
class NewReader(BaseReader):
enabled = True # Yeah, you probably want that :-)
# The list of file extensions you want this reader to match with.
# If multiple readers were to use the same extension, the latest will
# win (so the one you're defining here, most probably).
file_extensions = ['yeah']
# You need to have a read method, which takes a filename and returns
# some content and the associated metadata.
def read(self, filename):
metadata = {'title': 'Oh yeah',
'category': 'Foo',
'date': '2012-12-01'}
parsed = {}
for key, value in metadata.items():
parsed[key] = self.process_metadata(key, value)
return "Some content", parsed
def add_reader(readers):
readers.reader_classes['yeah'] = NewReader
# This is how pelican works.
def register():
signals.readers_init.connect(add_reader)
|
主题(theme)
这玩意主要是前端的工作了,Jinja2模板,样式表,sass 等
penlican提供一些第三方的主题可用,但一般用户都定制一下。
- https://docs.getpelican.com/en/latest/themes.html
- https://github.com/getpelican/pelican-themes
模板结构
- static部分用于存放主题的静态资源文件,包括 CSS、图片、JavaScript 等。Pelican 会将此目录中的文件复制到输出目录中,与生成的 HTML 文件一起发布。
- templates目录下的Jinja2模板文件用于定义每个页面的布局和内容显示方式。Pelican 会根据内容类型(文章、标签、分类等)选择相应的模板文件来生成 HTML 页面。下面列出的这些模板必须都存在。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | mytheme/
├── static
│ ├── css
│ ├── js
│ └── images
└── templates
├── archives.html // 显示归档页面
├── article.html // 为每篇文章生成页面
├── author.html // 为每个作者生成页面
├── authors.html // 列出所有作者
├── categories.html // 列出所有分类
├── category.html // 为每个分类生成页面
├── index.html // 网站首页,列出所有文章
├── page.html // 为每个页面生成内容(例如“关于我们”页面)
├── period_archives.html // 按时间段显示归档
├── tag.html // 为每个标签生成页面
└── tags.html // 列出所有标签(可以是标签云)
|
变量
Pelican为模板提供的一些常规变量
变量 |
描述 |
output_file |
当前正在生成的文件名。例如,当 Pelican 正在渲染首页时,output_file 的值为 "index.html" 。 |
articles |
文章列表,按日期降序排列。所有元素都是 Article 对象,可访问其属性(如标题、摘要、作者等)。有时会被覆盖(例如在标签页面中)。在这种情况下,可以在 all_articles 变量中找到信息。 |
dates |
相同的文章列表,但按日期升序排列。 |
hidden_articles |
隐藏的文章列表。 |
drafts |
草稿文章列表。 |
period_archives |
一个字典,包含与时间段归档相关的元素(如果启用)。详见“Listing and Linking to Period Archives”部分的细节。 |
authors |
(作者, 文章) 元组的列表,包含所有作者及其对应的文章。 |
categories |
(分类, 文章) 元组的列表,包含所有分类及其对应的文章。 |
tags |
(标签, 文章) 元组的列表,包含所有标签及其对应的文章。 |
pages |
页面列表。 |
hidden_pages |
隐藏的页面列表。 |
draft_pages |
草稿页面列表。 |
另外,在配置文件 pelicanconf.py 中,可以自由定义变量(注意不要和pelian内置变量冲突),这些自定义变量,在jinja2模板中也可以直接用。
sass
兜兜转转,还是回来折腾折腾这个 octopress 这个久久没人维护的主题。克隆一下进行折腾pelican-octopress-theme:
它的样式表是通过sass来生成的,更改样式表要手动修改这些scss文件,而后使用sass工具来生成目标.css。
这个东西变化太多,先勉强从老的ruby版本一直到dart版本。还有一堆警告存在。
首先,安装nodejs及其下的sass包
而后
| sass main.scss ../static/css/main.css --style compressed
|
katex
Pelican的katex支持有两个方案。但都是离线转换方案,在Windows下奇慢无比(数十分钟),即使在github的actions,也是分钟级别的。
实在无法接受,故而当前blog 自己编写Markdown插件,不进行离线转换。速度只需数秒。
markdown-katex
网站:https://github.com/mbarkhau/markdown-katex
markdown-katex
插件通过调用 KaTeX 进行 离线转换,从而生成不需要浏览器加载 JavaScript 的公式。然而:
- 在我的个人电脑(Windows 10)上,性能表现非常差,尽管在 GitHub Actions 环境中速度尚可。
- 该插件只支持 GitLab 风格的公式分割符。
pelican-katex
网站:https://github.com/martenlienen/pelican-katex
pelican-katex
也是一个基于 KaTeX 的 离线转换 插件,且封装为 Pelican 插件,适合使用 Pelican 的用户。在实现上,它是一个 Markdown 插件,被 Pelican 通过传递对象的方式调用。
getpelican使用问题
- 它使用 python-markdown 不支持 commmark,与主流的Markdown编辑器由一定的兼容性问题。
- 内部链接使用自定义变量
{filenme}
、{static}
等,主流的Markdown编辑器不支持。
- ...
front matter
现在其他软件YAML Front Matter用的比较多,可以使用扩展
- https://pypi.org/project/pelican-yaml-metadata/
或者
- https://pypi.org/project/markdown-full-yaml-metadata/
| ---
title: I am the title of the article
---
|
注意,pelican源码中默认会启动python-markdown的自己的Mate扩展。
其他编辑器比如vscode等可以识别并隐藏这部分。
nest list
嵌套用4个空格,这是Python Markdown非常坚持的一点。见Issue:https://github.com/Python-Markdown/markdown/issues/3
针对python markdown和其他软件缩进空格不一致问题,可以
- https://pypi.org/project/mdx-truly-sane-lists/
其他扩展
方案! 手动写Python Markdown扩展,源码使用编辑器常规使用的格式,送入pelican时在内存中自动转换一下。这个终极灵活
比如如果没有可心的katex扩展,那就直接写一下:
- https://pypi.org/project/md-katex/
commonmark兼容?
另外,pelican也有三方的pelican-markdown-it-reader插件,它使用更符合markdown-it 生态的 markdown-it-py。先观望,后面考虑是否切换。
参考
- https://docs.getpelican.com/en/latest/internals.html
- https://github.com/getpelican/pelican
- https://peps.python.org/pep-0518/
- https://pypi.org/project/pelican-markdown-it-reader/