tokenizers
259 строк · 8.4 Кб
1from collections import defaultdict, abc
2from typing import cast
3
4from docutils import nodes
5from docutils.parsers.rst import Directive
6
7import sphinx
8from sphinx.locale import _
9from sphinx.util.docutils import SphinxDirective
10from sphinx.errors import ExtensionError
11
12from conf import languages as LANGUAGES
13
14logger = sphinx.util.logging.getLogger(__name__)
15
16GLOBALNAME = "$GLOBAL$"
17
18
19def update(d, u):
20for k, v in u.items():
21if isinstance(v, abc.Mapping):
22d[k] = update(d.get(k, {}), v)
23else:
24d[k] = v
25return d
26
27
28class EntityNode(nodes.General, nodes.Element):
29pass
30
31
32class EntitiesNode(nodes.General, nodes.Element):
33pass
34
35
36class AllEntities:
37def __init__(self):
38self.entities = defaultdict(dict)
39
40@classmethod
41def install(cls, env):
42if not hasattr(env, "entity_all_entities"):
43entities = cls()
44env.entity_all_entities = entities
45return env.entity_all_entities
46
47def merge(self, other):
48self.entities.update(other.entities)
49
50def purge(self, docname):
51for env_docname in [GLOBALNAME, docname]:
52self.entities[env_docname] = dict(
53[
54(name, entity)
55for name, entity in self.entities[env_docname].items()
56if entity["docname"] != docname
57]
58)
59
60def _extract_entities(self, nodes):
61pass
62
63def _extract_options(self, nodes):
64pass
65
66def _add_entities(self, entities, language, is_global, docname):
67scope = GLOBALNAME if is_global else docname
68for entity in entities:
69name = f'{language}-{entity["name"]}'
70content = entity["content"]
71
72if name in self.entities[scope]:
73logger.warning(
74f'Entity "{name}" has already been defined{" globally" if is_global else ""}',
75location=docname,
76)
77
78self.entities[scope][name] = {"docname": docname, "content": content}
79
80def _extract_global(self, nodes):
81for node in nodes:
82if node.tagname != "field":
83raise Exception(f"Expected a field, found {node.tagname}")
84
85name, _ = node.children
86if name.tagname != "field_name":
87raise Exception(f"Expected a field name here, found {name_node.tagname}")
88
89if str(name.children[0]) == "global":
90return True
91
92def _extract_entities(self, nodes):
93entities = []
94for node in nodes:
95if node.tagname != "definition_list_item":
96raise Exception(f"Expected a list item here, found {node.tagname}")
97
98name_node, content_node = node.children
99if name_node.tagname != "term":
100raise Exception(f"Expected a term here, found {name_node.tagname}")
101if content_node.tagname != "definition":
102raise Exception(f"Expected a definition here, found {content_node.tagname}")
103
104name = str(name_node.children[0])
105if len(content_node.children) == 1 and content_node.children[0].tagname == "paragraph":
106content = content_node.children[0].children[0]
107else:
108content = content_node
109
110entities.append({"name": name, "content": content})
111return entities
112
113def extract(self, node, docname):
114is_global = False
115entities = []
116
117language = None
118for node in node.children:
119if language is None and node.tagname != "paragraph":
120raise Exception(f"Expected language name:\n.. entities:: <LANGUAGE>")
121elif language is None and node.tagname == "paragraph":
122language = str(node.children[0])
123if language not in LANGUAGES:
124raise Exception(
125f'Unknown language "{language}. Might be missing a newline after language"'
126)
127elif node.tagname == "field_list":
128is_global = self._extract_global(node.children)
129elif node.tagname == "definition_list":
130entities.extend(self._extract_entities(node.children))
131else:
132raise Exception(f"Expected a list of terms/options, found {node.tagname}")
133
134self._add_entities(entities, language, is_global, docname)
135
136def resolve_pendings(self, app):
137env = app.builder.env
138
139updates = defaultdict(dict)
140for env_docname in self.entities.keys():
141for name, entity in self.entities[env_docname].items():
142docname = entity["docname"]
143node = entity["content"]
144
145for node in node.traverse(sphinx.addnodes.pending_xref):
146contnode = cast(nodes.TextElement, node[0].deepcopy())
147newnode = None
148
149typ = node["reftype"]
150target = node["reftarget"]
151refdoc = node.get("refdoc", docname)
152domain = None
153
154try:
155if "refdomain" in node and node["refdomain"]:
156# let the domain try to resolve the reference
157try:
158domain = env.domains[node["refdomain"]]
159except KeyError as exc:
160raise NoUri(target, typ) from exc
161newnode = domain.resolve_xref(
162env, refdoc, app.builder, typ, target, node, contnode
163)
164except NoUri:
165newnode = contnode
166
167updates[env_docname][name] = {
168"docname": docname,
169"content": newnode or contnode,
170}
171
172update(self.entities, updates)
173
174def get(self, language, name, docname):
175name = f"{language}-{name}"
176if name in self.entities[docname]:
177return self.entities[docname][name]
178elif name in self.entities[GLOBALNAME]:
179return self.entities[GLOBALNAME][name]
180else:
181return None
182
183
184class EntitiesDirective(SphinxDirective):
185has_content = True
186
187def run(self):
188content = nodes.definition_list()
189self.state.nested_parse(self.content, self.content_offset, content)
190
191try:
192entities = AllEntities.install(self.env)
193entities.extract(content, self.env.docname)
194except Exception as err:
195raise self.error(f'Malformed directive "entities": {err}')
196
197return []
198
199
200def entity_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
201node = EntityNode()
202node.entity = text
203
204return [node], []
205
206
207def process_entity_nodes(app, doctree, docname):
208""" Replace all the entities by their content """
209env = app.builder.env
210
211entities = AllEntities.install(env)
212entities.resolve_pendings(app)
213
214language = None
215try:
216language = next(l for l in LANGUAGES if l in app.tags)
217except Exception:
218logger.warning(f"No language tag specified, not resolving entities in {docname}")
219
220for node in doctree.traverse(EntityNode):
221if language is None:
222node.replace_self(nodes.Text(_(node.entity), _(node.entity)))
223else:
224entity = entities.get(language, node.entity, docname)
225if entity is None:
226node.replace_self(nodes.Text(_(node.entity), _(node.entity)))
227logger.warning(f'Entity "{node.entity}" has not been defined', location=node)
228else:
229node.replace_self(entity["content"])
230
231
232def purge_entities(app, env, docname):
233""" Purge any entity that comes from the given docname """
234entities = AllEntities.install(env)
235entities.purge(docname)
236
237
238def merge_entities(app, env, docnames, other):
239""" Merge multiple environment entities """
240entities = AllEntities.install(env)
241other_entities = AllEntities.install(other)
242entities.merge(other_entities)
243
244
245def setup(app):
246app.add_node(EntityNode)
247app.add_node(EntitiesNode)
248app.add_directive("entities", EntitiesDirective)
249app.add_role("entity", entity_role)
250
251app.connect("doctree-resolved", process_entity_nodes)
252app.connect("env-merge-info", merge_entities)
253app.connect("env-purge-doc", purge_entities)
254
255return {
256"version": "0.1",
257"parallel_read_safe": True,
258"parallel_write_safe": True,
259}
260