autonumber.py 4.13 KB
Newer Older
1
2
3
from pathlib import Path

from docutils.nodes import Text, label, reference, section
peastman's avatar
peastman committed
4
5
from docutils.parsers.rst import roles
from sphinx.roles import XRefRole
6

7

8
class autonumber(label):
peastman's avatar
peastman committed
9
    pass
10

11

peastman's avatar
peastman committed
12
13
14
class autonumber_ref(reference):
    pass

15

peastman's avatar
peastman committed
16
def autonumber_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
17
    return ([autonumber(text=text)], [])
peastman's avatar
peastman committed
18

19

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def warn(*args, **kwargs):
    """Issue a warning/notification to the user. Same signature as `print`"""
    print("\nAutonumber: ", *args, **kwargs)


def chapter_numbers_by_section(env):
    """Collect a mapping from a section's reference key to its position in the TOC

    The reference key is of the form `f'{path-to-doc}:{section-id}'`.
    The position in the TOC is useful eg. to label sections with numbers.
    It is returned as a tuple of ints"""
    # Record the number of each chapter
    section_numbers = {}
    for doc in env.toc_secnumbers:
        sections = env.toc_secnumbers[doc]
        for section_id in sections:
            # Include the src to disambiguate duplicates
37
            src = str(Path(env.srcdir)/doc)
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
            key = f"{src}:{section_id[1:]}"
            if key in section_numbers:
                warn(f"{section_id} is duplicated in {src}")
            section_numbers[key] = sections[section_id]
    return section_numbers


def get_chapter(node, depth, section_numbers):
    """Get the numerical position of the chapter in which node resides

    args:
        node:
            A docutils node whose chapter we want the number of
        depth:
            How many levels deep into the toctree is a "chapter"
        section_numbers:
            The output of chapter_numbers_by_section(env)
    """
    parent = node.parent
    chapter = None
    while chapter is None:
        if isinstance(parent, section):
            chapter = parent
        parent = parent.parent
    src = str(Path(chapter.source).with_suffix(""))
    chapter_id = chapter.attributes["ids"][0]
    key = src + ":" + chapter_id
    try:
        chapter = section_numbers[key][:depth]
    except KeyError:
        # The above will fail if the section is at the top of a file;
        # There doesn't seem to be a way to get the top section label
        # in chapter_numbers_by_section, so we'll just assume that if
        # the above fails, we're looking for a section with no label:
        key = src + ":"
        warn(f"Assuming {repr(chapter_id)} is a top level section")
        chapter = section_numbers[key][:depth]
    return ".".join(str(i) for i in chapter)


def proc_autonumber(app, doctree, docname):
    index = {}
    ref_table = {}
    autonum_depth = app.config.autonumber_by_depth
    if autonum_depth:
        section_numbers = chapter_numbers_by_section(app.builder.env)
        last_chapter = None
85

peastman's avatar
peastman committed
86
    # Assign numbers to all the autonumbered objects.
87

peastman's avatar
peastman committed
88
    for node in doctree.traverse(autonumber):
89
        category = node.astext().split(",")[0]
peastman's avatar
peastman committed
90
        if category in index:
91
            next_number = index[category] + 1
peastman's avatar
peastman committed
92
        else:
93
94
95
96
            next_number = 1
        if autonum_depth:
            chapter = get_chapter(node, autonum_depth, section_numbers)
            if chapter != last_chapter:
peastman's avatar
peastman committed
97
                index = {}
98
99
100
                next_number = 1
            new_node = Text(f"{category} {chapter}-{next_number}")
            last_chapter = chapter
peastman's avatar
peastman committed
101
        else:
102
103
104
105
            new_node = Text(f"{category} {next_number}")
        index[category] = next_number
        ref_table[node.astext()] = new_node
        node.parent.replace(node, new_node)
106

peastman's avatar
peastman committed
107
    # Replace references with the name of the referenced object
108

peastman's avatar
peastman committed
109
    for ref_info in doctree.traverse(autonumber_ref):
110
111
112
113
114
        target = ref_info["reftarget"]
        if target not in ref_table:
            raise ValueError(f"Unknown target for autonumber reference: {target}")
        ref_info.replace_self(Text(ref_table[target].astext()))

peastman's avatar
peastman committed
115
116

def setup(app):
117
118
    app.add_config_value("autonumber_by_depth", 1, "env")
    roles.register_local_role("autonumber", autonumber_role)
peastman's avatar
peastman committed
119
120
    app.add_node(autonumber)
    app.add_node(autonumber_ref)
121
122
    app.add_role("autonumref", XRefRole(nodeclass=autonumber_ref))
    app.connect("doctree-resolved", proc_autonumber)