Run `./2to3.py -w jinja2`
[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 _sig_re = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*(\(.*?\))')
81
82
83 def format_function(name, aliases, func):
84     lines = inspect.getdoc(func).splitlines()
85     signature = '()'
86     if isinstance(func, BuiltinFunctionType):
87         match = _sig_re.match(lines[0])
88         if match is not None:
89             del lines[:1 + bool(lines and not lines[0])]
90             signature = match.group(1)
91     else:
92         try:
93             argspec = inspect.getargspec(func)
94             if getattr(func, 'environmentfilter', False) or \
95                getattr(func, 'contextfilter', False) or \
96                getattr(func, 'evalcontextfilter', False):
97                 del argspec[0][0]
98             signature = inspect.formatargspec(*argspec)
99         except:
100             pass
101     result = ['.. function:: %s%s' % (name, signature), '']
102     result.extend('    ' + line for line in lines)
103     if aliases:
104         result.extend(('', '    :aliases: %s' % ', '.join(
105                       '``%s``' % x for x in sorted(aliases))))
106     return result
107
108
109 def dump_functions(mapping):
110     def directive(dirname, arguments, options, content, lineno,
111                       content_offset, block_text, state, state_machine):
112         reverse_mapping = {}
113         for name, func in mapping.iteritems():
114             reverse_mapping.setdefault(func, []).append(name)
115         filters = []
116         for func, names in reverse_mapping.iteritems():
117             aliases = sorted(names, key=lambda x: len(x))
118             name = aliases.pop()
119             filters.append((name, aliases, func))
120         filters.sort()
121
122         result = ViewList()
123         for name, aliases, func in filters:
124             for item in format_function(name, aliases, func):
125                 result.append(item, '<jinjaext>')
126
127         node = nodes.paragraph()
128         state.nested_parse(result, content_offset, node)
129         return node.children
130     return directive
131
132
133 from jinja2.defaults import DEFAULT_FILTERS, DEFAULT_TESTS
134 jinja_filters = dump_functions(DEFAULT_FILTERS)
135 jinja_tests = dump_functions(DEFAULT_TESTS)
136
137
138 def jinja_nodes(dirname, arguments, options, content, lineno,
139                 content_offset, block_text, state, state_machine):
140     from jinja2.nodes import Node
141     doc = ViewList()
142     def walk(node, indent):
143         p = ' ' * indent
144         sig = ', '.join(node.fields)
145         doc.append(p + '.. autoclass:: %s(%s)' % (node.__name__, sig), '')
146         if node.abstract:
147             members = []
148             for key, name in node.__dict__.iteritems():
149                 if not key.startswith('_') and \
150                    not hasattr(node.__base__, key) and callable(name):
151                     members.append(key)
152             if members:
153                 members.sort()
154                 doc.append('%s :members: %s' % (p, ', '.join(members)), '')
155         if node.__base__ != object:
156             doc.append('', '')
157             doc.append('%s :Node type: :class:`%s`' %
158                        (p, node.__base__.__name__), '')
159         doc.append('', '')
160         children = node.__subclasses__()
161         children.sort(key=lambda x: x.__name__.lower())
162         for child in children:
163             walk(child, indent)
164     walk(Node, 0)
165     return parse_rst(state, content_offset, doc)
166
167
168 def inject_toc(app, doctree, docname):
169     titleiter = iter(doctree.traverse(nodes.title))
170     try:
171         # skip first title, we are not interested in that one
172         titleiter.next()
173         title = titleiter.next()
174         # and check if there is at least another title
175         titleiter.next()
176     except StopIteration:
177         return
178     tocnode = nodes.section('')
179     tocnode['classes'].append('toc')
180     toctitle = nodes.section('')
181     toctitle['classes'].append('toctitle')
182     toctitle.append(nodes.title(text='Table Of Contents'))
183     tocnode.append(toctitle)
184     tocnode += doctree.document.settings.env.get_toc_for(docname)[0][1]
185     title.parent.insert(title.parent.children.index(title), tocnode)
186
187
188 def setup(app):
189     app.add_directive('jinjafilters', jinja_filters, 0, (0, 0, 0))
190     app.add_directive('jinjatests', jinja_tests, 0, (0, 0, 0))
191     app.add_directive('jinjanodes', jinja_nodes, 0, (0, 0, 0))
192     # uncomment for inline toc.  links are broken unfortunately
193     ##app.connect('doctree-resolved', inject_toc)