frosix

Multiparty signature service (experimental)
Log | Files | Refs | README | LICENSE

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)