开发 “recipe” 插件¶
本教程的目的是说明角色、指令和域。一旦完成,我们将能够使用这个插件来描述食谱,并在我们的文档的其他地方引用该食谱。
备注
本教程基于第一次发布在 opensource.com 的指南,并在原始作者的允许下提供。
概述¶
我们希望扩展添加以下 Sphinx:
recipe
directive,包含一些描述食谱步骤的内容,以及:contains:
选项,突出食谱的主要成分。ref
role,它提供了对配方本身的交叉引用。recipe
domain,它允许我们把上面的角色和域联系在一起,还有索引之类的东西。
为此,我们需要向 Sphinx 添加以下元素:
叫做
recipe
的新指令新的索引允许我们参考 ingredient 和食谱
名为
recipe
的新域名,它将包含recipe
指令和ref
角色
准备¶
我们需要相同的设置,如在 以前的插件。这一次,我们将把插件放在一个名为 recipe.py
的文件中。
下面是一个你可能获得的文件夹结构的例子:
└── source
├── _ext
│ └── recipe.py
├── conf.py
└── index.rst
编写插件¶
打开 recipe.py
,然后粘贴下面的代码进去,我们稍后会详细解释:
1from collections import defaultdict
2
3from docutils.parsers.rst import directives
4
5from sphinx import addnodes
6from sphinx.directives import ObjectDescription
7from sphinx.domains import Domain, Index
8from sphinx.roles import XRefRole
9from sphinx.util.nodes import make_refnode
10
11
12class RecipeDirective(ObjectDescription):
13 """A custom directive that describes a recipe."""
14
15 has_content = True
16 required_arguments = 1
17 option_spec = {
18 'contains': directives.unchanged_required,
19 }
20
21 def handle_signature(self, sig, signode):
22 signode += addnodes.desc_name(text=sig)
23 return sig
24
25 def add_target_and_index(self, name_cls, sig, signode):
26 signode['ids'].append('recipe' + '-' + sig)
27 if 'contains' in self.options:
28 ingredients = [
29 x.strip() for x in self.options.get('contains').split(',')]
30
31 recipes = self.env.get_domain('recipe')
32 recipes.add_recipe(sig, ingredients)
33
34
35class IngredientIndex(Index):
36 """A custom index that creates an ingredient matrix."""
37
38 name = 'ingredient'
39 localname = 'Ingredient Index'
40 shortname = 'Ingredient'
41
42 def generate(self, docnames=None):
43 content = defaultdict(list)
44
45 recipes = {name: (dispname, typ, docname, anchor)
46 for name, dispname, typ, docname, anchor, _
47 in self.domain.get_objects()}
48 recipe_ingredients = self.domain.data['recipe_ingredients']
49 ingredient_recipes = defaultdict(list)
50
51 # flip from recipe_ingredients to ingredient_recipes
52 for recipe_name, ingredients in recipe_ingredients.items():
53 for ingredient in ingredients:
54 ingredient_recipes[ingredient].append(recipe_name)
55
56 # convert the mapping of ingredient to recipes to produce the expected
57 # output, shown below, using the ingredient name as a key to group
58 #
59 # name, subtype, docname, anchor, extra, qualifier, description
60 for ingredient, recipe_names in ingredient_recipes.items():
61 for recipe_name in recipe_names:
62 dispname, typ, docname, anchor = recipes[recipe_name]
63 content[ingredient].append(
64 (dispname, 0, docname, anchor, docname, '', typ))
65
66 # convert the dict to the sorted list of tuples expected
67 content = sorted(content.items())
68
69 return content, True
70
71
72class RecipeIndex(Index):
73 """A custom index that creates an recipe matrix."""
74
75 name = 'recipe'
76 localname = 'Recipe Index'
77 shortname = 'Recipe'
78
79 def generate(self, docnames=None):
80 content = defaultdict(list)
81
82 # sort the list of recipes in alphabetical order
83 recipes = self.domain.get_objects()
84 recipes = sorted(recipes, key=lambda recipe: recipe[0])
85
86 # generate the expected output, shown below, from the above using the
87 # first letter of the recipe as a key to group thing
88 #
89 # name, subtype, docname, anchor, extra, qualifier, description
90 for _name, dispname, typ, docname, anchor, _priority in recipes:
91 content[dispname[0].lower()].append(
92 (dispname, 0, docname, anchor, docname, '', typ))
93
94 # convert the dict to the sorted list of tuples expected
95 content = sorted(content.items())
96
97 return content, True
98
99
100class RecipeDomain(Domain):
101
102 name = 'recipe'
103 label = 'Recipe Sample'
104 roles = {
105 'ref': XRefRole()
106 }
107 directives = {
108 'recipe': RecipeDirective,
109 }
110 indices = {
111 RecipeIndex,
112 IngredientIndex
113 }
114 initial_data = {
115 'recipes': [], # object list
116 'recipe_ingredients': {}, # name -> object
117 }
118
119 def get_full_qualified_name(self, node):
120 return '{}.{}'.format('recipe', node.arguments[0])
121
122 def get_objects(self):
123 for obj in self.data['recipes']:
124 yield(obj)
125
126 def resolve_xref(self, env, fromdocname, builder, typ, target, node,
127 contnode):
128 match = [(docname, anchor)
129 for name, sig, typ, docname, anchor, prio
130 in self.get_objects() if sig == target]
131
132 if len(match) > 0:
133 todocname = match[0][0]
134 targ = match[0][1]
135
136 return make_refnode(builder, fromdocname, todocname, targ,
137 contnode, targ)
138 else:
139 print('Awww, found nothing')
140 return None
141
142 def add_recipe(self, signature, ingredients):
143 """Add a new recipe to the domain."""
144 name = '{}.{}'.format('recipe', signature)
145 anchor = 'recipe-{}'.format(signature)
146
147 self.data['recipe_ingredients'][name] = ingredients
148 # name, dispname, type, docname, anchor, priority
149 self.data['recipes'].append(
150 (name, signature, 'Recipe', self.env.docname, anchor, 0))
151
152
153def setup(app):
154 app.add_domain(RecipeDomain)
155
156 return {
157 'version': '0.1',
158 'parallel_read_safe': True,
159 'parallel_write_safe': True,
160 }
让我们看看这个插件的每一块一步一步地解释发生了什么。
指令类
首先要检查的是 RecipeDirective 指令:
1class RecipeDirective(ObjectDescription):
2 """A custom directive that describes a recipe."""
3
4 has_content = True
5 required_arguments = 1
6 option_spec = {
7 'contains': directives.unchanged_required,
8 }
9
10 def handle_signature(self, sig, signode):
11 signode += addnodes.desc_name(text=sig)
12 return sig
13
14 def add_target_and_index(self, name_cls, sig, signode):
15 signode['ids'].append('recipe' + '-' + sig)
16 if 'contains' in self.options:
17 ingredients = [
18 x.strip() for x in self.options.get('contains').split(',')]
19
20 recipes = self.env.get_domain('recipe')
21 recipes.add_recipe(sig, ingredients)
不像 开发一个 “Hello world” 插件 和 开发 “TODO” 插件 ,这个指令不是从 docutils.parsers.rst.Directive
派生出来的,没有定义 run
方法。相反,它源自 sphinx.directives.ObjectDescription
对象描述并定义了 handle_signature
和 add_target_and_index
方法。这是因为 ObjectDescription
是一个特殊用途的指令,旨在描述诸如类、函数或在我们的例子中,食谱之类的东西。更具体地说, handle_signature
实现了对指令签名的解析,并将对象的名称和类型传递给它的超类,而 add_taget_and_index
则为该节点的索引添加了一个目标(要链接到)和一个条目。
我们还看到这个指令定义了 has_content
,required_arguments
和 option_spec
。不像在: 上个教程 中添加的 TodoDirective
,这个指令有一个参数,配方名,一个选项,contains
,除了嵌套的结构函数体中的 reStructuredText。
index 类
待处理
添加索引的简要概述
1class IngredientIndex(Index):
2 """A custom index that creates an ingredient matrix."""
3
4 name = 'ingredient'
5 localname = 'Ingredient Index'
6 shortname = 'Ingredient'
7
8 def generate(self, docnames=None):
9 content = defaultdict(list)
10
11 recipes = {name: (dispname, typ, docname, anchor)
12 for name, dispname, typ, docname, anchor, _
13 in self.domain.get_objects()}
14 recipe_ingredients = self.domain.data['recipe_ingredients']
15 ingredient_recipes = defaultdict(list)
16
17 # flip from recipe_ingredients to ingredient_recipes
18 for recipe_name, ingredients in recipe_ingredients.items():
19 for ingredient in ingredients:
20 ingredient_recipes[ingredient].append(recipe_name)
21
22 # convert the mapping of ingredient to recipes to produce the expected
23 # output, shown below, using the ingredient name as a key to group
24 #
25 # name, subtype, docname, anchor, extra, qualifier, description
26 for ingredient, recipe_names in ingredient_recipes.items():
27 for recipe_name in recipe_names:
28 dispname, typ, docname, anchor = recipes[recipe_name]
29 content[ingredient].append(
30 (dispname, 0, docname, anchor, docname, '', typ))
31
32 # convert the dict to the sorted list of tuples expected
33 content = sorted(content.items())
34
35 return content, True
1class RecipeIndex(Index):
2 """A custom index that creates an recipe matrix."""
3
4 name = 'recipe'
5 localname = 'Recipe Index'
6 shortname = 'Recipe'
7
8 def generate(self, docnames=None):
9 content = defaultdict(list)
10
11 # sort the list of recipes in alphabetical order
12 recipes = self.domain.get_objects()
13 recipes = sorted(recipes, key=lambda recipe: recipe[0])
14
15 # generate the expected output, shown below, from the above using the
16 # first letter of the recipe as a key to group thing
17 #
18 # name, subtype, docname, anchor, extra, qualifier, description
19 for _name, dispname, typ, docname, anchor, _priority in recipes:
20 content[dispname[0].lower()].append(
21 (dispname, 0, docname, anchor, docname, '', typ))
22
23 # convert the dict to the sorted list of tuples expected
24 content = sorted(content.items())
25
26 return content, True
IngredientIndex
和 RecipeIndex
都来源于 Index
。它们实现定制逻辑来生成定义索引的值元组。请注意,RecipeIndex
是一个只有一个条目的简单索引。将其扩展到涵盖更多对象类型还不是代码的一部分。
两个指数都使用了 Index.generate()
的方法去做他们的工作。这个方法组合来自我们的域的信息,对其进行排序,并以 Sphinx 可以接受的列表结构返回信息。这看起来可能很复杂,但它实际上是一个元组列表,如 ('tomato','TomatoSoup', 'test', 'rec-TomatoSoup',...)
。请参考 domain API guide 获取更多关于这个 API 的信息。
这些索引页可以通过域名和它的 name
的组合来引用,使用 ref
角色。例如,RecipeIndex
可以由 :ref:`recipe-recipe`
引用。
域
Sphinx 域是将角色、指令和索引等绑定在一起的专用容器。让我们看看在这里创建的域。
1class RecipeDomain(Domain):
2
3 name = 'recipe'
4 label = 'Recipe Sample'
5 roles = {
6 'ref': XRefRole()
7 }
8 directives = {
9 'recipe': RecipeDirective,
10 }
11 indices = {
12 RecipeIndex,
13 IngredientIndex
14 }
15 initial_data = {
16 'recipes': [], # object list
17 'recipe_ingredients': {}, # name -> object
18 }
19
20 def get_full_qualified_name(self, node):
21 return '{}.{}'.format('recipe', node.arguments[0])
22
23 def get_objects(self):
24 for obj in self.data['recipes']:
25 yield(obj)
26
27 def resolve_xref(self, env, fromdocname, builder, typ, target, node,
28 contnode):
29 match = [(docname, anchor)
30 for name, sig, typ, docname, anchor, prio
31 in self.get_objects() if sig == target]
32
33 if len(match) > 0:
34 todocname = match[0][0]
35 targ = match[0][1]
36
37 return make_refnode(builder, fromdocname, todocname, targ,
38 contnode, targ)
39 else:
40 print('Awww, found nothing')
41 return None
42
43 def add_recipe(self, signature, ingredients):
44 """Add a new recipe to the domain."""
45 name = '{}.{}'.format('recipe', signature)
46 anchor = 'recipe-{}'.format(signature)
47
48 self.data['recipe_ingredients'][name] = ingredients
49 # name, dispname, type, docname, anchor, priority
50 self.data['recipes'].append(
51 (name, signature, 'Recipe', self.env.docname, anchor, 0))
关于这个 recipe
域和一般域,有一些有趣的事情需要注意。首先,我们在这里通过 directives
, roles
和 indices
属性来注册我们的指令、角色和索引,而不是稍后在 setup
中调用。还可以注意到,我们实际上并没有定义自定义角色,而是重用了 sphinx.roles.XRefRole
方法,并且定义了 sphinx.domains.Domain.resolve_xref
方法。该方法接受两个参数, typ
和 target
,它们引用交叉引用类型及其目标名称。将使用 target
从我们的域 recipes
解析我们的目的地,因为我们目前只有一种类型的节点。
继续,可以看到我们已经定义了 initial_data
。在 initial_data
中定义的值将被复制到 env.domaindata[domain_name]
中作为域的初始数据,域实例可以通过 self.data
访问它。我们看到在 initial_data
中定义了两个项:recipes
和 recipe2ingredient
。它们包含所有已定义对象的列表(即所有菜谱)和一个将规范成分名称映射到对象列表的散列。我们命名对象的方式在扩展中是通用的,在 get_full_qualified_name
方法中定义。对于创建的每个对象,规范名称是 recipe.<recipename>
,其中 <recipename>
是文档编写者给对象(recipe)的名称。这使得插件可以使用共享相同名称的不同对象类型。拥有一个规范的名称和对象的中心位置是一个巨大的优势。我们的索引和交叉引用代码都使用了这个特性。
setup
函数
As always, setup
函数是必需的,用于将扩展的各个部分钩到 Sphinx 中。让我们看看这个扩展的 setup
函数。
1def setup(app):
2 app.add_domain(RecipeDomain)
3
4 return {
5 'version': '0.1',
6 'parallel_read_safe': True,
7 'parallel_write_safe': True,
8 }
这看起来和我们以前看到的有点不同。没有回调 add_directive()
或者 add_role()
。相反,我们有回调 add_domain()
后面跟着 standard domain 的初始化。这是因为我们已经将指令、角色和索引注册为指令本身的一部分。
使用插件¶
您现在可以在整个项目中使用插件。例如:
Joe's Recipes
=============
Below are a collection of my favourite recipes. I highly recommend the
:recipe:ref:`TomatoSoup` recipe in particular!
.. toctree::
tomato-soup
The recipe contains `tomato` and `cilantro`.
.. recipe:recipe:: TomatoSoup
:contains: tomato, cilantro, salt, pepper
This recipe is a tasty tomato soup, combine all ingredients
and cook.
需要注意的重要一点是 :recipe:ref:
角色的使用,以交叉引用实际在别处定义的食谱(使用 :recipe:recipe:
指令)。
进一步的阅读¶
更多信息,请参考 docutils documentation和 为 Sphinx 开发插件。