ebicsdomain.py (8241B)
1 """ 2 EBICS documentation domain. 3 4 :copyright: Copyright 2019 by Taler Systems SA 5 :license: LGPLv3+ 6 :author: Florian Dold 7 """ 8 9 import re 10 import unicodedata 11 12 from docutils import nodes 13 from typing import List, Optional, Iterable, Dict, Tuple 14 from typing import cast 15 16 from docutils import nodes 17 from docutils.nodes import Element, Node 18 from docutils.statemachine import StringList 19 20 from sphinx import addnodes 21 from sphinx.roles import XRefRole 22 from sphinx.domains import Domain, ObjType, Index 23 from sphinx.directives import directives 24 from sphinx.util.docutils import SphinxDirective 25 from sphinx.util.nodes import make_refnode 26 from sphinx.util import logging 27 28 logger = logging.getLogger(__name__) 29 30 def make_glossary_term(env: "BuildEnvironment", textnodes: Iterable[Node], index_key: str, 31 source: str, lineno: int, new_id: str = None) -> nodes.term: 32 # get a text-only representation of the term and register it 33 # as a cross-reference target 34 term = nodes.term('', '', *textnodes) 35 term.source = source 36 term.line = lineno 37 38 gloss_entries = env.temp_data.setdefault('gloss_entries', set()) 39 termtext = term.astext() 40 if new_id is None: 41 new_id = nodes.make_id('ebics-order-' + termtext.lower()) 42 if new_id == 'ebics-order': 43 # the term is not good for node_id. Generate it by sequence number instead. 44 new_id = 'ebics-order-%d' % env.new_serialno('ebics') 45 while new_id in gloss_entries: 46 new_id = 'ebics-order-%d' % env.new_serialno('ebics') 47 gloss_entries.add(new_id) 48 49 ebics = env.get_domain('ebics') 50 ebics.add_object('order', termtext.lower(), env.docname, new_id) 51 52 term['ids'].append(new_id) 53 term['names'].append(new_id) 54 55 return term 56 57 58 def split_term_classifiers(line: str) -> List[Optional[str]]: 59 # split line into a term and classifiers. if no classifier, None is used.. 60 parts = re.split(' +: +', line) + [None] 61 return parts 62 63 64 class EbicsOrders(SphinxDirective): 65 has_content = True 66 required_arguments = 0 67 optional_arguments = 0 68 final_argument_whitespace = False 69 option_spec = { 70 'sorted': directives.flag, 71 } 72 73 def run(self): 74 node = addnodes.glossary() 75 node.document = self.state.document 76 77 # This directive implements a custom format of the reST definition list 78 # that allows multiple lines of terms before the definition. This is 79 # easy to parse since we know that the contents of the glossary *must 80 # be* a definition list. 81 82 # first, collect single entries 83 entries = [] # type: List[Tuple[List[Tuple[str, str, int]], StringList]] 84 in_definition = True 85 in_comment = False 86 was_empty = True 87 messages = [] # type: List[nodes.Node] 88 for line, (source, lineno) in zip(self.content, self.content.items): 89 # empty line -> add to last definition 90 if not line: 91 if in_definition and entries: 92 entries[-1][1].append('', source, lineno) 93 was_empty = True 94 continue 95 # unindented line -> a term 96 if line and not line[0].isspace(): 97 # enable comments 98 if line.startswith('.. '): 99 in_comment = True 100 continue 101 else: 102 in_comment = False 103 104 # first term of definition 105 if in_definition: 106 if not was_empty: 107 messages.append(self.state.reporter.warning( 108 _('glossary term must be preceded by empty line'), 109 source=source, line=lineno)) 110 entries.append(([(line, source, lineno)], StringList())) 111 in_definition = False 112 # second term and following 113 else: 114 if was_empty: 115 messages.append(self.state.reporter.warning( 116 _('glossary terms must not be separated by empty lines'), 117 source=source, line=lineno)) 118 if entries: 119 entries[-1][0].append((line, source, lineno)) 120 else: 121 messages.append(self.state.reporter.warning( 122 _('glossary seems to be misformatted, check indentation'), 123 source=source, line=lineno)) 124 elif in_comment: 125 pass 126 else: 127 if not in_definition: 128 # first line of definition, determines indentation 129 in_definition = True 130 indent_len = len(line) - len(line.lstrip()) 131 if entries: 132 entries[-1][1].append(line[indent_len:], source, lineno) 133 else: 134 messages.append(self.state.reporter.warning( 135 _('glossary seems to be misformatted, check indentation'), 136 source=source, line=lineno)) 137 was_empty = False 138 139 # now, parse all the entries into a big definition list 140 items = [] 141 for terms, definition in entries: 142 termtexts = [] # type: List[str] 143 termnodes = [] # type: List[nodes.Node] 144 system_messages = [] # type: List[nodes.Node] 145 for line, source, lineno in terms: 146 parts = split_term_classifiers(line) 147 # parse the term with inline markup 148 # classifiers (parts[1:]) will not be shown on doctree 149 textnodes, sysmsg = self.state.inline_text(parts[0], lineno) 150 151 # use first classifier as a index key 152 term = make_glossary_term(self.env, textnodes, parts[1], source, lineno) 153 term.rawsource = line 154 system_messages.extend(sysmsg) 155 termtexts.append(term.astext()) 156 termnodes.append(term) 157 158 termnodes.extend(system_messages) 159 160 defnode = nodes.definition() 161 if definition: 162 self.state.nested_parse(definition, definition.items[0][1], 163 defnode) 164 termnodes.append(defnode) 165 items.append((termtexts, 166 nodes.definition_list_item('', *termnodes))) 167 168 if 'sorted' in self.options: 169 items.sort(key=lambda x: 170 unicodedata.normalize('NFD', x[0][0].lower())) 171 172 node["sorted"] = 'sorted' in self.options 173 174 dlist = nodes.definition_list() 175 dlist['classes'].append('glossary') 176 dlist.extend(item[1] for item in items) 177 node += dlist 178 return messages + [node] 179 180 181 class EbicsDomain(Domain): 182 """Ebics domain.""" 183 184 name = 'ebics' 185 label = 'EBICS' 186 187 object_types = { 188 'order': ObjType('order', 'ebics'), 189 } 190 191 directives = { 192 'orders': EbicsOrders, 193 } 194 195 roles = { 196 'order': XRefRole(lowercase=True, warn_dangling=True, innernodeclass=nodes.inline), 197 } 198 199 dangling_warnings = { 200 'order': 'undefined EBICS order type: %(target)s', 201 } 202 203 @property 204 def objects(self) -> Dict[Tuple[str, str], Tuple[str, str]]: 205 return self.data.setdefault('objects', {}) # (objtype, name) -> docname, labelid 206 207 def clear_doc(self, docname): 208 for key, (fn, _l) in list(self.objects.items()): 209 if fn == docname: 210 del self.objects[key] 211 212 def resolve_xref(self, env, fromdocname, builder, typ, target, 213 node, contnode): 214 try: 215 info = self.objects[(str(typ), str(target))] 216 except KeyError: 217 return None 218 else: 219 anchor = "ebics-order-{}".format(str(target)) 220 title = typ.upper() + ' ' + target 221 return make_refnode(builder, fromdocname, info[0], anchor, 222 contnode, title) 223 224 def add_object(self, objtype: str, name: str, docname: str, labelid: str) -> None: 225 self.objects[objtype, name] = (docname, labelid) 226 227 228 def setup(app): 229 app.add_domain(EbicsDomain)