Include statements can now be marked with ``ignore missing`` to skip
[jinja2.git] / docs / jinjaext.py
1 # -*- coding: utf-8 -*-
2 """
3     Jinja Documentation Extensions
4     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6     Support for automatically documenting filters and tests.
7
8     :copyright: Copyright 2008 by Armin Ronacher.
9     :license: BSD.
10 """
11 import os
12 import re
13 import inspect
14 import jinja2
15 from itertools import islice
16 from types import BuiltinFunctionType
17 from docutils import nodes
18 from docutils.statemachine import ViewList
19 from sphinx.ext.autodoc import prepare_docstring
20 from sphinx.application import TemplateBridge
21 from pygments.style import Style
22 from pygments.token import Keyword, Name, Comment, String, Error, \
23      Number, Operator, Generic
24 from jinja2 import Environment, FileSystemLoader
25
26
27 def parse_rst(state, content_offset, doc):
28     node = nodes.section()
29     # hack around title style bookkeeping
30     surrounding_title_styles = state.memo.title_styles
31     surrounding_section_level = state.memo.section_level
32     state.memo.title_styles = []
33     state.memo.section_level = 0
34     state.nested_parse(doc, content_offset, node, match_titles=1)
35     state.memo.title_styles = surrounding_title_styles
36     state.memo.section_level = surrounding_section_level
37     return node.children
38
39
40 class JinjaStyle(Style):
41     title = 'Jinja Style'
42     default_style = ""
43     styles = {
44         Comment:                    'italic #aaaaaa',
45         Comment.Preproc:            'noitalic #B11414',
46         Comment.Special:            'italic #505050',
47
48         Keyword:                    'bold #B80000',
49         Keyword.Type:               '#808080',
50
51         Operator.Word:              'bold #B80000',
52
53         Name.Builtin:               '#333333',
54         Name.Function:              '#333333',
55         Name.Class:                 'bold #333333',
56         Name.Namespace:             'bold #333333',
57         Name.Entity:                'bold #363636',
58         Name.Attribute:             '#686868',
59         Name.Tag:                   'bold #686868',
60         Name.Decorator:             '#686868',
61
62         String:                     '#AA891C',
63         Number:                     '#444444',
64
65         Generic.Heading:            'bold #000080',
66         Generic.Subheading:         'bold #800080',
67         Generic.Deleted:            '#aa0000',
68         Generic.Inserted:           '#00aa00',
69         Generic.Error:              '#aa0000',
70         Generic.Emph:               'italic',
71         Generic.Strong:             'bold',
72         Generic.Prompt:             '#555555',
73         Generic.Output:             '#888888',
74         Generic.Traceback:          '#aa0000',
75
76         Error:                      '#F00 bg:#FAA'
77     }
78
79
80 class Jinja2Bridge(TemplateBridge):
81
82     def init(self, builder):
83         path = builder.config.templates_path
84         self.env = Environment(loader=FileSystemLoader(path))
85
86     def render(self, template, context):
87         return self.env.get_template(template).render(context)
88
89
90 _sig_re = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*(\(.*?\))')
91
92
93 def format_function(name, aliases, func):
94     lines = inspect.getdoc(func).splitlines()
95     signature = '()'
96     if isinstance(func, BuiltinFunctionType):
97         match = _sig_re.match(lines[0])
98         if match is not None:
99             del lines[:1 + bool(lines and not lines[0])]
100             signature = match.group(1)
101     else:
102         try:
103             argspec = inspect.getargspec(func)
104             if getattr(func, 'environmentfilter', False) or \
105                getattr(func, 'contextfilter', False):
106                 del argspec[0][0]
107             signature = inspect.formatargspec(*argspec)
108         except:
109             pass
110     result = ['.. function:: %s%s' % (name, signature), '']
111     result.extend('    ' + line for line in lines)
112     if aliases:
113         result.extend(('', '    :aliases: %s' % ', '.join(
114                       '``%s``' % x for x in sorted(aliases))))
115     return result
116
117
118 def dump_functions(mapping):
119     def directive(dirname, arguments, options, content, lineno,
120                       content_offset, block_text, state, state_machine):
121         reverse_mapping = {}
122         for name, func in mapping.iteritems():
123             reverse_mapping.setdefault(func, []).append(name)
124         filters = []
125         for func, names in reverse_mapping.iteritems():
126             aliases = sorted(names, key=lambda x: len(x))
127             name = aliases.pop()
128             filters.append((name, aliases, func))
129         filters.sort()
130
131         result = ViewList()
132         for name, aliases, func in filters:
133             for item in format_function(name, aliases, func):
134                 result.append(item, '<jinjaext>')
135
136         node = nodes.paragraph()
137         state.nested_parse(result, content_offset, node)
138         return node.children
139     return directive
140
141
142 def jinja_changelog(dirname, arguments, options, content, lineno,
143                     content_offset, block_text, state, state_machine):
144     doc = ViewList()
145     changelog = file(os.path.join(os.path.dirname(jinja2.__file__), '..',
146                                   'CHANGES'))
147     try:
148         for line in islice(changelog, 3, None):
149             doc.append(line.rstrip().decode('utf-8'), '<jinjaext>')
150     finally:
151         changelog.close()
152     return parse_rst(state, content_offset, doc)
153
154
155 from jinja2.defaults import DEFAULT_FILTERS, DEFAULT_TESTS
156 jinja_filters = dump_functions(DEFAULT_FILTERS)
157 jinja_tests = dump_functions(DEFAULT_TESTS)
158
159
160 def jinja_nodes(dirname, arguments, options, content, lineno,
161                 content_offset, block_text, state, state_machine):
162     from jinja2.nodes import Node
163     doc = ViewList()
164     def walk(node, indent):
165         p = ' ' * indent
166         sig = ', '.join(node.fields)
167         doc.append(p + '.. autoclass:: %s(%s)' % (node.__name__, sig), '')
168         if node.abstract:
169             members = []
170             for key, name in node.__dict__.iteritems():
171                 if not key.startswith('_') and \
172                    not hasattr(node.__base__, key) and callable(name):
173                     members.append(key)
174             if members:
175                 members.sort()
176                 doc.append('%s :members: %s' % (p, ', '.join(members)), '')
177         if node.__base__ != object:
178             doc.append('', '')
179             doc.append('%s :Node type: :class:`%s`' %
180                        (p, node.__base__.__name__), '')
181         doc.append('', '')
182         children = node.__subclasses__()
183         children.sort(key=lambda x: x.__name__.lower())
184         for child in children:
185             walk(child, indent)
186     walk(Node, 0)
187     return parse_rst(state, content_offset, doc)
188
189
190 def inject_toc(app, doctree, docname):
191     titleiter = iter(doctree.traverse(nodes.title))
192     try:
193         # skip first title, we are not interested in that one
194         titleiter.next()
195         title = titleiter.next()
196         # and check if there is at least another title
197         titleiter.next()
198     except StopIteration:
199         return
200     tocnode = nodes.section('')
201     tocnode['classes'].append('toc')
202     toctitle = nodes.section('')
203     toctitle['classes'].append('toctitle')
204     toctitle.append(nodes.title(text='Table Of Contents'))
205     tocnode.append(toctitle)
206     tocnode += doctree.document.settings.env.get_toc_for(docname)[0][1]
207     title.parent.insert(title.parent.children.index(title), tocnode)
208
209
210 def setup(app):
211     app.add_directive('jinjafilters', jinja_filters, 0, (0, 0, 0))
212     app.add_directive('jinjatests', jinja_tests, 0, (0, 0, 0))
213     app.add_directive('jinjachangelog', jinja_changelog, 0, (0, 0, 0))
214     app.add_directive('jinjanodes', jinja_nodes, 0, (0, 0, 0))
215     # uncomment for inline toc.  links are broken unfortunately
216     ##app.connect('doctree-resolved', inject_toc)