|
|
|
"""
|
|
|
|
pygments.sphinxext
|
|
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
Sphinx extension to generate automatic documentation of lexers,
|
|
|
|
formatters and filters.
|
|
|
|
|
|
|
|
:copyright: Copyright 2006-2023 by the Pygments team, see AUTHORS.
|
|
|
|
:license: BSD, see LICENSE for details.
|
|
|
|
"""
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
from docutils import nodes
|
|
|
|
from docutils.statemachine import ViewList
|
|
|
|
from docutils.parsers.rst import Directive
|
|
|
|
from sphinx.util.nodes import nested_parse_with_titles
|
|
|
|
|
|
|
|
|
|
|
|
MODULEDOC = '''
|
|
|
|
.. module:: %s
|
|
|
|
|
|
|
|
%s
|
|
|
|
%s
|
|
|
|
'''
|
|
|
|
|
|
|
|
LEXERDOC = '''
|
|
|
|
.. class:: %s
|
|
|
|
|
|
|
|
:Short names: %s
|
|
|
|
:Filenames: %s
|
|
|
|
:MIME types: %s
|
|
|
|
|
|
|
|
%s
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
FMTERDOC = '''
|
|
|
|
.. class:: %s
|
|
|
|
|
|
|
|
:Short names: %s
|
|
|
|
:Filenames: %s
|
|
|
|
|
|
|
|
%s
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
FILTERDOC = '''
|
|
|
|
.. class:: %s
|
|
|
|
|
|
|
|
:Name: %s
|
|
|
|
|
|
|
|
%s
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
class PygmentsDoc(Directive):
|
|
|
|
"""
|
|
|
|
A directive to collect all lexers/formatters/filters and generate
|
|
|
|
autoclass directives for them.
|
|
|
|
"""
|
|
|
|
has_content = False
|
|
|
|
required_arguments = 1
|
|
|
|
optional_arguments = 0
|
|
|
|
final_argument_whitespace = False
|
|
|
|
option_spec = {}
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
self.filenames = set()
|
|
|
|
if self.arguments[0] == 'lexers':
|
|
|
|
out = self.document_lexers()
|
|
|
|
elif self.arguments[0] == 'formatters':
|
|
|
|
out = self.document_formatters()
|
|
|
|
elif self.arguments[0] == 'filters':
|
|
|
|
out = self.document_filters()
|
|
|
|
elif self.arguments[0] == 'lexers_overview':
|
|
|
|
out = self.document_lexers_overview()
|
|
|
|
else:
|
|
|
|
raise Exception('invalid argument for "pygmentsdoc" directive')
|
|
|
|
node = nodes.compound()
|
|
|
|
vl = ViewList(out.split('\n'), source='')
|
|
|
|
nested_parse_with_titles(self.state, vl, node)
|
|
|
|
for fn in self.filenames:
|
|
|
|
self.state.document.settings.record_dependencies.add(fn)
|
|
|
|
return node.children
|
|
|
|
|
|
|
|
def document_lexers_overview(self):
|
|
|
|
"""Generate a tabular overview of all lexers.
|
|
|
|
|
|
|
|
The columns are the lexer name, the extensions handled by this lexer
|
|
|
|
(or "None"), the aliases and a link to the lexer class."""
|
|
|
|
from pygments.lexers._mapping import LEXERS
|
|
|
|
import pygments.lexers
|
|
|
|
out = []
|
|
|
|
|
|
|
|
table = []
|
|
|
|
|
|
|
|
def format_link(name, url):
|
|
|
|
if url:
|
|
|
|
return f'`{name} <{url}>`_'
|
|
|
|
return name
|
|
|
|
|
|
|
|
for classname, data in sorted(LEXERS.items(), key=lambda x: x[1][1].lower()):
|
|
|
|
lexer_cls = pygments.lexers.find_lexer_class(data[1])
|
|
|
|
extensions = lexer_cls.filenames + lexer_cls.alias_filenames
|
|
|
|
|
|
|
|
table.append({
|
|
|
|
'name': format_link(data[1], lexer_cls.url),
|
|
|
|
'extensions': ', '.join(extensions).replace('*', '\\*').replace('_', '\\') or 'None',
|
|
|
|
'aliases': ', '.join(data[2]),
|
|
|
|
'class': f'{data[0]}.{classname}'
|
|
|
|
})
|
|
|
|
|
|
|
|
column_names = ['name', 'extensions', 'aliases', 'class']
|
|
|
|
column_lengths = [max([len(row[column]) for row in table if row[column]])
|
|
|
|
for column in column_names]
|
|
|
|
|
|
|
|
def write_row(*columns):
|
|
|
|
"""Format a table row"""
|
|
|
|
out = []
|
|
|
|
for l, c in zip(column_lengths, columns):
|
|
|
|
if c:
|
|
|
|
out.append(c.ljust(l))
|
|
|
|
else:
|
|
|
|
out.append(' '*l)
|
|
|
|
|
|
|
|
return ' '.join(out)
|
|
|
|
|
|
|
|
def write_seperator():
|
|
|
|
"""Write a table separator row"""
|
|
|
|
sep = ['='*c for c in column_lengths]
|
|
|
|
return write_row(*sep)
|
|
|
|
|
|
|
|
out.append(write_seperator())
|
|
|
|
out.append(write_row('Name', 'Extension(s)', 'Short name(s)', 'Lexer class'))
|
|
|
|
out.append(write_seperator())
|
|
|
|
for row in table:
|
|
|
|
out.append(write_row(
|
|
|
|
row['name'],
|
|
|
|
row['extensions'],
|
|
|
|
row['aliases'],
|
|
|
|
f':class:`~{row["class"]}`'))
|
|
|
|
out.append(write_seperator())
|
|
|
|
|
|
|
|
return '\n'.join(out)
|
|
|
|
|
|
|
|
def document_lexers(self):
|
|
|
|
from pygments.lexers._mapping import LEXERS
|
|
|
|
import pygments
|
|
|
|
import inspect
|
|
|
|
import pathlib
|
|
|
|
|
|
|
|
out = []
|
|
|
|
modules = {}
|
|
|
|
moduledocstrings = {}
|
|
|
|
for classname, data in sorted(LEXERS.items(), key=lambda x: x[0]):
|
|
|
|
module = data[0]
|
|
|
|
mod = __import__(module, None, None, [classname])
|
|
|
|
self.filenames.add(mod.__file__)
|
|
|
|
cls = getattr(mod, classname)
|
|
|
|
if not cls.__doc__:
|
|
|
|
print("Warning: %s does not have a docstring." % classname)
|
|
|
|
docstring = cls.__doc__
|
|
|
|
if isinstance(docstring, bytes):
|
|
|
|
docstring = docstring.decode('utf8')
|
|
|
|
|
|
|
|
example_file = getattr(cls, '_example', None)
|
|
|
|
if example_file:
|
|
|
|
p = pathlib.Path(inspect.getabsfile(pygments)).parent.parent /\
|
|
|
|
'tests' / 'examplefiles' / example_file
|
|
|
|
content = p.read_text(encoding='utf-8')
|
|
|
|
if not content:
|
|
|
|
raise Exception(
|
|
|
|
f"Empty example file '{example_file}' for lexer "
|
|
|
|
f"{classname}")
|
|
|
|
|
|
|
|
if data[2]:
|
|
|
|
lexer_name = data[2][0]
|
|
|
|
docstring += '\n\n .. admonition:: Example\n'
|
|
|
|
docstring += f'\n .. code-block:: {lexer_name}\n\n'
|
|
|
|
for line in content.splitlines():
|
|
|
|
docstring += f' {line}\n'
|
|
|
|
|
|
|
|
modules.setdefault(module, []).append((
|
|
|
|
classname,
|
|
|
|
', '.join(data[2]) or 'None',
|
|
|
|
', '.join(data[3]).replace('*', '\\*').replace('_', '\\') or 'None',
|
|
|
|
', '.join(data[4]) or 'None',
|
|
|
|
docstring))
|
|
|
|
if module not in moduledocstrings:
|
|
|
|
moddoc = mod.__doc__
|
|
|
|
if isinstance(moddoc, bytes):
|
|
|
|
moddoc = moddoc.decode('utf8')
|
|
|
|
moduledocstrings[module] = moddoc
|
|
|
|
|
|
|
|
for module, lexers in sorted(modules.items(), key=lambda x: x[0]):
|
|
|
|
if moduledocstrings[module] is None:
|
|
|
|
raise Exception("Missing docstring for %s" % (module,))
|
|
|
|
heading = moduledocstrings[module].splitlines()[4].strip().rstrip('.')
|
|
|
|
out.append(MODULEDOC % (module, heading, '-'*len(heading)))
|
|
|
|
for data in lexers:
|
|
|
|
out.append(LEXERDOC % data)
|
|
|
|
|
|
|
|
return ''.join(out)
|
|
|
|
|
|
|
|
def document_formatters(self):
|
|
|
|
from pygments.formatters import FORMATTERS
|
|
|
|
|
|
|
|
out = []
|
|
|
|
for classname, data in sorted(FORMATTERS.items(), key=lambda x: x[0]):
|
|
|
|
module = data[0]
|
|
|
|
mod = __import__(module, None, None, [classname])
|
|
|
|
self.filenames.add(mod.__file__)
|
|
|
|
cls = getattr(mod, classname)
|
|
|
|
docstring = cls.__doc__
|
|
|
|
if isinstance(docstring, bytes):
|
|
|
|
docstring = docstring.decode('utf8')
|
|
|
|
heading = cls.__name__
|
|
|
|
out.append(FMTERDOC % (heading, ', '.join(data[2]) or 'None',
|
|
|
|
', '.join(data[3]).replace('*', '\\*') or 'None',
|
|
|
|
docstring))
|
|
|
|
return ''.join(out)
|
|
|
|
|
|
|
|
def document_filters(self):
|
|
|
|
from pygments.filters import FILTERS
|
|
|
|
|
|
|
|
out = []
|
|
|
|
for name, cls in FILTERS.items():
|
|
|
|
self.filenames.add(sys.modules[cls.__module__].__file__)
|
|
|
|
docstring = cls.__doc__
|
|
|
|
if isinstance(docstring, bytes):
|
|
|
|
docstring = docstring.decode('utf8')
|
|
|
|
out.append(FILTERDOC % (cls.__name__, name, docstring))
|
|
|
|
return ''.join(out)
|
|
|
|
|
|
|
|
|
|
|
|
def setup(app):
|
|
|
|
app.add_directive('pygmentsdoc', PygmentsDoc)
|