擴展建置流程¶
本教學的目標是建立比使用角色和指令擴展語法中建立的更全面的擴充功能。 雖然該指南僅涵蓋編寫自訂的角色和指令,但本指南涵蓋了對 Sphinx 建置流程更複雜的擴展;新增多個指令,以及自訂節點、額外的組態值和自訂事件處理常式。
為此,我們將涵蓋一個 todo
擴充功能,該功能新增在文件中包含 todo 條目的功能,並將這些條目集中收集在一個地方。 這與 Sphinx 隨附的sphinx.ext.todo
擴充功能類似。
概觀¶
我們希望擴充功能為 Sphinx 新增以下內容
一個
todo
指令,包含一些標記為 “TODO” 的內容,並且僅在新組態值設定時才顯示在輸出中。 預設情況下,Todo 條目不應出現在輸出中。一個
todolist
指令,用於建立整個文件中所有 todo 條目的列表。
為此,我們需要將以下元素新增到 Sphinx
新的指令,稱為
todo
和todolist
。新的文件樹狀節點來表示這些指令,通常也稱為
todo
和todolist
。 如果新指令僅產生一些可由現有節點表示的內容,我們就不需要新的節點。一個新的組態值
todo_include_todos
(組態值名稱應以擴充功能名稱開頭,以保持唯一性),用於控制 todo 條目是否進入輸出。新的事件處理常式:一個用於
doctree-resolved
事件,以取代 todo 和 todolist 節點,一個用於env-merge-info
以合併來自平行建置的中間結果,以及一個用於env-purge-doc
(稍後將介紹其原因)。
先決條件¶
與使用角色和指令擴展語法一樣,我們將不會透過 PyPI 分發此外掛程式,因此我們再次需要一個 Sphinx 專案來從中呼叫。 您可以使用現有的專案,或使用 sphinx-quickstart 建立一個新的專案。
我們假設您正在使用單獨的原始碼 (source
) 和建置 (build
) 資料夾。 您的擴充功能檔案可以位於專案的任何資料夾中。 在我們的例子中,讓我們執行以下操作
在
source
中建立一個_ext
資料夾在
_ext
資料夾中建立一個名為todo.py
的新 Python 檔案
以下是您可能獲得的資料夾結構範例
└── source
├── _ext
│ └── todo.py
├── _static
├── conf.py
├── somefolder
├── index.rst
├── somefile.rst
└── someotherfile.rst
撰寫擴充功能¶
開啟 todo.py
並貼上以下程式碼,我們將在稍後詳細解釋所有程式碼
1from docutils import nodes
2from docutils.parsers.rst import Directive
3
4from sphinx.application import Sphinx
5from sphinx.locale import _
6from sphinx.util.docutils import SphinxDirective
7from sphinx.util.typing import ExtensionMetadata
8
9
10class todo(nodes.Admonition, nodes.Element):
11 pass
12
13
14class todolist(nodes.General, nodes.Element):
15 pass
16
17
18def visit_todo_node(self, node):
19 self.visit_admonition(node)
20
21
22def depart_todo_node(self, node):
23 self.depart_admonition(node)
24
25
26class TodolistDirective(Directive):
27 def run(self):
28 return [todolist('')]
29
30
31class TodoDirective(SphinxDirective):
32 # this enables content in the directive
33 has_content = True
34
35 def run(self):
36 targetid = 'todo-%d' % self.env.new_serialno('todo')
37 targetnode = nodes.target('', '', ids=[targetid])
38
39 todo_node = todo('\n'.join(self.content))
40 todo_node += nodes.title(_('Todo'), _('Todo'))
41 todo_node += self.parse_content_to_nodes()
42
43 if not hasattr(self.env, 'todo_all_todos'):
44 self.env.todo_all_todos = []
45
46 self.env.todo_all_todos.append({
47 'docname': self.env.docname,
48 'lineno': self.lineno,
49 'todo': todo_node.deepcopy(),
50 'target': targetnode,
51 })
52
53 return [targetnode, todo_node]
54
55
56def purge_todos(app, env, docname):
57 if not hasattr(env, 'todo_all_todos'):
58 return
59
60 env.todo_all_todos = [
61 todo for todo in env.todo_all_todos if todo['docname'] != docname
62 ]
63
64
65def merge_todos(app, env, docnames, other):
66 if not hasattr(env, 'todo_all_todos'):
67 env.todo_all_todos = []
68 if hasattr(other, 'todo_all_todos'):
69 env.todo_all_todos.extend(other.todo_all_todos)
70
71
72def process_todo_nodes(app, doctree, fromdocname):
73 if not app.config.todo_include_todos:
74 for node in doctree.findall(todo):
75 node.parent.remove(node)
76
77 # Replace all todolist nodes with a list of the collected todos.
78 # Augment each todo with a backlink to the original location.
79 env = app.env
80
81 if not hasattr(env, 'todo_all_todos'):
82 env.todo_all_todos = []
83
84 for node in doctree.findall(todolist):
85 if not app.config.todo_include_todos:
86 node.replace_self([])
87 continue
88
89 content = []
90
91 for todo_info in env.todo_all_todos:
92 para = nodes.paragraph()
93 filename = env.doc2path(todo_info['docname'], base=None)
94 description = _(
95 '(The original entry is located in %s, line %d and can be found '
96 ) % (filename, todo_info['lineno'])
97 para += nodes.Text(description)
98
99 # Create a reference
100 newnode = nodes.reference('', '')
101 innernode = nodes.emphasis(_('here'), _('here'))
102 newnode['refdocname'] = todo_info['docname']
103 newnode['refuri'] = app.builder.get_relative_uri(
104 fromdocname, todo_info['docname']
105 )
106 newnode['refuri'] += '#' + todo_info['target']['refid']
107 newnode.append(innernode)
108 para += newnode
109 para += nodes.Text('.)')
110
111 # Insert into the todolist
112 content.extend((
113 todo_info['todo'],
114 para,
115 ))
116
117 node.replace_self(content)
118
119
120def setup(app: Sphinx) -> ExtensionMetadata:
121 app.add_config_value('todo_include_todos', False, 'html')
122
123 app.add_node(todolist)
124 app.add_node(
125 todo,
126 html=(visit_todo_node, depart_todo_node),
127 latex=(visit_todo_node, depart_todo_node),
128 text=(visit_todo_node, depart_todo_node),
129 )
130
131 app.add_directive('todo', TodoDirective)
132 app.add_directive('todolist', TodolistDirective)
133 app.connect('doctree-resolved', process_todo_nodes)
134 app.connect('env-purge-doc', purge_todos)
135 app.connect('env-merge-info', merge_todos)
136
137 return {
138 'version': '0.1',
139 'env_version': 1,
140 'parallel_read_safe': True,
141 'parallel_write_safe': True,
142 }
這是一個比使用角色和指令擴展語法中詳細介紹的擴充功能更廣泛的擴充功能,但是,我們將逐步查看每個部分以解釋正在發生的事情。
節點類別
讓我們從節點類別開始
1
2
3class todo(nodes.Admonition, nodes.Element):
4 pass
5
6
7class todolist(nodes.General, nodes.Element):
8 pass
9
10
11def visit_todo_node(self, node):
12 self.visit_admonition(node)
13
14
節點類別通常不必執行任何操作,只需從 docutils.nodes
中定義的標準 docutils 類別繼承即可。 todo
從 Admonition
繼承,因為它應該像註釋或警告一樣處理,todolist
只是一個「通用」節點。
注意
重要的是要知道,雖然您可以在不離開 conf.py
的情況下擴展 Sphinx,但如果您在那裡宣告一個繼承的節點,您將遇到一個不明顯的 PickleError
。 因此,如果出現問題,請確保將繼承的節點放入單獨的 Python 模組中。
如需更多詳細資訊,請參閱
指令類別
指令類別通常是從 docutils.parsers.rst.Directive
派生的類別。 docutils 文件中也詳細介紹了指令介面; 重要的是該類別應具有配置允許標記的屬性,以及一個返回節點列表的 run
方法。
首先查看 TodolistDirective
指令
1
2
3class TodolistDirective(Directive):
4 def run(self):
它非常簡單,建立並傳回我們 todolist
節點類別的實例。 TodolistDirective
指令本身既沒有內容,也沒有需要處理的參數。 這將我們帶到 TodoDirective
指令
1
2class TodoDirective(SphinxDirective):
3 # this enables content in the directive
4 has_content = True
5
6 def run(self):
7 targetid = 'todo-%d' % self.env.new_serialno('todo')
8 targetnode = nodes.target('', '', ids=[targetid])
9
10 todo_node = todo('\n'.join(self.content))
11 todo_node += nodes.title(_('Todo'), _('Todo'))
12 todo_node += self.parse_content_to_nodes()
13
14 if not hasattr(self.env, 'todo_all_todos'):
15 self.env.todo_all_todos = []
16
17 self.env.todo_all_todos.append({
18 'docname': self.env.docname,
19 'lineno': self.lineno,
20 'todo': todo_node.deepcopy(),
21 'target': targetnode,
22 })
23
24 return [targetnode, todo_node]
這裡涵蓋了幾個重要的事項。 首先,如您所見,我們現在正在子類化 SphinxDirective
輔助類別,而不是通常的 Directive
類別。 這讓我們可以使用 self.env
屬性存取 建置環境實例。 如果沒有這個,我們將不得不使用相當複雜的 self.state.document.settings.env
。 然後,為了充當連結目標 (來自 TodolistDirective
),除了 todo
節點之外,TodoDirective
指令還需要傳回目標節點。 目標 ID (在 HTML 中,這將是錨點名稱) 是透過使用 env.new_serialno
產生的,它在每次呼叫時傳回一個新的唯一整數,因此會產生唯一的目標名稱。 目標節點在沒有任何文字 (前兩個參數) 的情況下被實例化。
在建立 admonition 節點時,指令的內容主體使用 self.state.nested_parse
進行解析。 第一個參數給出內容主體,第二個參數給出內容偏移量。 第三個參數給出已解析結果的父節點,在我們的例子中是 todo
節點。 之後,todo
節點會新增到環境中。 這是必要的,以便能夠在作者放置 todolist
指令的位置建立整個文件中所有 todo 條目的列表。 在這種情況下,使用環境屬性 todo_all_todos
(同樣,名稱應該是唯一的,因此它以擴充功能名稱作為前綴)。 當建立新環境時,它不存在,因此指令必須檢查並在必要時建立它。 關於 todo 條目位置的各種資訊與節點的副本一起儲存。
在最後一行中,傳回應放入文件樹狀結構中的節點:目標節點和 admonition 節點。
指令傳回的節點結構如下所示
+--------------------+
| target node |
+--------------------+
+--------------------+
| todo node |
+--------------------+
\__+--------------------+
| admonition title |
+--------------------+
| paragraph |
+--------------------+
| ... |
+--------------------+
事件處理常式
事件處理常式是 Sphinx 最強大的功能之一,它提供了一種掛鉤到文件流程任何部分的方法。 Sphinx 本身提供了許多事件,如API 指南中所述,我們將在此處使用它們的子集。
讓我們看看上述範例中使用的事件處理常式。 首先,用於 env-purge-doc
事件的處理常式
1def purge_todos(app, env, docname):
2 if not hasattr(env, 'todo_all_todos'):
3 return
4
5 env.todo_all_todos = [
6 todo for todo in env.todo_all_todos if todo['docname'] != docname
由於我們將來自原始碼檔案的資訊儲存在環境中 (環境是持久性的),因此當原始碼檔案變更時,它可能會過時。 因此,在讀取每個原始碼檔案之前,環境中關於它的記錄會被清除,而 env-purge-doc
事件讓擴充功能有機會執行相同的操作。 在這裡,我們從 todo_all_todos
列表中清除所有 docname 與給定 docname 匹配的 todos。 如果文件中還有 todos,它們將在解析期間再次新增。
下一個處理常式,用於 env-merge-info
事件,用於平行建置期間。 由於在平行建置期間,所有執行緒都有自己的 env
,因此需要合併多個 todo_all_todos
列表
1
2def merge_todos(app, env, docnames, other):
3 if not hasattr(env, 'todo_all_todos'):
4 env.todo_all_todos = []
5 if hasattr(other, 'todo_all_todos'):
另一個處理常式屬於 doctree-resolved
事件
1
2def process_todo_nodes(app, doctree, fromdocname):
3 if not app.config.todo_include_todos:
4 for node in doctree.findall(todo):
5 node.parent.remove(node)
6
7 # Replace all todolist nodes with a list of the collected todos.
8 # Augment each todo with a backlink to the original location.
9 env = app.env
10
11 if not hasattr(env, 'todo_all_todos'):
12 env.todo_all_todos = []
13
14 for node in doctree.findall(todolist):
15 if not app.config.todo_include_todos:
16 node.replace_self([])
17 continue
18
19 content = []
20
21 for todo_info in env.todo_all_todos:
22 para = nodes.paragraph()
23 filename = env.doc2path(todo_info['docname'], base=None)
24 description = _(
25 '(The original entry is located in %s, line %d and can be found '
26 ) % (filename, todo_info['lineno'])
27 para += nodes.Text(description)
28
29 # Create a reference
30 newnode = nodes.reference('', '')
31 innernode = nodes.emphasis(_('here'), _('here'))
32 newnode['refdocname'] = todo_info['docname']
33 newnode['refuri'] = app.builder.get_relative_uri(
34 fromdocname, todo_info['docname']
35 )
36 newnode['refuri'] += '#' + todo_info['target']['refid']
37 newnode.append(innernode)
38 para += newnode
39 para += nodes.Text('.)')
40
41 # Insert into the todolist
42 content.extend((
43 todo_info['todo'],
doctree-resolved
事件在階段 3 (解析)結束時發出,並允許執行自訂解析。 我們為此事件編寫的處理常式稍微複雜一些。 如果 todo_include_todos
組態值 (我們將在稍後描述) 為 false,則所有 todo
和 todolist
節點都會從文件中移除。 如果不是,todo
節點只會保持原位和原樣。 todolist
節點會被 todo 條目的列表取代,其中包含指向其來源位置的反向連結。 列表項目由來自 todo
條目的節點和動態建立的 docutils 節點組成:每個條目一個段落,其中包含提供位置的文字,以及一個帶有反向參考的連結 (包含斜體節點的參考節點)。 參考 URI 是由 sphinx.builders.Builder.get_relative_uri()
建構的,它會根據使用的建置器建立合適的 URI,並將 todo 節點 (目標) 的 ID 附加為錨點名稱。
setup
函數
如先前所述,setup
函數是一個要求,用於將指令插入 Sphinx。 但是,我們也使用它來連接擴充功能的其他部分。 讓我們看看我們的 setup
函數
1
2 node.replace_self(content)
3
4
5def setup(app: Sphinx) -> ExtensionMetadata:
6 app.add_config_value('todo_include_todos', False, 'html')
7
8 app.add_node(todolist)
9 app.add_node(
10 todo,
11 html=(visit_todo_node, depart_todo_node),
12 latex=(visit_todo_node, depart_todo_node),
13 text=(visit_todo_node, depart_todo_node),
14 )
15
16 app.add_directive('todo', TodoDirective)
17 app.add_directive('todolist', TodolistDirective)
18 app.connect('doctree-resolved', process_todo_nodes)
19 app.connect('env-purge-doc', purge_todos)
20 app.connect('env-merge-info', merge_todos)
21
22 return {
23 'version': '0.1',
24 'env_version': 1,
25 'parallel_read_safe': True,
26 'parallel_write_safe': True,
27 }
此函數中的呼叫引用了我們先前新增的類別和函數。 個別呼叫的作用如下
add_config_value()
讓 Sphinx 知道它應該識別新的組態值todo_include_todos
,其預設值應為False
(這也告訴 Sphinx 它是一個布林值)。如果第三個參數是
'html'
,則如果組態值變更其值,HTML 文件將會完整重建。 這對於影響讀取 (建置階段 1 (讀取)) 的組態值是必要的。add_node()
將新的節點類別新增到建置系統。 它還可以為每種支援的輸出格式指定訪問者函數。 當新節點保留到階段 4 (寫入)時,需要這些訪問者函數。 由於todolist
節點始終在階段 3 (解析)中被取代,因此它不需要任何訪問者函數。add_directive()
新增一個新的指令,由名稱和類別給出。最後,
connect()
將一個事件處理常式新增到由第一個參數給出的名稱的事件。 事件處理常式函數使用多個參數呼叫,這些參數記錄在事件中。
有了這個,我們的擴充功能就完成了。
使用擴充功能¶
與之前一樣,我們需要透過在 conf.py
檔案中宣告擴充功能來啟用它。 這裡需要兩個步驟
使用
sys.path.append
將_ext
目錄新增到 Python 路徑。 這應該放在檔案的頂部。更新或建立
extensions
列表,並將擴充功能檔案名稱新增到列表中
此外,我們可能希望設定 todo_include_todos
組態值。 如上所述,這預設為 False
,但我們可以明確設定它。
例如
import sys
from pathlib import Path
sys.path.append(str(Path('_ext').resolve()))
extensions = ['todo']
todo_include_todos = False
您現在可以在整個專案中使用擴充功能。 例如
Hello, world
============
.. toctree::
somefile.rst
someotherfile.rst
Hello world. Below is the list of TODOs.
.. todolist::
foo
===
Some intro text here...
.. todo:: Fix this
bar
===
Some more text here...
.. todo:: Fix that
由於我們已將 todo_include_todos
設定為 False
,因此我們實際上不會看到為 todo
和 todolist
指令呈現的任何內容。 但是,如果我們將其切換為 true,我們將看到先前描述的輸出。
延伸閱讀¶
如需更多資訊,請參閱 docutils 文件和 Sphinx API。
如果您希望在多個專案或與其他人分享您的擴充功能,請查看第三方擴充功能章節。