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