Merged
[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 from jinja2.defaults import DEFAULT_FILTERS, DEFAULT_TESTS
143 jinja_filters = dump_functions(DEFAULT_FILTERS)
144 jinja_tests = dump_functions(DEFAULT_TESTS)
145
146
147 def jinja_nodes(dirname, arguments, options, content, lineno,
148                 content_offset, block_text, state, state_machine):
149     from jinja2.nodes import Node
150     doc = ViewList()
151     def walk(node, indent):
152         p = ' ' * indent
153         sig = ', '.join(node.fields)
154         doc.append(p + '.. autoclass:: %s(%s)' % (node.__name__, sig), '')
155         if node.abstract:
156             members = []
157             for key, name in node.__dict__.iteritems():
158                 if not key.startswith('_') and \
159                    not hasattr(node.__base__, key) and callable(name):
160                     members.append(key)
161             if members:
162                 members.sort()
163                 doc.append('%s :members: %s' % (p, ', '.join(members)), '')
164         if node.__base__ != object:
165             doc.append('', '')
166             doc.append('%s :Node type: :class:`%s`' %
167                        (p, node.__base__.__name__), '')
168         doc.append('', '')
169         children = node.__subclasses__()
170         children.sort(key=lambda x: x.__name__.lower())
171         for child in children:
172             walk(child, indent)
173     walk(Node, 0)
174     return parse_rst(state, content_offset, doc)
175
176
177 def inject_toc(app, doctree, docname):
178     titleiter = iter(doctree.traverse(nodes.title))
179     try:
180         # skip first title, we are not interested in that one
181         titleiter.next()
182         title = titleiter.next()
183         # and check if there is at least another title
184         titleiter.next()
185     except StopIteration:
186         return
187     tocnode = nodes.section('')
188     tocnode['classes'].append('toc')
189     toctitle = nodes.section('')
190     toctitle['classes'].append('toctitle')
191     toctitle.append(nodes.title(text='Table Of Contents'))
192     tocnode.append(toctitle)
193     tocnode += doctree.document.settings.env.get_toc_for(docname)[0][1]
194     title.parent.insert(title.parent.children.index(title), tocnode)
195
196
197 def setup(app):
198     app.add_directive('jinjafilters', jinja_filters, 0, (0, 0, 0))
199     app.add_directive('jinjatests', jinja_tests, 0, (0, 0, 0))
200     app.add_directive('jinjanodes', jinja_nodes, 0, (0, 0, 0))
201     # uncomment for inline toc.  links are broken unfortunately
202     ##app.connect('doctree-resolved', inject_toc)