automarkup.py 8.7 KB

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