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'\b(([a-zA-Z_]\w+)\(\))', flags=re.ASCII) 26 27# 28# Sphinx 2 uses the same :c:type role for struct, union, enum and typedef 29# 30RE_generic_type = re.compile(r'\b(struct|union|enum|typedef)\s+([a-zA-Z_]\w+)', 31 flags=re.ASCII) 32 33# 34# Sphinx 3 uses a different C role for each one of struct, union, enum and 35# typedef 36# 37RE_struct = re.compile(r'\b(struct)\s+([a-zA-Z_]\w+)', flags=re.ASCII) 38RE_union = re.compile(r'\b(union)\s+([a-zA-Z_]\w+)', flags=re.ASCII) 39RE_enum = re.compile(r'\b(enum)\s+([a-zA-Z_]\w+)', flags=re.ASCII) 40RE_typedef = re.compile(r'\b(typedef)\s+([a-zA-Z_]\w+)', flags=re.ASCII) 41 42# 43# Detects a reference to a documentation page of the form Documentation/... with 44# an optional extension 45# 46RE_doc = re.compile(r'\bDocumentation(/[\w\-_/]+)(\.\w+)*') 47 48# 49# Reserved C words that we should skip when cross-referencing 50# 51Skipnames = [ 'for', 'if', 'register', 'sizeof', 'struct', 'unsigned' ] 52 53 54# 55# Many places in the docs refer to common system calls. It is 56# pointless to try to cross-reference them and, as has been known 57# to happen, somebody defining a function by these names can lead 58# to the creation of incorrect and confusing cross references. So 59# just don't even try with these names. 60# 61Skipfuncs = [ 'open', 'close', 'read', 'write', 'fcntl', 'mmap', 62 'select', 'poll', 'fork', 'execve', 'clone', 'ioctl', 63 'socket' ] 64 65def markup_refs(docname, app, node): 66 t = node.astext() 67 done = 0 68 repl = [ ] 69 # 70 # Associate each regex with the function that will markup its matches 71 # 72 markup_func_sphinx2 = {RE_doc: markup_doc_ref, 73 RE_function: markup_c_ref, 74 RE_generic_type: markup_c_ref} 75 76 markup_func_sphinx3 = {RE_doc: markup_doc_ref, 77 RE_function: markup_func_ref_sphinx3, 78 RE_struct: markup_c_ref, 79 RE_union: markup_c_ref, 80 RE_enum: markup_c_ref, 81 RE_typedef: markup_c_ref} 82 83 if sphinx.version_info[0] >= 3: 84 markup_func = markup_func_sphinx3 85 else: 86 markup_func = markup_func_sphinx2 87 88 match_iterators = [regex.finditer(t) for regex in markup_func] 89 # 90 # Sort all references by the starting position in text 91 # 92 sorted_matches = sorted(chain(*match_iterators), key=lambda m: m.start()) 93 for m in sorted_matches: 94 # 95 # Include any text prior to match as a normal text node. 96 # 97 if m.start() > done: 98 repl.append(nodes.Text(t[done:m.start()])) 99 100 # 101 # Call the function associated with the regex that matched this text and 102 # append its return to the text 103 # 104 repl.append(markup_func[m.re](docname, app, m)) 105 106 done = m.end() 107 if done < len(t): 108 repl.append(nodes.Text(t[done:])) 109 return repl 110 111# 112# In sphinx3 we can cross-reference to C macro and function, each one with its 113# own C role, but both match the same regex, so we try both. 114# 115def markup_func_ref_sphinx3(docname, app, match): 116 class_str = ['c-func', 'c-macro'] 117 reftype_str = ['function', 'macro'] 118 119 cdom = app.env.domains['c'] 120 # 121 # Go through the dance of getting an xref out of the C domain 122 # 123 target = match.group(2) 124 target_text = nodes.Text(match.group(0)) 125 xref = None 126 if not (target in Skipfuncs or target in Skipnames): 127 for class_s, reftype_s in zip(class_str, reftype_str): 128 lit_text = nodes.literal(classes=['xref', 'c', class_s]) 129 lit_text += target_text 130 pxref = addnodes.pending_xref('', refdomain = 'c', 131 reftype = reftype_s, 132 reftarget = target, modname = None, 133 classname = None) 134 # 135 # XXX The Latex builder will throw NoUri exceptions here, 136 # work around that by ignoring them. 137 # 138 try: 139 xref = cdom.resolve_xref(app.env, docname, app.builder, 140 reftype_s, target, pxref, 141 lit_text) 142 except NoUri: 143 xref = None 144 145 if xref: 146 return xref 147 148 return target_text 149 150def markup_c_ref(docname, app, match): 151 class_str = {# Sphinx 2 only 152 RE_function: 'c-func', 153 RE_generic_type: 'c-type', 154 # Sphinx 3+ only 155 RE_struct: 'c-struct', 156 RE_union: 'c-union', 157 RE_enum: 'c-enum', 158 RE_typedef: 'c-type', 159 } 160 reftype_str = {# Sphinx 2 only 161 RE_function: 'function', 162 RE_generic_type: 'type', 163 # Sphinx 3+ only 164 RE_struct: 'struct', 165 RE_union: 'union', 166 RE_enum: 'enum', 167 RE_typedef: 'type', 168 } 169 170 cdom = app.env.domains['c'] 171 # 172 # Go through the dance of getting an xref out of the C domain 173 # 174 target = match.group(2) 175 target_text = nodes.Text(match.group(0)) 176 xref = None 177 if not ((match.re == RE_function and target in Skipfuncs) 178 or (target in Skipnames)): 179 lit_text = nodes.literal(classes=['xref', 'c', class_str[match.re]]) 180 lit_text += target_text 181 pxref = addnodes.pending_xref('', refdomain = 'c', 182 reftype = reftype_str[match.re], 183 reftarget = target, modname = None, 184 classname = None) 185 # 186 # XXX The Latex builder will throw NoUri exceptions here, 187 # work around that by ignoring them. 188 # 189 try: 190 xref = cdom.resolve_xref(app.env, docname, app.builder, 191 reftype_str[match.re], target, pxref, 192 lit_text) 193 except NoUri: 194 xref = None 195 # 196 # Return the xref if we got it; otherwise just return the plain text. 197 # 198 if xref: 199 return xref 200 else: 201 return target_text 202 203# 204# Try to replace a documentation reference of the form Documentation/... with a 205# cross reference to that page 206# 207def markup_doc_ref(docname, app, match): 208 stddom = app.env.domains['std'] 209 # 210 # Go through the dance of getting an xref out of the std domain 211 # 212 target = match.group(1) 213 xref = None 214 pxref = addnodes.pending_xref('', refdomain = 'std', reftype = 'doc', 215 reftarget = target, modname = None, 216 classname = None, refexplicit = False) 217 # 218 # XXX The Latex builder will throw NoUri exceptions here, 219 # work around that by ignoring them. 220 # 221 try: 222 xref = stddom.resolve_xref(app.env, docname, app.builder, 'doc', 223 target, pxref, None) 224 except NoUri: 225 xref = None 226 # 227 # Return the xref if we got it; otherwise just return the plain text. 228 # 229 if xref: 230 return xref 231 else: 232 return nodes.Text(match.group(0)) 233 234def auto_markup(app, doctree, name): 235 # 236 # This loop could eventually be improved on. Someday maybe we 237 # want a proper tree traversal with a lot of awareness of which 238 # kinds of nodes to prune. But this works well for now. 239 # 240 # The nodes.literal test catches ``literal text``, its purpose is to 241 # avoid adding cross-references to functions that have been explicitly 242 # marked with cc:func:. 243 # 244 for para in doctree.traverse(nodes.paragraph): 245 for node in para.traverse(nodes.Text): 246 if not isinstance(node.parent, nodes.literal): 247 node.parent.replace(node, markup_refs(name, app, node)) 248 249def setup(app): 250 app.connect('doctree-resolved', auto_markup) 251 return { 252 'parallel_read_safe': True, 253 'parallel_write_safe': True, 254 } 255