summaryrefslogtreecommitdiff
blob: 9679bbaa490f3217109d455ae98a4d0d7822b9d2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# Handle :PG: policy identifiers for Policy Guide
# (c) 2020 Michał Górny
# 2-clause BSD license

import collections

from docutils import nodes

from sphinx.domains import Index
from sphinx.environment.collectors.toctree import TocTreeCollector
from sphinx.util import logging


logger = logging.getLogger(__name__)

Policy = collections.namedtuple('Policy', ('id', 'title', 'docname',
                                           'chapter'))

toccollector = TocTreeCollector()


class PolicyIndex(Index):
    name = 'policy-index'
    localname = 'Policy Index'
    shortname = 'Policy Index'

    def generate(self, docnames=None):
        env = self.domain.env
        if not hasattr(env, 'policy_index'):
            env.policy_index = []

        entries = collections.defaultdict(list)
        for p in env.policy_index:
            if docnames is not None and p.docname not in docnames:
                continue
            entries[p.chapter].append(('PG' + p.id,  # name
                                       0,            # subtype
                                       p.docname,    # docname
                                       'pg' + p.id,  # anchor
                                       p.title,      # extra
                                       '',           # qualifier
                                       ''))          # descr

        return (sorted([(k, sorted(v)) for k, v in entries.items()],
                key=lambda kv: kv[1]),
                False)


def find_pg_id(section):
    # first child should be title
    title = section.children[0]
    assert isinstance(title, nodes.title)
    # second child should be field list
    cl = section.children[1]
    if not isinstance(cl, nodes.field_list):
        return None, title.astext(), None

    for f in cl.traverse(nodes.field):
        fn = next(iter(f.traverse(nodes.field_name)))
        fv = next(iter(f.traverse(nodes.field_body)))
        if fn.astext().upper() == 'PG':
            if fn.astext() != 'PG':
                raise RuntimeError('PG field must be uppercase')
            iv = '{:04d}'.format(int(fv.astext(), 10))
            if fv.astext() != iv:
                raise RuntimeError('PG value must be 4 digits, zero-padded ({})'
                                   .format(iv))

            el = section
            titles = []
            while el.parent is not None:
                title = el.children[0]
                assert isinstance(title, nodes.title)
                titles.append(title.astext())
                el = el.parent
            # combine all section titles up to but excluding
            # the chapter title
            title = ': '.join(reversed(titles[:-1]))

            return iv, title, titles[-1]

    logger.warning('%s: no PG identifier found', title.astext())
    return None, title.astext(), None


def on_doctree_read(app, doctree):
    env = app.builder.env
    if not hasattr(env, 'policy_index'):
        env.policy_index = []

    for node in doctree.traverse(nodes.section):
        pg_id, title, chapter = find_pg_id(node)
        if pg_id is not None:
            node['ids'].insert(0, 'pg' + pg_id)
            env.policy_index.append(Policy(pg_id, title, env.docname,
                                           chapter))

    # update the table of conents to use the 'pgXXXX' ids
    toccollector.process_doc(app, doctree)


def on_env_purge_doc(app, env, docname):
    if not hasattr(env, 'policy_index'):
        return

    env.policy_index = [p for p in env.policy_index
                        if p.docname != docname]


def on_env_merge_info(app, env, docnames, other):
    if not hasattr(other, 'policy_index'):
        return
    if not hasattr(env, 'policy_index'):
        env.policy_index = []

    env.policy_index.extend(other.policy_index)


def setup(app):
    app.connect('doctree-read', on_doctree_read)
    app.connect('env-purge-doc', on_env_purge_doc)
    app.connect('env-merge-info', on_env_merge_info)
    app.add_index_to_domain('std', PolicyIndex)
    return {
        'version': '0',
        'parallel_read_safe': True,
        'parallel_write_safe': True,
    }