xref: /linux/Documentation/sphinx/automarkup.py (revision 4ebdf7be21d627cd36026e4fe366a784bdde377a)
1# SPDX-License-Identifier: GPL-2.0
2# Copyright 2019 Jonathan Corbet <corbet@lwn.net>
3#
4# Apply kernel-specific tweaks after the initial document processing
5# has been done.
6#
7from docutils import nodes
8import sphinx
9from sphinx import addnodes
10if sphinx.version_info[0] < 2 or \
11   sphinx.version_info[0] == 2 and sphinx.version_info[1] < 1:
12    from sphinx.environment import NoUri
13else:
14    from sphinx.errors import NoUri
15import re
16from itertools import chain
17
18#
19# Regex nastiness.  Of course.
20# Try to identify "function()" that's not already marked up some
21# other way.  Sphinx doesn't like a lot of stuff right after a
22# :c:func: block (i.e. ":c:func:`mmap()`s" flakes out), so the last
23# bit tries to restrict matches to things that won't create trouble.
24#
25RE_function = re.compile(r'(([\w_][\w\d_]+)\(\))')
26RE_type = re.compile(r'(struct|union|enum|typedef)\s+([\w_][\w\d_]+)')
27
28#
29# Many places in the docs refer to common system calls.  It is
30# pointless to try to cross-reference them and, as has been known
31# to happen, somebody defining a function by these names can lead
32# to the creation of incorrect and confusing cross references.  So
33# just don't even try with these names.
34#
35Skipfuncs = [ 'open', 'close', 'read', 'write', 'fcntl', 'mmap',
36              'select', 'poll', 'fork', 'execve', 'clone', 'ioctl',
37              'socket' ]
38
39#
40# Find all occurrences of C references (function() and struct/union/enum/typedef
41# type_name) and try to replace them with appropriate cross references.
42#
43def markup_c_refs(docname, app, node):
44    class_str = {RE_function: 'c-func', RE_type: 'c-type'}
45    reftype_str = {RE_function: 'function', RE_type: 'type'}
46
47    cdom = app.env.domains['c']
48    t = node.astext()
49    done = 0
50    repl = [ ]
51    #
52    # Sort all C references by the starting position in text
53    #
54    sorted_matches = sorted(chain(RE_type.finditer(t), RE_function.finditer(t)),
55                            key=lambda m: m.start())
56    for m in sorted_matches:
57        #
58        # Include any text prior to match as a normal text node.
59        #
60        if m.start() > done:
61            repl.append(nodes.Text(t[done:m.start()]))
62        #
63        # Go through the dance of getting an xref out of the C domain
64        #
65        target = m.group(2)
66        target_text = nodes.Text(m.group(0))
67        xref = None
68        if not (m.re == RE_function and target in Skipfuncs):
69            lit_text = nodes.literal(classes=['xref', 'c', class_str[m.re]])
70            lit_text += target_text
71            pxref = addnodes.pending_xref('', refdomain = 'c',
72                                          reftype = reftype_str[m.re],
73                                          reftarget = target, modname = None,
74                                          classname = None)
75            #
76            # XXX The Latex builder will throw NoUri exceptions here,
77            # work around that by ignoring them.
78            #
79            try:
80                xref = cdom.resolve_xref(app.env, docname, app.builder,
81                                         reftype_str[m.re], target, pxref,
82                                         lit_text)
83            except NoUri:
84                xref = None
85        #
86        # Toss the xref into the list if we got it; otherwise just put
87        # the function text.
88        #
89        if xref:
90            repl.append(xref)
91        else:
92            repl.append(target_text)
93        done = m.end()
94    if done < len(t):
95        repl.append(nodes.Text(t[done:]))
96    return repl
97
98def auto_markup(app, doctree, name):
99    #
100    # This loop could eventually be improved on.  Someday maybe we
101    # want a proper tree traversal with a lot of awareness of which
102    # kinds of nodes to prune.  But this works well for now.
103    #
104    # The nodes.literal test catches ``literal text``, its purpose is to
105    # avoid adding cross-references to functions that have been explicitly
106    # marked with cc:func:.
107    #
108    for para in doctree.traverse(nodes.paragraph):
109        for node in para.traverse(nodes.Text):
110            if not isinstance(node.parent, nodes.literal):
111                node.parent.replace(node, markup_c_refs(name, app, node))
112
113def setup(app):
114    app.connect('doctree-resolved', auto_markup)
115    return {
116        'parallel_read_safe': True,
117        'parallel_write_safe': True,
118        }
119