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
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
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
            src = env.srcdir + "/" + doc
            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)