sphinxext.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. """
  2. pygments.sphinxext
  3. ~~~~~~~~~~~~~~~~~~
  4. Sphinx extension to generate automatic documentation of lexers,
  5. formatters and filters.
  6. :copyright: Copyright 2006-2024 by the Pygments team, see AUTHORS.
  7. :license: BSD, see LICENSE for details.
  8. """
  9. import sys
  10. from docutils import nodes
  11. from docutils.statemachine import ViewList
  12. from docutils.parsers.rst import Directive
  13. from sphinx.util.nodes import nested_parse_with_titles
  14. MODULEDOC = '''
  15. .. module:: %s
  16. %s
  17. %s
  18. '''
  19. LEXERDOC = '''
  20. .. class:: %s
  21. :Short names: %s
  22. :Filenames: %s
  23. :MIME types: %s
  24. %s
  25. %s
  26. '''
  27. FMTERDOC = '''
  28. .. class:: %s
  29. :Short names: %s
  30. :Filenames: %s
  31. %s
  32. '''
  33. FILTERDOC = '''
  34. .. class:: %s
  35. :Name: %s
  36. %s
  37. '''
  38. class PygmentsDoc(Directive):
  39. """
  40. A directive to collect all lexers/formatters/filters and generate
  41. autoclass directives for them.
  42. """
  43. has_content = False
  44. required_arguments = 1
  45. optional_arguments = 0
  46. final_argument_whitespace = False
  47. option_spec = {}
  48. def run(self):
  49. self.filenames = set()
  50. if self.arguments[0] == 'lexers':
  51. out = self.document_lexers()
  52. elif self.arguments[0] == 'formatters':
  53. out = self.document_formatters()
  54. elif self.arguments[0] == 'filters':
  55. out = self.document_filters()
  56. elif self.arguments[0] == 'lexers_overview':
  57. out = self.document_lexers_overview()
  58. else:
  59. raise Exception('invalid argument for "pygmentsdoc" directive')
  60. node = nodes.compound()
  61. vl = ViewList(out.split('\n'), source='')
  62. nested_parse_with_titles(self.state, vl, node)
  63. for fn in self.filenames:
  64. self.state.document.settings.record_dependencies.add(fn)
  65. return node.children
  66. def document_lexers_overview(self):
  67. """Generate a tabular overview of all lexers.
  68. The columns are the lexer name, the extensions handled by this lexer
  69. (or "None"), the aliases and a link to the lexer class."""
  70. from pygments.lexers._mapping import LEXERS
  71. import pygments.lexers
  72. out = []
  73. table = []
  74. def format_link(name, url):
  75. if url:
  76. return f'`{name} <{url}>`_'
  77. return name
  78. for classname, data in sorted(LEXERS.items(), key=lambda x: x[1][1].lower()):
  79. lexer_cls = pygments.lexers.find_lexer_class(data[1])
  80. extensions = lexer_cls.filenames + lexer_cls.alias_filenames
  81. table.append({
  82. 'name': format_link(data[1], lexer_cls.url),
  83. 'extensions': ', '.join(extensions).replace('*', '\\*').replace('_', '\\') or 'None',
  84. 'aliases': ', '.join(data[2]),
  85. 'class': f'{data[0]}.{classname}'
  86. })
  87. column_names = ['name', 'extensions', 'aliases', 'class']
  88. column_lengths = [max([len(row[column]) for row in table if row[column]])
  89. for column in column_names]
  90. def write_row(*columns):
  91. """Format a table row"""
  92. out = []
  93. for length, col in zip(column_lengths, columns):
  94. if col:
  95. out.append(col.ljust(length))
  96. else:
  97. out.append(' '*length)
  98. return ' '.join(out)
  99. def write_seperator():
  100. """Write a table separator row"""
  101. sep = ['='*c for c in column_lengths]
  102. return write_row(*sep)
  103. out.append(write_seperator())
  104. out.append(write_row('Name', 'Extension(s)', 'Short name(s)', 'Lexer class'))
  105. out.append(write_seperator())
  106. for row in table:
  107. out.append(write_row(
  108. row['name'],
  109. row['extensions'],
  110. row['aliases'],
  111. f':class:`~{row["class"]}`'))
  112. out.append(write_seperator())
  113. return '\n'.join(out)
  114. def document_lexers(self):
  115. from pygments.lexers._mapping import LEXERS
  116. import pygments
  117. import inspect
  118. import pathlib
  119. out = []
  120. modules = {}
  121. moduledocstrings = {}
  122. for classname, data in sorted(LEXERS.items(), key=lambda x: x[0]):
  123. module = data[0]
  124. mod = __import__(module, None, None, [classname])
  125. self.filenames.add(mod.__file__)
  126. cls = getattr(mod, classname)
  127. if not cls.__doc__:
  128. print(f"Warning: {classname} does not have a docstring.")
  129. docstring = cls.__doc__
  130. if isinstance(docstring, bytes):
  131. docstring = docstring.decode('utf8')
  132. example_file = getattr(cls, '_example', None)
  133. if example_file:
  134. p = pathlib.Path(inspect.getabsfile(pygments)).parent.parent /\
  135. 'tests' / 'examplefiles' / example_file
  136. content = p.read_text(encoding='utf-8')
  137. if not content:
  138. raise Exception(
  139. f"Empty example file '{example_file}' for lexer "
  140. f"{classname}")
  141. if data[2]:
  142. lexer_name = data[2][0]
  143. docstring += '\n\n .. admonition:: Example\n'
  144. docstring += f'\n .. code-block:: {lexer_name}\n\n'
  145. for line in content.splitlines():
  146. docstring += f' {line}\n'
  147. if cls.version_added:
  148. version_line = f'.. versionadded:: {cls.version_added}'
  149. else:
  150. version_line = ''
  151. modules.setdefault(module, []).append((
  152. classname,
  153. ', '.join(data[2]) or 'None',
  154. ', '.join(data[3]).replace('*', '\\*').replace('_', '\\') or 'None',
  155. ', '.join(data[4]) or 'None',
  156. docstring,
  157. version_line))
  158. if module not in moduledocstrings:
  159. moddoc = mod.__doc__
  160. if isinstance(moddoc, bytes):
  161. moddoc = moddoc.decode('utf8')
  162. moduledocstrings[module] = moddoc
  163. for module, lexers in sorted(modules.items(), key=lambda x: x[0]):
  164. if moduledocstrings[module] is None:
  165. raise Exception(f"Missing docstring for {module}")
  166. heading = moduledocstrings[module].splitlines()[4].strip().rstrip('.')
  167. out.append(MODULEDOC % (module, heading, '-'*len(heading)))
  168. for data in lexers:
  169. out.append(LEXERDOC % data)
  170. return ''.join(out)
  171. def document_formatters(self):
  172. from pygments.formatters import FORMATTERS
  173. out = []
  174. for classname, data in sorted(FORMATTERS.items(), key=lambda x: x[0]):
  175. module = data[0]
  176. mod = __import__(module, None, None, [classname])
  177. self.filenames.add(mod.__file__)
  178. cls = getattr(mod, classname)
  179. docstring = cls.__doc__
  180. if isinstance(docstring, bytes):
  181. docstring = docstring.decode('utf8')
  182. heading = cls.__name__
  183. out.append(FMTERDOC % (heading, ', '.join(data[2]) or 'None',
  184. ', '.join(data[3]).replace('*', '\\*') or 'None',
  185. docstring))
  186. return ''.join(out)
  187. def document_filters(self):
  188. from pygments.filters import FILTERS
  189. out = []
  190. for name, cls in FILTERS.items():
  191. self.filenames.add(sys.modules[cls.__module__].__file__)
  192. docstring = cls.__doc__
  193. if isinstance(docstring, bytes):
  194. docstring = docstring.decode('utf8')
  195. out.append(FILTERDOC % (cls.__name__, name, docstring))
  196. return ''.join(out)
  197. def setup(app):
  198. app.add_directive('pygmentsdoc', PygmentsDoc)