开发“开发“食谱”扩展名¶
本教程的目的是说明角色,指令和域。 完成后,我们将能够使用此扩展来描述配方并从我们文档的其他地方引用该配方。
备注
本教程基于首先在`opensource.com`_上发布的指南,并在得到原始作者许可的情况下提供。
概述¶
我们希望插件将以下内容添加到Sphinx中:
一个``recipe’’:term:directive,包含一些描述食谱步骤的内容,以及一个``:contains:``选项,突出显示食谱的主要成分。
ref:term:role,提供对配方本身的交叉引用。
``recipe’’:term:domain,它使我们可以将上述角色和领域以及索引等东西结合在一起。
为此,我们需要在 Sphinx 中添加以下元素:
一个名为``recipe’’的新指令
新索引使我们可以参考成分和配方
一个名为“ recipe”的新域,其中将包含“ recipe”指令和“ ref”角色
系统需求¶
我们需要与以前的扩展<todo>中的文档相同的设置。 这次,我们将把扩展名放到一个名为:recipe.py 的文件中。
这是您可能获得的文件夹结构的示例:
└── source
├── _ext
│ └── recipe.py
├── conf.py
└── index.rst
编写扩展¶
打开:file:recipe.py 并将以下代码粘贴到其中,我们将在稍后详细解释所有这些代码:
1from collections import defaultdict
2
3from docutils.parsers.rst import directives
4
5from sphinx import addnodes
6from sphinx.application import Sphinx
7from sphinx.directives import ObjectDescription
8from sphinx.domains import Domain, Index
9from sphinx.roles import XRefRole
10from sphinx.util.nodes import make_refnode
11from sphinx.util.typing import ExtensionMetadata
12
13
14class RecipeDirective(ObjectDescription):
15 """A custom directive that describes a recipe."""
16
17 has_content = True
18 required_arguments = 1
19 option_spec = {
20 'contains': directives.unchanged_required,
21 }
22
23 def handle_signature(self, sig, signode):
24 signode += addnodes.desc_name(text=sig)
25 return sig
26
27 def add_target_and_index(self, name_cls, sig, signode):
28 signode['ids'].append('recipe' + '-' + sig)
29 if 'contains' in self.options:
30 ingredients = [x.strip() for x in self.options.get('contains').split(',')]
31
32 recipes = self.env.get_domain('recipe')
33 recipes.add_recipe(sig, ingredients)
34
35
36class IngredientIndex(Index):
37 """A custom index that creates an ingredient matrix."""
38
39 name = 'ingredient'
40 localname = 'Ingredient Index'
41 shortname = 'Ingredient'
42
43 def generate(self, docnames=None):
44 content = defaultdict(list)
45
46 recipes = {
47 name: (dispname, typ, docname, anchor)
48 for name, dispname, typ, docname, anchor, _ in self.domain.get_objects()
49 }
50 recipe_ingredients = self.domain.data['recipe_ingredients']
51 ingredient_recipes = defaultdict(list)
52
53 # flip from recipe_ingredients to ingredient_recipes
54 for recipe_name, ingredients in recipe_ingredients.items():
55 for ingredient in ingredients:
56 ingredient_recipes[ingredient].append(recipe_name)
57
58 # convert the mapping of ingredient to recipes to produce the expected
59 # output, shown below, using the ingredient name as a key to group
60 #
61 # name, subtype, docname, anchor, extra, qualifier, description
62 for ingredient, recipe_names in ingredient_recipes.items():
63 for recipe_name in recipe_names:
64 dispname, typ, docname, anchor = recipes[recipe_name]
65 content[ingredient].append((dispname, 0, docname, anchor, docname, '', typ))
66
67 # convert the dict to the sorted list of tuples expected
68 content = sorted(content.items())
69
70 return content, True
71
72
73class RecipeIndex(Index):
74 """A custom index that creates an recipe matrix."""
75
76 name = 'recipe'
77 localname = 'Recipe Index'
78 shortname = 'Recipe'
79
80 def generate(self, docnames=None):
81 content = defaultdict(list)
82
83 # sort the list of recipes in alphabetical order
84 recipes = self.domain.get_objects()
85 recipes = sorted(recipes, key=lambda recipe: recipe[0])
86
87 # generate the expected output, shown below, from the above using the
88 # first letter of the recipe as a key to group thing
89 #
90 # name, subtype, docname, anchor, extra, qualifier, description
91 for _name, dispname, typ, docname, anchor, _priority in recipes:
92 content[dispname[0].lower()].append((
93 dispname,
94 0,
95 docname,
96 anchor,
97 docname,
98 '',
99 typ,
100 ))
101
102 # convert the dict to the sorted list of tuples expected
103 content = sorted(content.items())
104
105 return content, True
106
107
108class RecipeDomain(Domain):
109 name = 'recipe'
110 label = 'Recipe Sample'
111 roles = {
112 'ref': XRefRole(),
113 }
114 directives = {
115 'recipe': RecipeDirective,
116 }
117 indices = {
118 RecipeIndex,
119 IngredientIndex,
120 }
121 initial_data = {
122 'recipes': [], # object list
123 'recipe_ingredients': {}, # name -> object
124 }
125 data_version = 0
126
127 def get_full_qualified_name(self, node):
128 return f'recipe.{node.arguments[0]}'
129
130 def get_objects(self):
131 yield from self.data['recipes']
132
133 def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
134 match = [
135 (docname, anchor)
136 for name, sig, typ, docname, anchor, prio in self.get_objects()
137 if sig == target
138 ]
139
140 if len(match) > 0:
141 todocname = match[0][0]
142 targ = match[0][1]
143
144 return make_refnode(builder, fromdocname, todocname, targ, contnode, targ)
145 else:
146 print('Awww, found nothing')
147 return None
148
149 def add_recipe(self, signature, ingredients):
150 """Add a new recipe to the domain."""
151 name = f'recipe.{signature}'
152 anchor = f'recipe-{signature}'
153
154 self.data['recipe_ingredients'][name] = ingredients
155 # name, dispname, type, docname, anchor, priority
156 self.data['recipes'].append((name, signature, 'Recipe', self.env.docname, anchor, 0))
157
158
159def setup(app: Sphinx) -> ExtensionMetadata:
160 app.add_domain(RecipeDomain)
161
162 return {
163 'version': '0.1',
164 'parallel_read_safe': True,
165 'parallel_write_safe': True,
166 }
让我们逐步看一下该扩展的每一部分,以解释发生了什么。
指令集
首先要检查的是 “ 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 = [x.strip() for x in self.options.get('contains').split(',')]
18
19 recipes = self.env.get_domain('recipe')
20 recipes.add_recipe(sig, ingredients)
Unlike 开发了一个 “Hello world” 扩展 and Developing a “TODO” extension, this directive doesn’t derive from
docutils.parsers.rst.Directive
and doesn’t define a run
method.
Instead, it derives from sphinx.directives.ObjectDescription
and
defines handle_signature
and add_target_and_index
methods. This is
because ObjectDescription
is a special-purpose directive that’s intended
for describing things like classes, functions, or, in our case, recipes. More
specifically, handle_signature
implements parsing the signature of the
directive and passes on the object’s name and type to its superclass, while
add_target_and_index
adds a target (to link to) and an entry to the index
for this node.
我们还看到该指令定义了has_content,required_arguments和option_spec。 与先前教程1中的doc中添加的``TodoDirective’’指令不同,该指令除了主体中嵌套的reStructuredText外,还接受单个参数,配方名称和选项``contains’’ 。
索引类
待处理
新增索引的简要概述
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 = {
12 name: (dispname, typ, docname, anchor)
13 for name, dispname, typ, docname, anchor, _ in self.domain.get_objects()
14 }
15 recipe_ingredients = self.domain.data['recipe_ingredients']
16 ingredient_recipes = defaultdict(list)
17
18 # flip from recipe_ingredients to ingredient_recipes
19 for recipe_name, ingredients in recipe_ingredients.items():
20 for ingredient in ingredients:
21 ingredient_recipes[ingredient].append(recipe_name)
22
23 # convert the mapping of ingredient to recipes to produce the expected
24 # output, shown below, using the ingredient name as a key to group
25 #
26 # name, subtype, docname, anchor, extra, qualifier, description
27 for ingredient, recipe_names in ingredient_recipes.items():
28 for recipe_name in recipe_names:
29 dispname, typ, docname, anchor = recipes[recipe_name]
30 content[ingredient].append((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,
22 0,
23 docname,
24 anchor,
25 docname,
26 '',
27 typ,
28 ))
29
30 # convert the dict to the sorted list of tuples expected
31 content = sorted(content.items())
32
33 return content, True
IngredientIndex 和 RecipeIndex 都是从 Index 派生的。 它们实现自定义逻辑以生成定义索引的值的元组。 注意“RecipeIndex”是一个只有一个条目的简单索引。 扩展它以涵盖更多对象类型尚不是代码的一部分。
两个索引都使用方法:meth:Index.generate`来完成工作。 该方法将来自我们域的信息进行组合,排序,然后以Sphinx接受的列表结构返回。 这看起来可能很复杂,但实际上只是一个元组列表,如``(’tomato’,’TomatoSoup’,’test’,’rec-TomatoSoup’…)’’。 有关此API的更多信息,请参阅:doc:`域API指南</extdev/domainapi>。
These index pages can be referenced with the ref
role by combining
the domain name and the index name
value. For example, RecipeIndex
can be
referenced with :ref:`recipe-recipe`
and IngredientIndex
can be referenced
with :ref:`recipe-ingredient`
.
域
Sphinx 域是一个专门的容器,将角色,指令和索引等联系在一起。现在, 让我们一起看看我们在这里创建的域。
1class RecipeDomain(Domain):
2 name = 'recipe'
3 label = 'Recipe Sample'
4 roles = {
5 'ref': XRefRole(),
6 }
7 directives = {
8 'recipe': RecipeDirective,
9 }
10 indices = {
11 RecipeIndex,
12 IngredientIndex,
13 }
14 initial_data = {
15 'recipes': [], # object list
16 'recipe_ingredients': {}, # name -> object
17 }
18 data_version = 0
19
20 def get_full_qualified_name(self, node):
21 return f'recipe.{node.arguments[0]}'
22
23 def get_objects(self):
24 yield from self.data['recipes']
25
26 def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
27 match = [
28 (docname, anchor)
29 for name, sig, typ, docname, anchor, prio in self.get_objects()
30 if sig == target
31 ]
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, contnode, targ)
38 else:
39 print('Awww, found nothing')
40 return None
41
42 def add_recipe(self, signature, ingredients):
43 """Add a new recipe to the domain."""
44 name = f'recipe.{signature}'
45 anchor = f'recipe-{signature}'
46
47 self.data['recipe_ingredients'][name] = ingredients
48 # name, dispname, type, docname, anchor, priority
49 self.data['recipes'].append((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”中解析出目的地。
Moving on, we can see that we’ve defined initial_data
. The values defined in
initial_data
will be copied to env.domaindata[domain_name]
as the
initial data of the domain, and domain instances can access it via
self.data
. We see that we have defined two items in initial_data
:
recipes
and recipe_ingredients
. Each contains a list of all objects
defined (i.e. all recipes) and a hash that maps a canonical ingredient name to
the list of objects. The way we name objects is common across our extension and
is defined in the get_full_qualified_name
method. For each object created,
the canonical name is recipe.<recipename>
, where <recipename>
is the
name the documentation writer gives the object (a recipe). This enables the
extension to use different object types that share the same name. Having a
canonical name and central place for our objects is a huge advantage. Both our
indices and our cross-referencing code use this feature.
setup 功能
:doc:一如往常<todo>,``setup’’功能是必需的,用于将我们扩展的各个部分挂接到Sphinx中。 让我们看一下这个扩展的``setup’’功能。
1def setup(app: Sphinx) -> ExtensionMetadata:
2 app.add_domain(RecipeDomain)
3
4 return {
5 'version': '0.1',
6 'parallel_read_safe': True,
7 'parallel_write_safe': True,
8 }
This looks a little different to what we’re used to seeing. There are no calls
to add_directive()
or even add_role()
. Instead, we
have a single call to add_domain()
followed by some
initialization of the standard domain.
This is because we had already registered our directives,
roles and indexes as part of the directive itself.
使用扩展¶
现在,您可以在整个项目中使用扩展名。 例如:
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.
The important things to note are the use of the :recipe:ref:
role to
cross-reference the recipe actually defined elsewhere (using the
:recipe:recipe:
directive).
延伸阅读¶
有关更多信息,请参考 docutils 文档和:doc:/ extdev / index。