1 #emacs: -*- coding: utf-8; mode: python-mode; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: t -*-
2 #ex: set sts=4 ts=4 sw=4 et:
9 :copyright: Copyright 2010 Yaroslav O. Halchenko
14 from random import sample
16 from docutils import nodes
17 from docutils.parsers.rst import directives, Directive
18 from docutils.parsers.rst.directives import body
20 from sphinx.environment import NoUri
21 from sphinx.util.compat import Directive, make_admonition
24 class quote_node(nodes.Body, nodes.Element): pass
25 class quotes(nodes.General, nodes.Element): pass
31 def _prep_tags(options):
32 """Extract tags listed in a string"""
33 options['tags'] = set((x.strip()
35 options.get('tags', '').split(',')))
37 class Quote(Directive):
39 A quote entry, displayed (if configured) in the form of an admonition.
43 required_arguments = 0
44 optional_arguments = 0
45 final_argument_whitespace = False
47 'author': directives.unchanged,
48 'affiliation': directives.unchanged,
49 'date': directives.unchanged,
50 'group': directives.unchanged,
51 'tags': directives.unchanged, # list of tags per quote
52 'source': directives.unchanged}
56 env = self.state.document.settings.env
57 options = self.options
58 if hasattr(env, 'new_serialno'):
59 targetid = 'index-%s' % env.new_serialno('index')
61 targetid = "index-%s" % env.index_num
62 targetnode = nodes.target('', '', ids=[targetid])
63 targetnode['classes'] = ['epigraph']
66 node += nodes.block_quote(
68 nodes.paragraph('', '\n'.join(self.content), classes=['text']))
69 #state.nested_parse(self.content, self.content_offset, node)
72 if isinstance(element, nodes.block_quote):
73 element['classes'] += ['epigraph']
75 signode = [nodes.attribution('--', '--')]
76 # Embed all components within attributions
77 siglb = nodes.line_block('')
80 options['date'] = '[%(date)s]' % options
81 if 'source' in options:
82 options['source'] = 'Source: %(source)s' % options
83 for el in ['author', 'date', 'affiliation', 'source']:
85 siglb += [nodes.inline('', ' '+options[el], classes=[el])]
86 signode[0].extend(siglb)
87 node[0].extend(signode)
88 node.line = self.lineno
90 _prep_tags(self.options)
91 node.options = options
92 return [targetnode] + [node]
96 def process_quotes(app, doctree):
97 # collect all quotes in the environment
98 # this is not done in the directive itself because it some transformations
99 # must have already been run, e.g. substitutions
100 _info("process_quotes")
102 env = app.builder.env
103 if not hasattr(env, 'quote_all_quotes'):
104 env.quote_all_quotes = []
105 for node in doctree.traverse(quote_node):
107 targetnode = node.parent[node.parent.index(node) - 1]
108 if not isinstance(targetnode, nodes.target):
112 env.quote_all_quotes.append({
113 'docname': env.docname,
115 'quote': node, #.deepcopy(),
116 'target': targetnode,
120 class Quotes(Directive):
122 A list of all quote entries.
126 required_arguments = 0
127 optional_arguments = 0
128 final_argument_whitespace = False
130 'random': directives.positive_int, # how many to randomly output
131 'group': directives.unchanged, # what group to show
132 'tags': directives.unchanged, # list of tags to be matched
133 #'sections': lambda a: directives.choice(a, ('date', 'group'))
137 # Simply insert an empty quotes node which will be replaced later
138 # when process_quote_nodes is called
140 # tags which must be matched
141 if 'tags' in self.options:
142 _prep_tags(self.options)
143 res.options = self.options
144 _info("Run Quotes %s" % res)
149 def process_quote_nodes(app, doctree, fromdocname):
150 ## if not app.config['quote_include_quotes']:
151 ## for node in doctree.traverse(quote_node):
152 ## node.parent.remove(node)
154 # Replace all quotes nodes with a list of the collected quotes.
155 # Augment each quote with a backlink to the original location.
156 env = app.builder.env
158 if not hasattr(env, 'quote_all_quotes'):
159 env.quote_all_quotes = []
161 #_info("process_quote_nodes '%s' %i"
162 # % (fromdocname, len(env.quote_all_quotes)))
164 for node in doctree.traverse(quotes):
165 ## if not app.config['quote_include_quotes']:
166 ## node.replace_self([])
171 loptions = node.options
174 if 'tags' in loptions:
175 ltags = loptions['tags']
177 lambda quote: set.issuperset(
178 quote.options['tags'], ltags))
180 if 'group' in loptions:
184 quote.options.get('group', '') == loptions['group'])
186 for quote_info in env.quote_all_quotes:
187 quote_entry = quote_info['quote']
188 if not reduce(operator.__and__,
189 [f(quote_entry) for f in filters],
192 ## Commented out mechanism to back-reference original
195 ## para = nodes.paragraph(classes=['quote-source'])
196 ## filename = env.doc2path(quote_info['docname'], base=None)
197 ## description = _('(The <<original entry>> is located in '
198 ## ' %s, line %d.)') % (filename, quote_info['lineno'])
199 ## desc1 = description[:description.find('<<')]
200 ## desc2 = description[description.find('>>')+2:]
201 ## para += nodes.Text(desc1, desc1)
203 ## # Create a reference
204 ## newnode = nodes.reference('', '', internal=True)
205 ## innernode = nodes.emphasis(_('original entry'), _('original entry'))
207 ## newnode['refuri'] = app.builder.get_relative_uri(
208 ## fromdocname, quote_info['docname'])
209 ## newnode['refuri'] += '#' + quote_info['target']['refid']
211 ## # ignore if no URI can be determined, e.g. for LaTeX output
213 ## newnode.append(innernode)
215 ## para += nodes.Text(desc2, desc2)
216 ## #para += nodes.Text("XXX", "YYY")
218 # (Recursively) resolve references in the quote content
219 env.resolve_references(quote_entry, quote_info['docname'],
222 # Insert into the quotes
223 content.append(quote_entry)
224 ## content.append(para)
226 # Handle additional quotes options
228 # Select a limited number of random samples
229 if loptions.get('random', None):
230 # Randomly select desired number of quotes
231 content = sample(content, loptions['random'])
233 # Group items into sections and intersperse the list
235 if loptions.get('sections', None):
236 raise NotImplementedError
237 term = loptions['sections']
238 terms = [q.options.get(term, None) for q in content]
239 terms = list(set([x for x in terms if x]))
240 # Simply sort according to what to group on,
241 # and then insert sections?
244 section = nodes.section('')
245 section.extend(nodes.title('', 'XXX'))
246 section += content[:2]
247 section.parent = content[0].parent
248 content = [ section ]
250 # Substitute with the content
251 node.replace_self(content)
255 def purge_quotes(app, env, docname):
256 if not hasattr(env, 'quote_all_quotes'):
258 _info("purge_quotes")
259 env.quote_all_quotes = [quote for quote in env.quote_all_quotes
260 if quote['docname'] != docname]
263 def quotes_noop(self, node):
267 ## app.add_config_value('quotes_include_quotes', False, False)
271 app.add_node(quote_node,
272 html=(quotes_noop, quotes_noop),
273 latex=(quotes_noop, quotes_noop),
274 text=(quotes_noop, quotes_noop))
276 app.add_directive('quote', Quote)
277 app.add_directive('quotes', Quotes)
278 app.connect('doctree-read', process_quotes)
279 app.connect('doctree-resolved', process_quote_nodes)
280 app.connect('env-purge-doc', purge_quotes)