~azzar1/unity/add-show-desktop-key

« back to all changes in this revision

Viewing changes to ivle/worksheet/rst.py

  • Committer: stevenbird
  • Date: 2007-12-05 22:41:05 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:7
GPL license

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python
2
 
#
3
 
# Natural Language Toolkit: Documentation generation script
4
 
#
5
 
# Copyright (C) 2001-2006 University of Pennsylvania
6
 
# Author: Edward Loper <edloper@gradient.cis.upenn.edu>
7
 
#         Steven Bird (substantially cut down)
8
 
# URL: <http://nltk.sf.net>
9
 
# For license information, see LICENSE.TXT
10
 
 
11
 
r"""
12
 
This is a customized driver for converting docutils reStructuredText
13
 
documents into HTML and LaTeX.  It customizes the standard writers in
14
 
the following ways:
15
 
    
16
 
    - Source code highlighting is added to all doctest blocks.  In
17
 
      the HTML output, highlighting is performed using css classes:
18
 
      'pysrc-prompt', 'pysrc-keyword', 'pysrc-string', 'pysrc-comment',
19
 
      and 'pysrc-output'.
20
 
"""
21
 
import re, os.path, textwrap, sys, pickle, inspect
22
 
from optparse import OptionParser
23
 
 
24
 
import docutils.core, docutils.nodes, docutils.io
25
 
from docutils.writers import Writer
26
 
from docutils.writers.html4css1 import HTMLTranslator, Writer as HTMLWriter
27
 
from docutils.parsers.rst import directives, roles
28
 
from docutils.readers.standalone import Reader as StandaloneReader
29
 
from docutils.transforms import Transform
30
 
import docutils.writers.html4css1
31
 
from doctest import DocTestParser
32
 
import docutils.statemachine
33
 
 
34
 
 
35
 
 
36
 
LATEX_VALIGN_IS_BROKEN = True
37
 
"""Set to true to compensate for a bug in the latex writer.  I've
38
 
   submitted a patch to docutils, so hopefully this wil be fixed
39
 
   soon."""
40
 
 
41
 
LATEX_DPI = 140
42
 
"""The scaling factor that should be used to display bitmapped images
43
 
   in latex/pdf output (specified in dots per inch).  E.g., if a
44
 
   bitmapped image is 100 pixels wide, it will be scaled to
45
 
   100/LATEX_DPI inches wide for the latex/pdf output.  (Larger
46
 
   values produce smaller images in the generated pdf.)"""
47
 
 
48
 
TREE_IMAGE_DIR = 'tree_images/'
49
 
"""The directory that tree images should be written to."""
50
 
 
51
 
EXTERN_REFERENCE_FILES = []
52
 
"""A list of .ref files, for crossrefering to external documents (used
53
 
   when building one chapter at a time)."""
54
 
 
55
 
BIBTEX_FILE = '../refs.bib'
56
 
"""The name of the bibtex file used to generate bibliographic entries."""
57
 
 
58
 
BIBLIOGRAPHY_HTML = "bibliography.html"
59
 
"""The name of the HTML file containing the bibliography (for
60
 
   hyperrefs from citations)."""
61
 
 
62
 
# needs to include "../doc" so it works in /doc_contrib
63
 
LATEX_STYLESHEET_PATH = '../../doc/definitions.sty'
64
 
"""The name of the LaTeX style file used for generating PDF output."""
65
 
 
66
 
LOCAL_BIBLIOGRAPHY = False
67
 
"""If true, assume that this document contains the bibliography, and
68
 
   link to it locally; if false, assume that bibliographic links
69
 
   should point to L{BIBLIOGRAPHY_HTML}."""
70
 
 
71
 
PYLISTING_DIR = 'pylisting/'
72
 
"""The directory where pylisting files should be written."""
73
 
 
74
 
PYLISTING_EXTENSION = ".py"
75
 
"""Extension for pylisting files."""
76
 
 
77
 
INCLUDE_DOCTESTS_IN_PYLISTING_FILES = False
78
 
"""If true, include code from doctests in the generated pylisting
79
 
   files. """
80
 
 
81
 
CALLOUT_IMG = '<img src="callouts/callout%s.gif" alt="[%s]" class="callout" />'
82
 
"""HTML code for callout images in pylisting blocks."""
83
 
 
84
 
REF_EXTENSION = '.ref'
85
 
"""File extension for reference files."""
86
 
 
87
 
# needs to include "../doc" so it works in /doc_contrib
88
 
CSS_STYLESHEET = '/dev/null' #/home/nick/exercise-ui/ivle/webapp/tutorial/media/nltkdoc.css'
89
 
 
90
 
 
91
 
OUTPUT_FORMAT = None
92
 
"""A global variable, set by main(), indicating the output format for
93
 
   the current file.  Can be 'latex' or 'html' or 'ref'."""
94
 
 
95
 
OUTPUT_BASENAME = None
96
 
"""A global variable, set by main(), indicating the base filename
97
 
   of the current file (i.e., the filename with its extension
98
 
   stripped).  This is used to generate filenames for images."""
99
 
 
100
 
COPY_CLIPBOARD_JS = ''
101
 
 
102
 
 
103
 
######################################################################
104
 
#{ Reference files
105
 
######################################################################
106
 
 
107
 
def read_ref_file(basename=None):
108
 
    if basename is None: basename = OUTPUT_BASENAME
109
 
    if not os.path.exists(basename + REF_EXTENSION):
110
 
        warning('File %r does not exist!' %
111
 
                (basename + REF_EXTENSION))
112
 
        return dict(targets=(),terms={},reference_labes={})
113
 
    f = open(basename + REF_EXTENSION)
114
 
    ref_info = pickle.load(f)
115
 
    f.close()
116
 
    return ref_info
117
 
 
118
 
def write_ref_file(ref_info):
119
 
    f = open(OUTPUT_BASENAME + REF_EXTENSION, 'w')
120
 
    pickle.dump(ref_info, f)
121
 
    f.close()
122
 
 
123
 
def add_to_ref_file(**ref_info):
124
 
    if os.path.exists(OUTPUT_BASENAME + REF_EXTENSION):
125
 
        info = read_ref_file()
126
 
        info.update(ref_info)
127
 
        write_ref_file(info)
128
 
    else:
129
 
        write_ref_file(ref_info)
130
 
 
131
 
######################################################################
132
 
#{ Directives
133
 
######################################################################
134
 
 
135
 
class example(docutils.nodes.paragraph): pass
136
 
 
137
 
def example_directive(name, arguments, options, content, lineno,
138
 
                      content_offset, block_text, state, state_machine):
139
 
    """
140
 
    Basic use::
141
 
 
142
 
        .. example:: John went to the store.
143
 
 
144
 
    To refer to examples, use::
145
 
 
146
 
        .. _store:
147
 
        .. example:: John went to the store.
148
 
 
149
 
        In store_, John performed an action.
150
 
    """
151
 
    text = '\n'.join(content)
152
 
    node = example(text)
153
 
    state.nested_parse(content, content_offset, node)
154
 
    return [node]
155
 
example_directive.content = True
156
 
directives.register_directive('example', example_directive)
157
 
directives.register_directive('ex', example_directive)
158
 
 
159
 
def doctest_directive(name, arguments, options, content, lineno,
160
 
                      content_offset, block_text, state, state_machine):
161
 
    """
162
 
    Used to explicitly mark as doctest blocks things that otherwise
163
 
    wouldn't look like doctest blocks.
164
 
    """
165
 
    text = '\n'.join(content)
166
 
    if re.match(r'.*\n\s*\n', block_text):
167
 
        warning('doctest-ignore on line %d will not be ignored, '
168
 
             'because there is\na blank line between ".. doctest-ignore::"'
169
 
             ' and the doctest example.' % lineno)
170
 
    return [docutils.nodes.doctest_block(text, text, codeblock=True)]
171
 
doctest_directive.content = True
172
 
directives.register_directive('doctest-ignore', doctest_directive)
173
 
 
174
 
_treenum = 0
175
 
def tree_directive(name, arguments, options, content, lineno,
176
 
                   content_offset, block_text, state, state_machine):
177
 
    global _treenum
178
 
    text = '\n'.join(arguments) + '\n'.join(content)
179
 
    _treenum += 1
180
 
    # Note: the two filenames generated by these two cases should be
181
 
    # different, to prevent conflicts.
182
 
    if OUTPUT_FORMAT == 'latex':
183
 
        density, scale = 300, 150
184
 
        scale = scale * options.get('scale', 100) / 100
185
 
        filename = '%s-tree-%s.pdf' % (OUTPUT_BASENAME, _treenum)
186
 
        align = LATEX_VALIGN_IS_BROKEN and 'bottom' or 'top'
187
 
    elif OUTPUT_FORMAT == 'html':
188
 
        density, scale = 100, 100
189
 
        density = density * options.get('scale', 100) / 100
190
 
        filename = '%s-tree-%s.png' % (OUTPUT_BASENAME, _treenum)
191
 
        align = 'top'
192
 
    elif OUTPUT_FORMAT == 'ref':
193
 
        return []
194
 
    else:
195
 
        assert 0, 'bad output format %r' % OUTPUT_FORMAT
196
 
    if not os.path.exists(TREE_IMAGE_DIR):
197
 
        os.mkdir(TREE_IMAGE_DIR)
198
 
    try:
199
 
        filename = os.path.join(TREE_IMAGE_DIR, filename)
200
 
        tree_to_image(text, filename, density)
201
 
    except Exception, e:
202
 
        raise
203
 
        warning('Error parsing tree: %s\n%s\n%s' % (e, text, filename))
204
 
        return [example(text, text)]
205
 
 
206
 
    imagenode = docutils.nodes.image(uri=filename, scale=scale, align=align)
207
 
    return [imagenode]
208
 
 
209
 
tree_directive.arguments = (1,0,1)
210
 
tree_directive.content = True
211
 
tree_directive.options = {'scale': directives.nonnegative_int}
212
 
directives.register_directive('tree', tree_directive)
213
 
 
214
 
def avm_directive(name, arguments, options, content, lineno,
215
 
                      content_offset, block_text, state, state_machine):
216
 
    text = '\n'.join(content)
217
 
    try:
218
 
        if OUTPUT_FORMAT == 'latex':
219
 
            latex_avm = parse_avm(textwrap.dedent(text)).as_latex()
220
 
            return [docutils.nodes.paragraph('','',
221
 
                       docutils.nodes.raw('', latex_avm, format='latex'))]
222
 
        elif OUTPUT_FORMAT == 'html':
223
 
            return [parse_avm(textwrap.dedent(text)).as_table()]
224
 
        elif OUTPUT_FORMAT == 'ref':
225
 
            return [docutils.nodes.paragraph()]
226
 
    except ValueError, e:
227
 
        if isinstance(e.args[0], int):
228
 
            warning('Error parsing avm on line %s' % (lineno+e.args[0]))
229
 
        else:
230
 
            raise
231
 
            warning('Error parsing avm on line %s: %s' % (lineno, e))
232
 
        node = example(text, text)
233
 
        state.nested_parse(content, content_offset, node)
234
 
        return [node]
235
 
avm_directive.content = True
236
 
directives.register_directive('avm', avm_directive)
237
 
 
238
 
def def_directive(name, arguments, options, content, lineno,
239
 
                  content_offset, block_text, state, state_machine):
240
 
    state_machine.document.setdefault('__defs__', {})[arguments[0]] = 1
241
 
    return []
242
 
def_directive.arguments = (1, 0, 0)
243
 
directives.register_directive('def', def_directive)
244
 
    
245
 
def ifdef_directive(name, arguments, options, content, lineno,
246
 
                    content_offset, block_text, state, state_machine):
247
 
    if arguments[0] in state_machine.document.get('__defs__', ()):
248
 
        node = docutils.nodes.compound('')
249
 
        state.nested_parse(content, content_offset, node)
250
 
        return [node]
251
 
    else:
252
 
        return []
253
 
ifdef_directive.arguments = (1, 0, 0)
254
 
ifdef_directive.content = True
255
 
directives.register_directive('ifdef', ifdef_directive)
256
 
    
257
 
def ifndef_directive(name, arguments, options, content, lineno,
258
 
                    content_offset, block_text, state, state_machine):
259
 
    if arguments[0] not in state_machine.document.get('__defs__', ()):
260
 
        node = docutils.nodes.compound('')
261
 
        state.nested_parse(content, content_offset, node)
262
 
        return [node]
263
 
    else:
264
 
        return []
265
 
ifndef_directive.arguments = (1, 0, 0)
266
 
ifndef_directive.content = True
267
 
directives.register_directive('ifndef', ifndef_directive)
268
 
 
269
 
 
270
 
######################################################################
271
 
#{ Table Directive
272
 
######################################################################
273
 
_table_ids = set()
274
 
def table_directive(name, arguments, options, content, lineno,
275
 
                    content_offset, block_text, state, state_machine):
276
 
    # The identifier for this table.
277
 
    if arguments:
278
 
        table_id = arguments[0]
279
 
        if table_id in _table_ids:
280
 
            warning("Duplicate table id %r" % table_id)
281
 
        _table_ids.add(table_id)
282
 
 
283
 
        # Create a target element for the table
284
 
        target = docutils.nodes.target(names=[table_id])
285
 
        state_machine.document.note_explicit_target(target)
286
 
 
287
 
    # Parse the contents.
288
 
    node = docutils.nodes.compound('')
289
 
    state.nested_parse(content, content_offset, node)
290
 
    if len(node) == 0 or not isinstance(node[0], docutils.nodes.table):
291
 
        return [state_machine.reporter.error(
292
 
            'Error in "%s" directive: expected table as first child' %
293
 
            name)]
294
 
 
295
 
    # Move the caption into the table.
296
 
    table = node[0]
297
 
    caption = docutils.nodes.caption('','', *node[1:])
298
 
    table.append(caption)
299
 
 
300
 
    # Return the target and the table.
301
 
    if arguments:
302
 
        return [target, table]
303
 
    else:
304
 
        return [table]
305
 
    
306
 
    
307
 
table_directive.arguments = (0,1,0) # 1 optional arg, no whitespace
308
 
table_directive.content = True
309
 
table_directive.options = {'caption': directives.unchanged}
310
 
directives.register_directive('table', table_directive)
311
 
 
312
 
 
313
 
######################################################################
314
 
#{ Program Listings
315
 
######################################################################
316
 
# We define a new attribute for doctest blocks: 'is_codeblock'.  If
317
 
# this attribute is true, then the block contains python code only
318
 
# (i.e., don't expect to find prompts.)
319
 
 
320
 
class pylisting(docutils.nodes.General, docutils.nodes.Element):
321
 
    """
322
 
    Python source code listing.
323
 
 
324
 
    Children: doctest_block+ caption?
325
 
    """
326
 
class callout_marker(docutils.nodes.Inline, docutils.nodes.Element):
327
 
    """
328
 
    A callout marker for doctest block.  This element contains no
329
 
    children; and defines the attribute 'number'.
330
 
    """
331
 
 
332
 
DOCTEST_BLOCK_RE = re.compile('((?:[ ]*>>>.*\n?(?:.*[^ ].*\n?)+\s*)+)',
333
 
                              re.MULTILINE)
334
 
CALLOUT_RE = re.compile(r'#[ ]+\[_([\w-]+)\][ ]*$', re.MULTILINE)
335
 
 
336
 
from docutils.nodes import fully_normalize_name as normalize_name
337
 
 
338
 
_listing_ids = set()
339
 
def pylisting_directive(name, arguments, options, content, lineno,
340
 
                      content_offset, block_text, state, state_machine):
341
 
    # The identifier for this listing.
342
 
    listing_id = arguments[0]
343
 
    if listing_id in _listing_ids:
344
 
        warning("Duplicate listing id %r" % listing_id)
345
 
    _listing_ids.add(listing_id)
346
 
    
347
 
    # Create the pylisting element itself.
348
 
    listing = pylisting('\n'.join(content), name=listing_id, callouts={})
349
 
 
350
 
    # Create a target element for the pylisting.
351
 
    target = docutils.nodes.target(names=[listing_id])
352
 
    state_machine.document.note_explicit_target(target)
353
 
 
354
 
    # Divide the text into doctest blocks.
355
 
    for i, v in enumerate(DOCTEST_BLOCK_RE.split('\n'.join(content))):
356
 
        pysrc = re.sub(r'\A( *\n)+', '', v.rstrip())
357
 
        if pysrc.strip():
358
 
            listing.append(docutils.nodes.doctest_block(pysrc, pysrc,
359
 
                                                        is_codeblock=(i%2==0)))
360
 
 
361
 
    # Add an optional caption.
362
 
    if options.get('caption'):
363
 
        cap = options['caption'].split('\n')
364
 
        caption = docutils.nodes.compound()
365
 
        state.nested_parse(docutils.statemachine.StringList(cap),
366
 
                           content_offset, caption)
367
 
        if (len(caption) == 1 and isinstance(caption[0],
368
 
                                             docutils.nodes.paragraph)):
369
 
            listing.append(docutils.nodes.caption('', '', *caption[0]))
370
 
        else:
371
 
            warning("Caption should be a single paragraph")
372
 
            listing.append(docutils.nodes.caption('', '', *caption))
373
 
 
374
 
    return [target, listing]
375
 
 
376
 
pylisting_directive.arguments = (1,0,0) # 1 required arg, no whitespace
377
 
pylisting_directive.content = True
378
 
pylisting_directive.options = {'caption': directives.unchanged}
379
 
directives.register_directive('pylisting', pylisting_directive)
380
 
 
381
 
def callout_directive(name, arguments, options, content, lineno,
382
 
                      content_offset, block_text, state, state_machine):
383
 
    if arguments:
384
 
        prefix = '%s-' % arguments[0]
385
 
    else:
386
 
        prefix = ''
387
 
    node = docutils.nodes.compound('')
388
 
    state.nested_parse(content, content_offset, node)
389
 
    if not (len(node.children) == 1 and
390
 
            isinstance(node[0], docutils.nodes.field_list)):
391
 
        return [state_machine.reporter.error(
392
 
            'Error in "%s" directive: may contain a single defintion '
393
 
            'list only.' % (name), line=lineno)]
394
 
 
395
 
    node[0]['classes'] = ['callouts']
396
 
    for field in node[0]:
397
 
        if len(field[0]) != 1:
398
 
            return [state_machine.reporter.error(
399
 
                'Error in "%s" directive: bad field id' % (name), line=lineno)]
400
 
            
401
 
        field_name = prefix+('%s' % field[0][0])
402
 
        field[0].clear()
403
 
        field[0].append(docutils.nodes.reference(field_name, field_name,
404
 
                                                 refid=field_name))
405
 
        field[0]['classes'] = ['callout']
406
 
 
407
 
    return [node[0]]
408
 
 
409
 
callout_directive.arguments = (0,1,0) # 1 optional arg, no whitespace
410
 
callout_directive.content = True
411
 
directives.register_directive('callouts', callout_directive)
412
 
 
413
 
_OPTION_DIRECTIVE_RE = re.compile(
414
 
    r'(\n[ ]*\.\.\.[ ]*)?#\s*doctest:\s*([^\n\'"]*)$', re.MULTILINE)
415
 
def strip_doctest_directives(text):
416
 
    return _OPTION_DIRECTIVE_RE.sub('', text)
417
 
 
418
 
 
419
 
######################################################################
420
 
#{ RST In/Out table
421
 
######################################################################
422
 
 
423
 
def rst_example_directive(name, arguments, options, content, lineno,
424
 
                    content_offset, block_text, state, state_machine):
425
 
    raw = docutils.nodes.literal_block('', '\n'.join(content))
426
 
    out = docutils.nodes.compound('')
427
 
    state.nested_parse(content, content_offset, out)
428
 
    if OUTPUT_FORMAT == 'latex':
429
 
        return [
430
 
            docutils.nodes.definition_list('',
431
 
              docutils.nodes.definition_list_item('',
432
 
                docutils.nodes.term('','Input'),
433
 
                docutils.nodes.definition('', raw)),
434
 
              docutils.nodes.definition_list_item('',
435
 
                docutils.nodes.term('','Rendered'),
436
 
                docutils.nodes.definition('', out)))]
437
 
    else:
438
 
        return [
439
 
            docutils.nodes.table('',
440
 
              docutils.nodes.tgroup('',
441
 
                docutils.nodes.colspec(colwidth=5,classes=['rst-raw']),
442
 
                docutils.nodes.colspec(colwidth=5),
443
 
                docutils.nodes.thead('',
444
 
                  docutils.nodes.row('',
445
 
                    docutils.nodes.entry('',
446
 
                      docutils.nodes.paragraph('','Input')),
447
 
                    docutils.nodes.entry('',
448
 
                      docutils.nodes.paragraph('','Rendered')))),
449
 
                docutils.nodes.tbody('',
450
 
                  docutils.nodes.row('',
451
 
                    docutils.nodes.entry('',raw),
452
 
                    docutils.nodes.entry('',out)))),
453
 
              classes=["rst-example"])]
454
 
 
455
 
rst_example_directive.arguments = (0, 0, 0)
456
 
rst_example_directive.content = True
457
 
directives.register_directive('rst_example', rst_example_directive)
458
 
 
459
 
 
460
 
######################################################################
461
 
#{ Glosses
462
 
######################################################################
463
 
 
464
 
"""
465
 
.. gloss::
466
 
   This  | is | used | to | make | aligned | glosses.
467
 
    NN   | BE |  VB  | TO |  VB  |  JJ     |   NN
468
 
   *Foog blogg blarg.*
469
 
"""
470
 
 
471
 
class gloss(docutils.nodes.Element): "glossrow+"
472
 
class glossrow(docutils.nodes.Element): "paragraph+"
473
 
 
474
 
def gloss_directive(name, arguments, options, content, lineno,
475
 
                    content_offset, block_text, state, state_machine):
476
 
    # Transform into a table.
477
 
    lines = list(content)
478
 
    maxlen = max(len(line) for line in lines)
479
 
    lines = [('|%-'+`maxlen`+'s|') % line for line in lines]
480
 
    tablestr = ''
481
 
    prevline = ''
482
 
    for line in (lines+['']):
483
 
        div = ['-']*(maxlen+2)
484
 
        for m in re.finditer(r'\|', prevline):
485
 
            div[m.start()] = '+'
486
 
        for m in re.finditer(r'\|', line):
487
 
            div[m.start()] = '+'
488
 
        tablestr += ''.join(div) + '\n' + line + '\n'
489
 
        prevline = line
490
 
    table_lines = tablestr.strip().split('\n')
491
 
    new_content = docutils.statemachine.StringList(table_lines)
492
 
    # [XX] DEBUG GLOSSES:
493
 
    # print 'converted to:'
494
 
    # print tablestr
495
 
 
496
 
    # Parse the table.
497
 
    node = docutils.nodes.compound('')
498
 
    state.nested_parse(new_content, content_offset, node)
499
 
    if not (len(node.children) == 1 and
500
 
            isinstance(node[0], docutils.nodes.table)):
501
 
        error = state_machine.reporter.error(
502
 
            'Error in "%s" directive: may contain a single table '
503
 
            'only.' % (name), line=lineno)
504
 
        return [error]
505
 
    table = node[0]
506
 
    table['classes'] = ['gloss', 'nolines']
507
 
    
508
 
    colspecs = table[0]
509
 
    for colspec in colspecs:
510
 
        colspec['colwidth'] = colspec.get('colwidth',4)/2
511
 
    
512
 
    return [example('', '', table)]
513
 
gloss_directive.arguments = (0, 0, 0)
514
 
gloss_directive.content = True
515
 
directives.register_directive('gloss', gloss_directive)
516
 
 
517
 
 
518
 
######################################################################
519
 
#{ Bibliography
520
 
######################################################################
521
 
 
522
 
class Citations(Transform):
523
 
    default_priority = 500 # before footnotes.
524
 
    def apply(self):
525
 
        if not os.path.exists(BIBTEX_FILE):
526
 
            warning('Warning bibtex file %r not found.  '
527
 
                    'Not linking citations.' % BIBTEX_FILE)
528
 
            return
529
 
        bibliography = self.read_bibinfo(BIBTEX_FILE)
530
 
        for k, citation_refs in self.document.citation_refs.items():
531
 
            for citation_ref in citation_refs[:]:
532
 
                cite = bibliography.get(citation_ref['refname'].lower())
533
 
                if cite:
534
 
                    new_cite = self.citeref(cite, citation_ref['refname'])
535
 
                    citation_ref.replace_self(new_cite)
536
 
                    self.document.citation_refs[k].remove(citation_ref)
537
 
 
538
 
    def citeref(self, cite, key):
539
 
        if LOCAL_BIBLIOGRAPHY:
540
 
            return docutils.nodes.raw('', '\cite{%s}' % key, format='latex')
541
 
        else:
542
 
            return docutils.nodes.reference('', '', docutils.nodes.Text(cite),
543
 
                                    refuri='%s#%s' % (BIBLIOGRAPHY_HTML, key))
544
 
 
545
 
    BIB_ENTRY = re.compile(r'@\w+{.*')
546
 
    def read_bibinfo(self, filename):
547
 
        bibliography = {} # key -> authors, year
548
 
        key = None
549
 
        for line in open(filename):
550
 
            line = line.strip()
551
 
            
552
 
            # @InProceedings{<key>,
553
 
            m = re.match(r'@\w+{([^,]+),$', line)
554
 
            if m:
555
 
                key = m.group(1).strip().lower()
556
 
                bibliography[key] = [None, None]
557
 
                
558
 
            #   author = <authors>,
559
 
            m = re.match(r'(?i)author\s*=\s*(.*)$', line)
560
 
            if m and key:
561
 
                bibliography[key][0] = self.bib_authors(m.group(1))
562
 
            else:
563
 
                m = re.match(r'(?i)editor\s*=\s*(.*)$', line)
564
 
                if m and key:
565
 
                    bibliography[key][0] = self.bib_authors(m.group(1))
566
 
                
567
 
            #   year = <year>,
568
 
            m = re.match(r'(?i)year\s*=\s*(.*)$', line)
569
 
            if m and key:
570
 
                bibliography[key][1] = self.bib_year(m.group(1))
571
 
        for key in bibliography:
572
 
            if bibliography[key][0] is None: warning('no author found:', key)
573
 
            if bibliography[key][1] is None: warning('no year found:', key)
574
 
            bibliography[key] = '[%s, %s]' % tuple(bibliography[key])
575
 
            #debug('%20s %s' % (key, `bibliography[key]`))
576
 
        return bibliography
577
 
 
578
 
    def bib_year(self, year):
579
 
        return re.sub(r'["\'{},]', "", year)
580
 
 
581
 
    def bib_authors(self, authors):
582
 
        # Strip trailing comma:
583
 
        if authors[-1:] == ',': authors=authors[:-1]
584
 
        # Strip quotes or braces:
585
 
        authors = re.sub(r'"(.*)"$', r'\1', authors)
586
 
        authors = re.sub(r'{(.*)}$', r'\1', authors)
587
 
        authors = re.sub(r"'(.*)'$", r'\1', authors)
588
 
        # Split on 'and':
589
 
        authors = re.split(r'\s+and\s+', authors)
590
 
        # Keep last name only:
591
 
        authors = [a.split()[-1] for a in authors]
592
 
        # Combine:
593
 
        if len(authors) == 1:
594
 
            return authors[0]
595
 
        elif len(authors) == 2:
596
 
            return '%s & %s' % tuple(authors)
597
 
        elif len(authors) == 3:
598
 
            return '%s, %s, & %s' % tuple(authors)
599
 
        else:
600
 
            return '%s et al' % authors[0]
601
 
        return authors
602
 
 
603
 
 
604
 
######################################################################
605
 
#{ Indexing
606
 
######################################################################
607
 
class termdef(docutils.nodes.Inline, docutils.nodes.TextElement): pass
608
 
class idxterm(docutils.nodes.Inline, docutils.nodes.TextElement): pass
609
 
class index(docutils.nodes.Element): pass
610
 
 
611
 
def idxterm_role(name, rawtext, text, lineno, inliner,
612
 
                 options={}, content=[]):
613
 
    if name == 'dt': options['classes'] = ['termdef']
614
 
    elif name == 'topic': options['classes'] = ['topic']
615
 
    else: options['classes'] = ['term']
616
 
    # Recursively parse the contents of the index term, in case it
617
 
    # contains a substitiution (like |alpha|).
618
 
    nodes, msgs = inliner.parse(text, lineno, memo=inliner,
619
 
                                parent=inliner.parent)
620
 
    return [idxterm(rawtext, '', *nodes, **options)], []
621
 
 
622
 
roles.register_canonical_role('dt', idxterm_role)
623
 
roles.register_canonical_role('idx', idxterm_role)
624
 
roles.register_canonical_role('topic', idxterm_role)
625
 
 
626
 
def index_directive(name, arguments, options, content, lineno,
627
 
                    content_offset, block_text, state, state_machine):
628
 
    pending = docutils.nodes.pending(ConstructIndex)
629
 
    pending.details.update(options)
630
 
    state_machine.document.note_pending(pending)
631
 
    return [index('', pending)]
632
 
index_directive.arguments = (0, 0, 0)
633
 
index_directive.content = False
634
 
index_directive.options = {'extern': directives.flag}
635
 
directives.register_directive('index', index_directive)
636
 
 
637
 
 
638
 
class SaveIndexTerms(Transform):
639
 
    default_priority = 810 # before NumberReferences transform
640
 
    def apply(self):
641
 
        v = FindTermVisitor(self.document)
642
 
        self.document.walkabout(v)
643
 
        
644
 
        if OUTPUT_FORMAT == 'ref':
645
 
            add_to_ref_file(terms=v.terms)
646
 
 
647
 
class ConstructIndex(Transform):
648
 
    default_priority = 820 # after NumberNodes, before NumberReferences.
649
 
    def apply(self):
650
 
        # Find any indexed terms in this document.
651
 
        v = FindTermVisitor(self.document)
652
 
        self.document.walkabout(v)
653
 
        terms = v.terms
654
 
 
655
 
        # Check the extern reference files for additional terms.
656
 
        if 'extern' in self.startnode.details:
657
 
            for filename in EXTERN_REFERENCE_FILES:
658
 
                basename = os.path.splitext(filename)[0]
659
 
                terms.update(read_ref_file(basename)['terms'])
660
 
 
661
 
        # Build the index & insert it into the document.
662
 
        index_node = self.build_index(terms)
663
 
        self.startnode.replace_self(index_node)
664
 
 
665
 
    def build_index(self, terms):
666
 
        if not terms: return []
667
 
        
668
 
        top = docutils.nodes.bullet_list('', classes=['index'])
669
 
        start_letter = None
670
 
        
671
 
        section = None
672
 
        for key in sorted(terms.keys()):
673
 
            if key[:1] != start_letter:
674
 
                top.append(docutils.nodes.list_item(
675
 
                    '', docutils.nodes.paragraph('', key[:1].upper()+'\n',
676
 
                                                 classes=['index-heading']),
677
 
                    docutils.nodes.bullet_list('', classes=['index-section']),
678
 
                    classes=['index']))
679
 
                section = top[-1][-1]
680
 
            section.append(self.entry(terms[key]))
681
 
            start_letter = key[:1]
682
 
        
683
 
        return top
684
 
 
685
 
    def entry(self, term_info):
686
 
        entrytext, name, sectnum = term_info
687
 
        if sectnum is not None:
688
 
            entrytext.append(docutils.nodes.emphasis('', ' (%s)' % sectnum))
689
 
        ref = docutils.nodes.reference('', '', refid=name,
690
 
                                       #resolved=True,
691
 
                                       *entrytext)
692
 
        para = docutils.nodes.paragraph('', '', ref)
693
 
        return docutils.nodes.list_item('', para, classes=['index'])
694
 
 
695
 
class FindTermVisitor(docutils.nodes.SparseNodeVisitor):
696
 
    def __init__(self, document):
697
 
        self.terms = {}
698
 
        docutils.nodes.NodeVisitor.__init__(self, document)
699
 
    def unknown_visit(self, node): pass
700
 
    def unknown_departure(self, node): pass
701
 
 
702
 
    def visit_idxterm(self, node):
703
 
        node['name'] = node['id'] = self.idxterm_key(node)
704
 
        node['names'] = node['ids'] = [node['id']]
705
 
        container = self.container_section(node)
706
 
        
707
 
        entrytext = node.deepcopy()
708
 
        if container: sectnum = container.get('sectnum')
709
 
        else: sectnum = '0'
710
 
        name = node['name']
711
 
        self.terms[node['name']] = (entrytext, name, sectnum)
712
 
            
713
 
    def idxterm_key(self, node):
714
 
        key = re.sub('\W', '_', node.astext().lower())+'_index_term'
715
 
        if key not in self.terms: return key
716
 
        n = 2
717
 
        while '%s_%d' % (key, n) in self.terms: n += 1
718
 
        return '%s_%d' % (key, n)
719
 
 
720
 
    def container_section(self, node):
721
 
        while not isinstance(node, docutils.nodes.section):
722
 
            if node.parent is None: return None
723
 
            else: node = node.parent
724
 
        return node
725
 
 
726
 
 
727
 
 
728
 
######################################################################
729
 
#{ Crossreferences
730
 
######################################################################
731
 
 
732
 
class ResolveExternalCrossrefs(Transform):
733
 
    """
734
 
    Using the information from EXTERN_REFERENCE_FILES, look for any
735
 
    links to external targets, and set their `refuid` appropriately.
736
 
    Also, if they are a figure, section, table, or example, then
737
 
    replace the link of the text with the appropriate counter.
738
 
    """
739
 
    default_priority = 849 # right before dangling refs
740
 
 
741
 
    def apply(self):
742
 
        ref_dict = self.build_ref_dict()
743
 
        v = ExternalCrossrefVisitor(self.document, ref_dict)
744
 
        self.document.walkabout(v)
745
 
 
746
 
    def build_ref_dict(self):
747
 
        """{target -> (uri, label)}"""
748
 
        ref_dict = {}
749
 
        for filename in EXTERN_REFERENCE_FILES:
750
 
            basename = os.path.splitext(filename)[0]
751
 
            if OUTPUT_FORMAT == 'html':
752
 
                uri = os.path.split(basename)[-1]+'.html'
753
 
            else:
754
 
                uri = os.path.split(basename)[-1]+'.pdf'
755
 
            if basename == OUTPUT_BASENAME:
756
 
                pass # don't read our own ref file.
757
 
            elif not os.path.exists(basename+REF_EXTENSION):
758
 
                warning('%s does not exist' % (basename+REF_EXTENSION))
759
 
            else:
760
 
                ref_info = read_ref_file(basename)
761
 
                for ref in ref_info['targets']:
762
 
                    label = ref_info['reference_labels'].get(ref)
763
 
                    ref_dict[ref] = (uri, label)
764
 
 
765
 
        return ref_dict
766
 
    
767
 
class ExternalCrossrefVisitor(docutils.nodes.NodeVisitor):
768
 
    def __init__(self, document, ref_dict):
769
 
        docutils.nodes.NodeVisitor.__init__(self, document)
770
 
        self.ref_dict = ref_dict
771
 
    def unknown_visit(self, node): pass
772
 
    def unknown_departure(self, node): pass
773
 
 
774
 
    # Don't mess with the table of contents.
775
 
    def visit_topic(self, node):
776
 
        if 'contents' in node.get('classes', ()):
777
 
            raise docutils.nodes.SkipNode
778
 
 
779
 
    def visit_reference(self, node):
780
 
        if node.resolved: return
781
 
        node_id = node.get('refid') or node.get('refname')
782
 
        if node_id in self.ref_dict:
783
 
            uri, label = self.ref_dict[node_id]
784
 
            #debug('xref: %20s -> %-30s (label=%s)' % (
785
 
            #    node_id, uri+'#'+node_id, label))
786
 
            node['refuri'] = '%s#%s' % (uri, node_id)
787
 
            node.resolved = True
788
 
 
789
 
            if label is not None:
790
 
                if node.get('expanded_ref'):
791
 
                    warning('Label %s is defined both locally (%s) and '
792
 
                            'externally (%s)' % (node_id, node[0], label))
793
 
                    # hmm...
794
 
                else:
795
 
                    node.clear()
796
 
                    node.append(docutils.nodes.Text(label))
797
 
                    expand_reference_text(node)
798
 
 
799
 
######################################################################
800
 
#{ Exercises
801
 
######################################################################
802
 
 
803
 
"""
804
 
.. exercise:: path.xml
805
 
"""
806
 
 
807
 
class exercise(docutils.nodes.paragraph,docutils.nodes.Element): pass
808
 
 
809
 
def exercise_directive(name, arguments, options, content, lineno,
810
 
                    content_offset, block_text, state, state_machine):
811
 
    return [exercise('', arguments[0])]
812
 
 
813
 
exercise_directive.arguments = (1, 0, 0)
814
 
exercise_directive.content = False
815
 
directives.register_directive('exercise', exercise_directive)
816
 
 
817
 
 
818
 
######################################################################
819
 
#{ Challenges (optional exercises; harder than usual)
820
 
######################################################################
821
 
 
822
 
"""
823
 
.. challenge:: path.xml
824
 
"""
825
 
 
826
 
class challenge(docutils.nodes.paragraph,docutils.nodes.Element): pass
827
 
 
828
 
def challenge_directive(name, arguments, options, content, lineno,
829
 
                    content_offset, block_text, state, state_machine):
830
 
    return [challenge('', arguments[0])]
831
 
 
832
 
challenge_directive.arguments = (1, 0, 0)
833
 
challenge_directive.content = False
834
 
directives.register_directive('challenge', challenge_directive)
835
 
 
836
 
 
837
 
 
838
 
######################################################################
839
 
#{ Figure & Example Numbering
840
 
######################################################################
841
 
 
842
 
# [xx] number examples, figures, etc, relative to chapter?  e.g.,
843
 
# figure 3.2?  maybe number examples within-chapter, but then restart
844
 
# the counter?
845
 
 
846
 
class section_context(docutils.nodes.Invisible, docutils.nodes.Element):
847
 
    def __init__(self, context):
848
 
        docutils.nodes.Element.__init__(self, '', context=context)
849
 
        assert self['context'] in ('body', 'preface', 'appendix')
850
 
 
851
 
def section_context_directive(name, arguments, options, content, lineno,
852
 
                       content_offset, block_text, state, state_machine):
853
 
    return [section_context(name)]
854
 
section_context_directive.arguments = (0,0,0)
855
 
directives.register_directive('preface', section_context_directive)
856
 
directives.register_directive('body', section_context_directive)
857
 
directives.register_directive('appendix', section_context_directive)
858
 
        
859
 
class NumberNodes(Transform):
860
 
    """
861
 
    This transform adds numbers to figures, tables, and examples; and
862
 
    converts references to the figures, tables, and examples to use
863
 
    these numbers.  For example, given the rst source::
864
 
 
865
 
        .. _my_example:
866
 
        .. ex:: John likes Mary.
867
 
 
868
 
        See example my_example_.
869
 
 
870
 
    This transform will assign a number to the example, '(1)', and
871
 
    will replace the following text with 'see example (1)', with an
872
 
    appropriate link.
873
 
    """
874
 
    # dangling = 850; contents = 720.
875
 
    default_priority = 800
876
 
    def apply(self):
877
 
        v = NumberingVisitor(self.document)
878
 
        self.document.walkabout(v)
879
 
        self.document.reference_labels = v.reference_labels
880
 
        self.document.callout_labels = v.callout_labels
881
 
 
882
 
class NumberReferences(Transform):
883
 
    default_priority = 830
884
 
    def apply(self):
885
 
        v = ReferenceVisitor(self.document, self.document.reference_labels,
886
 
                             self.document.callout_labels)
887
 
        self.document.walkabout(v)
888
 
 
889
 
        # Save reference info to a pickle file.
890
 
        if OUTPUT_FORMAT == 'ref':
891
 
            add_to_ref_file(reference_labels=self.document.reference_labels,
892
 
                            targets=v.targets)
893
 
 
894
 
class NumberingVisitor(docutils.nodes.NodeVisitor):
895
 
    """
896
 
    A transforming visitor that adds figure numbers to all figures,
897
 
    and converts any references to figures to use the text 'Figure #';
898
 
    and adds example numbers to all examples, and converts any
899
 
    references to examples to use the text 'Example #'.
900
 
    """
901
 
    LETTERS = 'abcdefghijklmnopqrstuvwxyz'
902
 
    ROMAN = 'i ii iii iv v vi vii viii ix x'.split()
903
 
    ROMAN += ['x%s' % r for r in ROMAN]
904
 
    
905
 
    def __init__(self, document):
906
 
        docutils.nodes.NodeVisitor.__init__(self, document)
907
 
        self.reference_labels = {}
908
 
        self.figure_num = 0
909
 
        self.table_num = 0
910
 
        self.example_num = [0]
911
 
        self.section_num = [0]
912
 
        self.listing_num = 0
913
 
        self.callout_labels = {} # name -> number
914
 
        self.set_section_context = None
915
 
        self.section_context = 'body' # preface, appendix, body
916
 
        
917
 
    #////////////////////////////////////////////////////////////
918
 
    # Figures
919
 
    #////////////////////////////////////////////////////////////
920
 
 
921
 
    def visit_figure(self, node):
922
 
        self.figure_num += 1
923
 
        num = '%s.%s' % (self.format_section_num(1), self.figure_num)
924
 
        for node_id in self.get_ids(node):
925
 
            self.reference_labels[node_id] = '%s' % num
926
 
        self.label_node(node, 'Figure %s' % num)
927
 
            
928
 
    #////////////////////////////////////////////////////////////
929
 
    # Tables
930
 
    #////////////////////////////////////////////////////////////
931
 
 
932
 
    def visit_table(self, node):
933
 
        if 'avm' in node['classes']: return
934
 
        if 'gloss' in node['classes']: return
935
 
        if 'rst-example' in node['classes']: return
936
 
        if 'doctest-list' in node['classes']: return
937
 
        self.table_num += 1
938
 
        num = '%s.%s' % (self.format_section_num(1), self.table_num)
939
 
        for node_id in self.get_ids(node):
940
 
            self.reference_labels[node_id] = '%s' % num
941
 
        self.label_node(node, 'Table %s' % num)
942
 
 
943
 
    #////////////////////////////////////////////////////////////
944
 
    # Listings
945
 
    #////////////////////////////////////////////////////////////
946
 
 
947
 
    def visit_pylisting(self, node):
948
 
        self.listing_num += 1
949
 
        num = '%s.%s' % (self.format_section_num(1), self.listing_num)
950
 
        for node_id in self.get_ids(node):
951
 
            self.reference_labels[node_id] = '%s' % num
952
 
        pyfile = re.sub('\W', '_', node['name']) + PYLISTING_EXTENSION
953
 
        self.label_node(node, 'Listing %s (%s)' % (num, pyfile),
954
 
                      PYLISTING_DIR + pyfile)
955
 
        self.callout_labels.update(node['callouts'])
956
 
 
957
 
    def visit_doctest_block(self, node):
958
 
        if isinstance(node.parent, pylisting):
959
 
            callouts = node['callouts'] = node.parent['callouts']
960
 
        else:
961
 
            callouts = node['callouts'] = {}
962
 
        
963
 
        pysrc = ''.join(('%s' % c) for c in node)
964
 
        for callout_id in CALLOUT_RE.findall(pysrc):
965
 
            callouts[callout_id] = len(callouts)+1
966
 
        self.callout_labels.update(callouts)
967
 
 
968
 
    #////////////////////////////////////////////////////////////
969
 
    # Sections
970
 
    #////////////////////////////////////////////////////////////
971
 
    max_section_depth = 3
972
 
    no_section_numbers_in_preface = True
973
 
    TOP_SECTION = 'chapter'
974
 
 
975
 
    # [xx] I don't think this currently does anything..
976
 
    def visit_document(self, node):
977
 
        if (len(node)>0 and isinstance(node[0], docutils.nodes.title) and
978
 
            isinstance(node[0].children[0], docutils.nodes.Text) and
979
 
            re.match(r'(\d+(.\d+)*)\.?\s+', node[0].children[0].data)):
980
 
                node['sectnum'] = node[0].children[0].data.split()[0]
981
 
                for node_id in node.get('ids', []):
982
 
                    self.reference_labels[node_id] = '%s' % node['sectnum']
983
 
 
984
 
    def visit_section(self, node):
985
 
        title = node[0]
986
 
        
987
 
        # Check if we're entering a new context.
988
 
        if len(self.section_num) == 1 and self.set_section_context:
989
 
            self.start_new_context(node)
990
 
 
991
 
        # Record the section's context in its title.
992
 
        title['section_context'] = self.section_context
993
 
 
994
 
        # Increment the section counter.
995
 
        self.section_num[-1] += 1
996
 
        
997
 
        # If a section number is given explicitly as part of the
998
 
        # title, then it overrides our counter.
999
 
        if isinstance(title.children[0], docutils.nodes.Text):
1000
 
            m = re.match(r'(\d+(.\d+)*)\.?\s+', title.children[0].data)
1001
 
            if m:
1002
 
                pieces = [int(n) for n in m.group(1).split('.')]
1003
 
                if len(pieces) == len(self.section_num):
1004
 
                    self.section_num = pieces
1005
 
                    title.children[0].data = title.children[0].data[m.end():]
1006
 
                else:
1007
 
                    warning('Explicit section number (%s) does not match '
1008
 
                         'current section depth' % m.group(1))
1009
 
                self.prepend_raw_latex(node, r'\setcounter{%s}{%d}' %
1010
 
                               (self.TOP_SECTION, self.section_num[0]-1))
1011
 
 
1012
 
        # Record the reference pointer for this section; and add the
1013
 
        # section number to the section title.
1014
 
        node['sectnum'] = self.format_section_num()
1015
 
        for node_id in node.get('ids', []):
1016
 
            self.reference_labels[node_id] = '%s' % node['sectnum']
1017
 
        if (len(self.section_num) <= self.max_section_depth and
1018
 
            (OUTPUT_FORMAT != 'latex') and
1019
 
            not (self.section_context == 'preface' and
1020
 
                 self.no_section_numbers_in_preface)):
1021
 
            label = docutils.nodes.generated('', node['sectnum']+u'\u00a0'*3,
1022
 
                                             classes=['sectnum'])
1023
 
            title.insert(0, label)
1024
 
            title['auto'] = 1
1025
 
 
1026
 
        # Record the section number.
1027
 
        self.section_num.append(0)
1028
 
 
1029
 
        # If this was a top-level section, then restart the figure,
1030
 
        # table, and listing counters
1031
 
        if len(self.section_num) == 2:
1032
 
            self.figure_num = 0
1033
 
            self.table_num = 0
1034
 
            self.listing_num = 0
1035
 
 
1036
 
    def start_new_context(self,node):
1037
 
        # Set the 'section_context' var.
1038
 
        self.section_context = self.set_section_context
1039
 
        self.set_section_context = None
1040
 
 
1041
 
        # Update our counter.
1042
 
        self.section_num[0] = 0
1043
 
 
1044
 
        # Update latex's counter.
1045
 
        if self.section_context == 'preface': style = 'Roman'
1046
 
        elif self.section_context == 'body': style = 'arabic'
1047
 
        elif self.section_context == 'appendix': style = 'Alph'
1048
 
        raw_latex = (('\n'+r'\setcounter{%s}{0}' + '\n' + 
1049
 
                      r'\renewcommand \the%s{\%s{%s}}'+'\n') %
1050
 
               (self.TOP_SECTION, self.TOP_SECTION, style, self.TOP_SECTION))
1051
 
        if self.section_context == 'appendix':
1052
 
            raw_latex += '\\appendix\n'
1053
 
        self.prepend_raw_latex(node, raw_latex)
1054
 
 
1055
 
    def prepend_raw_latex(self, node, raw_latex):
1056
 
        if isinstance(node, docutils.nodes.document):
1057
 
            node.insert(0, docutils.nodes.raw('', raw_latex, format='latex'))
1058
 
        else:
1059
 
            node_index = node.parent.children.index(node)
1060
 
            node.parent.insert(node_index, docutils.nodes.raw('', raw_latex,
1061
 
                                                              format='latex'))
1062
 
        
1063
 
    def depart_section(self, node):
1064
 
        self.section_num.pop()
1065
 
 
1066
 
    def format_section_num(self, depth=None):
1067
 
        pieces = [('%s' % p) for p in self.section_num]
1068
 
        if self.section_context == 'body':
1069
 
            pieces[0] = ('%s' % self.section_num[0])
1070
 
        elif self.section_context == 'preface':
1071
 
            pieces[0] = self.ROMAN[self.section_num[0]-1].upper()
1072
 
        elif self.section_context == 'appendix':
1073
 
            pieces[0] = self.LETTERS[self.section_num[0]-1].upper()
1074
 
        else:
1075
 
            assert 0, 'unexpected section context'
1076
 
        if depth is None:
1077
 
            return '.'.join(pieces)
1078
 
        else:
1079
 
            return '.'.join(pieces[:depth])
1080
 
            
1081
 
            
1082
 
    def visit_section_context(self, node):
1083
 
        assert node['context'] in ('body', 'preface', 'appendix')
1084
 
        self.set_section_context = node['context']
1085
 
        node.replace_self([])
1086
 
 
1087
 
    #////////////////////////////////////////////////////////////
1088
 
    # Examples
1089
 
    #////////////////////////////////////////////////////////////
1090
 
    NESTED_EXAMPLES = True
1091
 
 
1092
 
    def visit_example(self, node):
1093
 
        self.example_num[-1] += 1
1094
 
        node['num'] = self.short_example_num()
1095
 
        for node_id in self.get_ids(node):
1096
 
            self.reference_labels[node_id] = self.format_example_num()
1097
 
        self.example_num.append(0)
1098
 
 
1099
 
    def depart_example(self, node):
1100
 
        if not self.NESTED_EXAMPLES:
1101
 
            if self.example_num[-1] > 0:
1102
 
                # If the example contains a list of subexamples, then
1103
 
                # splice them in to our parent.
1104
 
                node.replace_self(list(node))
1105
 
        self.example_num.pop()
1106
 
 
1107
 
    def short_example_num(self):
1108
 
        if len(self.example_num) == 1:
1109
 
            return '(%s)' % self.example_num[0]
1110
 
        if len(self.example_num) == 2:
1111
 
            return '%s.' % self.LETTERS[self.example_num[1]-1]
1112
 
        if len(self.example_num) == 3:
1113
 
            return '%s.' % self.ROMAN[self.example_num[2]-1]
1114
 
        else:
1115
 
            return '%s.' % self.example_num[-1]
1116
 
 
1117
 
    def format_example_num(self):
1118
 
        """ (1), (2); (1a), (1b); (1a.i), (1a.ii)"""
1119
 
        ex_num = ('%s' % self.example_num[0])
1120
 
        if len(self.example_num) > 1:
1121
 
            ex_num += self.LETTERS[self.example_num[1]-1]
1122
 
        if len(self.example_num) > 2:
1123
 
            ex_num += '.%s' % self.ROMAN[self.example_num[2]-1]
1124
 
        for n in self.example_num[3:]:
1125
 
            ex_num += '.%s' % n
1126
 
        return '(%s)' % ex_num
1127
 
 
1128
 
    #////////////////////////////////////////////////////////////
1129
 
    # Helpers
1130
 
    #////////////////////////////////////////////////////////////
1131
 
 
1132
 
    def unknown_visit(self, node): pass
1133
 
    def unknown_departure(self, node): pass
1134
 
 
1135
 
    def get_ids(self, node):
1136
 
        node_index = node.parent.children.index(node)
1137
 
        if node_index>0 and isinstance(node.parent[node_index-1],
1138
 
                                       docutils.nodes.target):
1139
 
            target = node.parent[node_index-1]
1140
 
            if target.has_key('refid'):
1141
 
                refid = target['refid']
1142
 
                target['ids'] = [refid]
1143
 
                del target['refid']
1144
 
                return [refid]
1145
 
            elif target.has_key('ids'):
1146
 
                return target['ids']
1147
 
            else:
1148
 
                warning('unable to find id for %s' % target)
1149
 
                return []
1150
 
        return []
1151
 
 
1152
 
    def label_node(self, node, label, refuri=None, cls='caption-label'):
1153
 
        if not isinstance(node[-1], docutils.nodes.caption):
1154
 
            node.append(docutils.nodes.caption())
1155
 
        caption = node[-1]
1156
 
 
1157
 
        if OUTPUT_FORMAT == 'html':
1158
 
            cap = docutils.nodes.inline('', label, classes=[cls])
1159
 
            if refuri:
1160
 
                cap = docutils.nodes.reference('', '', cap, refuri=refuri,
1161
 
                                               mimetype='text/x-python')
1162
 
            caption.insert(0, cap)
1163
 
            if len(caption) > 1:
1164
 
                caption.insert(1, docutils.nodes.Text(': '))
1165
 
        
1166
 
class ReferenceVisitor(docutils.nodes.NodeVisitor):
1167
 
    def __init__(self, document, reference_labels, callout_labels):
1168
 
        self.reference_labels = reference_labels
1169
 
        self.callout_labels = callout_labels
1170
 
        self.targets = set()
1171
 
        docutils.nodes.NodeVisitor.__init__(self, document)
1172
 
    def unknown_visit(self, node):
1173
 
        if isinstance(node, docutils.nodes.Element):
1174
 
            self.targets.update(node.get('names', []))
1175
 
            self.targets.update(node.get('ids', []))
1176
 
    def unknown_departure(self, node): pass
1177
 
 
1178
 
    # Don't mess with the table of contents.
1179
 
    def visit_topic(self, node):
1180
 
        if 'contents' in node.get('classes', ()):
1181
 
            raise docutils.nodes.SkipNode
1182
 
 
1183
 
    def visit_reference(self, node):
1184
 
        node_id = (node.get('refid') or
1185
 
                   self.document.nameids.get(node.get('refname')) or
1186
 
                   node.get('refname'))
1187
 
        if node_id in self.reference_labels:
1188
 
            label = self.reference_labels[node_id]
1189
 
            node.clear()
1190
 
            node.append(docutils.nodes.Text(label))
1191
 
            expand_reference_text(node)
1192
 
        elif node_id in self.callout_labels:
1193
 
            label = self.callout_labels[node_id]
1194
 
            node.clear()
1195
 
            node.append(callout_marker(number=label, name='ref-%s' % node_id))
1196
 
            expand_reference_text(node)
1197
 
            # There's no explicitly encoded target element, so manually
1198
 
            # resolve the reference:
1199
 
            node['refid'] = node_id
1200
 
            node.resolved = True
1201
 
 
1202
 
_EXPAND_REF_RE = re.compile(r'(?is)^(.*)(%s)\s+$' % '|'.join(
1203
 
    ['figure', 'table', 'example', 'chapter', 'section', 'appendix',
1204
 
     'sentence', 'tree', 'listing', 'program']))
1205
 
def expand_reference_text(node):
1206
 
    """If the reference is immediately preceeded by the word 'figure'
1207
 
    or the word 'table' or 'example', then include that word in the
1208
 
    link (rather than just the number)."""
1209
 
    if node.get('expanded_ref'):
1210
 
        assert 0, ('Already expanded!!  %s' % node)
1211
 
    node_index = node.parent.children.index(node)
1212
 
    if node_index > 0:
1213
 
        prev_node = node.parent.children[node_index-1]
1214
 
        if (isinstance(prev_node, docutils.nodes.Text)):
1215
 
            m = _EXPAND_REF_RE.match(prev_node.data)
1216
 
            if m:
1217
 
                prev_node.data = m.group(1)
1218
 
                link = node.children[0]
1219
 
                link.data = '%s %s' % (m.group(2), link.data)
1220
 
                node['expanded_ref'] = True
1221
 
 
1222
 
######################################################################
1223
 
#{ Feature Structures (AVMs)
1224
 
######################################################################
1225
 
 
1226
 
class AVM:
1227
 
    def __init__(self, ident):
1228
 
        self.ident = ident
1229
 
        self.keys = []
1230
 
        self.vals = {}
1231
 
    def assign(self, key, val):
1232
 
        if key in self.keys: raise ValueError('duplicate key')
1233
 
        self.keys.append(key)
1234
 
        self.vals[key] = val
1235
 
    def __str__(self):
1236
 
        vals = []
1237
 
        for key in self.keys:
1238
 
            val = self.vals[key]
1239
 
            if isinstance(val, AVMPointer):
1240
 
                vals.append('%s -> %s' % (key, val.ident))
1241
 
            else:
1242
 
                vals.append('%s = %s' % (key, val))
1243
 
        s = '{%s}' % ', '.join(vals)
1244
 
        if self.ident: s += '[%s]' % self.ident
1245
 
        return s
1246
 
 
1247
 
    def as_latex(self):
1248
 
        return '\\begin{avm}\n%s\\end{avm}\n' % self._as_latex()
1249
 
 
1250
 
    def _as_latex(self, indent=0):
1251
 
        if self.ident: ident = '\\@%s ' % self.ident
1252
 
        else: ident = ''
1253
 
        lines = ['%s %s & %s' % (indent*'    ', key,
1254
 
                                 self.vals[key]._as_latex(indent+1))
1255
 
                 for key in self.keys]
1256
 
        return ident + '\\[\n' + ' \\\\\n'.join(lines) + '\\]\n'
1257
 
 
1258
 
    def _entry(self, val, cls):
1259
 
        if isinstance(val, basestring):
1260
 
            return docutils.nodes.entry('',
1261
 
                docutils.nodes.paragraph('', val), classes=[cls])
1262
 
        else:
1263
 
            return docutils.nodes.entry('', val, classes=[cls])
1264
 
 
1265
 
    def _pointer(self, ident):
1266
 
        return docutils.nodes.paragraph('', '', 
1267
 
                    docutils.nodes.inline(ident, ident,
1268
 
                                          classes=['avm-pointer']))
1269
 
    def as_table(self):
1270
 
        if not self.keys:
1271
 
            return docutils.nodes.paragraph('', '[]',
1272
 
                                            classes=['avm-empty'])
1273
 
        
1274
 
        rows = []
1275
 
        for key in self.keys:
1276
 
            val = self.vals[key]
1277
 
            key_node = self._entry(key, 'avm-key')
1278
 
            if isinstance(val, AVMPointer):
1279
 
                eq_node = self._entry(u'\u2192', 'avm-eq') # right arrow
1280
 
                val_node = self._entry(self._pointer(val.ident), 'avm-val')
1281
 
            elif isinstance(val, AVM):
1282
 
                eq_node = self._entry('=', 'avm-eq')
1283
 
                val_node = self._entry(val.as_table(), 'avm-val')
1284
 
            else:
1285
 
                value = ('%s' % val.val).replace(' ', u'\u00a0') # =nbsp
1286
 
                eq_node = self._entry('=', 'avm-eq')
1287
 
                val_node = self._entry(value, 'avm-val')
1288
 
                
1289
 
            rows.append(docutils.nodes.row('', key_node, eq_node, val_node))
1290
 
 
1291
 
            # Add left/right bracket nodes:
1292
 
            if len(self.keys)==1: vpos = 'topbot'
1293
 
            elif key == self.keys[0]: vpos = 'top'
1294
 
            elif key == self.keys[-1]: vpos = 'bot'
1295
 
            else: vpos = ''
1296
 
            rows[-1].insert(0, self._entry(u'\u00a0', 'avm-%sleft' % vpos))
1297
 
            rows[-1].append(self._entry(u'\u00a0', 'avm-%sright' % vpos))
1298
 
 
1299
 
            # Add id:
1300
 
            if key == self.keys[0] and self.ident:
1301
 
                rows[-1].append(self._entry(self._pointer(self.ident),
1302
 
                                            'avm-ident'))
1303
 
            else:
1304
 
                rows[-1].append(self._entry(u'\u00a0', 'avm-ident'))
1305
 
 
1306
 
        colspecs = [docutils.nodes.colspec(colwidth=1) for i in range(6)]
1307
 
 
1308
 
        tbody = docutils.nodes.tbody('', *rows)
1309
 
        tgroup = docutils.nodes.tgroup('', cols=3, *(colspecs+[tbody]))
1310
 
        table = docutils.nodes.table('', tgroup, classes=['avm'])
1311
 
        return table
1312
 
    
1313
 
class AVMValue:
1314
 
    def __init__(self, ident, val):
1315
 
        self.ident = ident
1316
 
        self.val = val
1317
 
    def __str__(self):
1318
 
        if self.ident: return '%s[%s]' % (self.val, self.ident)
1319
 
        else: return '%r' % self.val
1320
 
    def _as_latex(self, indent=0):
1321
 
        return '%s' % self.val
1322
 
 
1323
 
class AVMPointer:
1324
 
    def __init__(self, ident):
1325
 
        self.ident = ident
1326
 
    def __str__(self):
1327
 
        return '[%s]' % self.ident
1328
 
    def _as_latex(self, indent=0):
1329
 
        return '\\@{%s}' % self.ident
1330
 
 
1331
 
def parse_avm(s, ident=None):
1332
 
    lines = [l.rstrip() for l in s.split('\n') if l.strip()]
1333
 
    if not lines: raise ValueError(0)
1334
 
    lines.append('[%s]' % (' '*(len(lines[0])-2)))
1335
 
 
1336
 
    # Create our new AVM.
1337
 
    avm = AVM(ident)
1338
 
    
1339
 
    w = len(lines[0]) # Line width
1340
 
    avmval_pos = None # (left, right, top) for nested AVMs
1341
 
    key = None        # Key for nested AVMs
1342
 
    ident = None      # Identifier for nested AVMs
1343
 
    
1344
 
    NESTED = re.compile(r'\[\s+(\[.*\])\s*\]$')
1345
 
    ASSIGN = re.compile(r'\[\s*(?P<KEY>[^\[=>]+?)\s*'
1346
 
                        r'(?P<EQ>=|->)\s*'
1347
 
                        r'(\((?P<ID>\d+)\))?\s*'
1348
 
                        r'((?P<VAL>.+?))\s*\]$')
1349
 
    BLANK = re.compile(r'\[\s+\]$')
1350
 
 
1351
 
    for lineno, line in enumerate(lines):
1352
 
        #debug('%s %s %s %r' % (lineno, key, avmval_pos, line))
1353
 
        if line[0] != '[' or line[-1] != ']' or len(line) != w:
1354
 
            raise ValueError(lineno)
1355
 
 
1356
 
        nested_m = NESTED.match(line)
1357
 
        assign_m = ASSIGN.match(line)
1358
 
        blank_m = BLANK.match(line)
1359
 
        if not (nested_m or assign_m or blank_m):
1360
 
            raise ValueError(lineno)
1361
 
        
1362
 
        if nested_m or (assign_m and assign_m.group('VAL').startswith('[')):
1363
 
            left, right = line.index('[',1), line.rindex(']', 0, -1)+1
1364
 
            if avmval_pos is None:
1365
 
                avmval_pos = (left, right, lineno)
1366
 
            elif avmval_pos[:2] != (left, right):
1367
 
                raise ValueError(lineno)
1368
 
 
1369
 
        if assign_m:
1370
 
            if assign_m.group('VAL').startswith('['):
1371
 
                if key is not None: raise ValueError(lineno)
1372
 
                if assign_m.group('EQ') != '=': raise ValueError(lineno)
1373
 
                key = assign_m.group('KEY')
1374
 
                ident = assign_m.group('ID')
1375
 
            else:
1376
 
                if assign_m.group('EQ') == '=':
1377
 
                    avm.assign(assign_m.group('KEY'),
1378
 
                               AVMValue(assign_m.group('ID'),
1379
 
                                        assign_m.group('VAL')))
1380
 
                else:
1381
 
                    if assign_m.group('VAL').strip(): raise ValueError(lineno)
1382
 
                    avm.assign(assign_m.group('KEY'),
1383
 
                               AVMPointer(assign_m.group('ID')))
1384
 
 
1385
 
        if blank_m and avmval_pos is not None:
1386
 
            left, right, top = avmval_pos
1387
 
            valstr = '\n'.join(l[left:right] for l in lines[top:lineno])
1388
 
            avm.assign(key, parse_avm(valstr, ident))
1389
 
            key = avmval_pos = None
1390
 
            
1391
 
    return avm
1392
 
 
1393
 
 
1394
 
 
1395
 
######################################################################
1396
 
#{ Doctest Indentation
1397
 
######################################################################
1398
 
 
1399
 
class UnindentDoctests(Transform):
1400
 
    """
1401
 
    In our source text, we have indented most of the doctest blocks,
1402
 
    for two reasons: it makes copy/pasting with the doctest script
1403
 
    easier; and it's more readable.  But we don't *actually* want them
1404
 
    to be included in block_quote environments when we output them.
1405
 
    So this transform looks for any doctest_block's that are the only
1406
 
    child of a block_quote, and eliminates the block_quote.
1407
 
    """
1408
 
    default_priority = 1000
1409
 
    def apply(self):
1410
 
        self.document.walkabout(UnindentDoctestVisitor(self.document))
1411
 
 
1412
 
class UnindentDoctestVisitor(docutils.nodes.NodeVisitor):
1413
 
    def __init__(self, document):
1414
 
        docutils.nodes.NodeVisitor.__init__(self, document)
1415
 
    def unknown_visit(self, node): pass
1416
 
    def unknown_departure(self, node): pass
1417
 
    def visit_block_quote(self, node):
1418
 
        if (len(node) == sum([1 for c in node if
1419
 
                              isinstance(c, docutils.nodes.doctest_block)])):
1420
 
            node.replace_self(list(node))
1421
 
        raise docutils.nodes.SkipNode()
1422
 
        
1423
 
_OPTION_DIRECTIVE_RE = re.compile(
1424
 
    r'(\n[ ]*\.\.\.[ ]*)?#\s*doctest:\s*([^\n\'"]*)$', re.MULTILINE)
1425
 
def strip_doctest_directives(text):
1426
 
    return _OPTION_DIRECTIVE_RE.sub('', text)
1427
 
 
1428
 
class pylisting(docutils.nodes.General, docutils.nodes.Element):
1429
 
    """
1430
 
    Python source code listing.
1431
 
 
1432
 
    Children: doctest_block+ caption?
1433
 
    """
1434
 
######################################################################
1435
 
#{ HTML Output
1436
 
######################################################################
1437
 
from epydoc.docwriter.html_colorize import PythonSourceColorizer
1438
 
import epydoc.docwriter.html_colorize
1439
 
epydoc.docwriter.html_colorize .PYSRC_EXPANDTO_JAVASCRIPT = ''
1440
 
 
1441
 
class CustomizedHTMLWriter(HTMLWriter):
1442
 
    settings_defaults = HTMLWriter.settings_defaults.copy()
1443
 
    settings_defaults.update({
1444
 
        'stylesheet': CSS_STYLESHEET,
1445
 
        'stylesheet_path': None,
1446
 
        'output_encoding': 'unicode',
1447
 
        'output_encoding_error_handler': 'xmlcharrefreplace',
1448
 
        })
1449
 
        
1450
 
    def __init__(self):
1451
 
        HTMLWriter.__init__(self)
1452
 
        self.translator_class = CustomizedHTMLTranslator
1453
 
 
1454
 
    #def translate(self):
1455
 
    #    postprocess(self.document)
1456
 
    #    HTMLWriter.translate(self)
1457
 
 
1458
 
class CustomizedHTMLTranslator(HTMLTranslator):
1459
 
    def __init__(self, document):
1460
 
        HTMLTranslator.__init__(self, document)
1461
 
        self.head_prefix.append(COPY_CLIPBOARD_JS)
1462
 
 
1463
 
    def visit_pylisting(self, node):
1464
 
        self._write_pylisting_file(node)
1465
 
        self.body.append(self.CODEBOX_HEADER % ('pylisting', 'pylisting'))
1466
 
 
1467
 
    def depart_pylisting(self, node):
1468
 
        self.body.append(self.CODEBOX_FOOTER)
1469
 
 
1470
 
    def visit_doctest_block(self, node):
1471
 
        # Collect the text content of the doctest block.
1472
 
        text = ''.join(('%s' % c) for c in node)
1473
 
        text = textwrap.dedent(text)
1474
 
        text = strip_doctest_directives(text)
1475
 
        text = text.decode('latin1')
1476
 
 
1477
 
        # Colorize the contents of the doctest block.
1478
 
        if hasattr(node, 'callouts'):
1479
 
            callouts = node['callouts']
1480
 
        else:
1481
 
            callouts = None
1482
 
        colorizer = HTMLDoctestColorizer(self.encode, callouts)
1483
 
        if node.get('is_codeblock'):
1484
 
            pysrc = colorizer.colorize_codeblock(text)
1485
 
        else:
1486
 
            try:
1487
 
                pysrc = colorizer.colorize_doctest(text)
1488
 
            except:
1489
 
                print '='*70
1490
 
                print text
1491
 
                print '='*70
1492
 
                raise
1493
 
 
1494
 
        if node.get('is_codeblock'): typ = 'codeblock' 
1495
 
        else: typ = 'doctest'
1496
 
        pysrc = self.CODEBOX_ROW % (typ, typ, pysrc)
1497
 
 
1498
 
        if not isinstance(node.parent, pylisting):
1499
 
            self.body.append(self.CODEBOX_HEADER % ('doctest', 'doctest'))
1500
 
            self.body.append(pysrc)
1501
 
            self.body.append(self.CODEBOX_FOOTER)
1502
 
        else:
1503
 
            self.body.append(pysrc)
1504
 
            
1505
 
        raise docutils.nodes.SkipNode() # Content already processed
1506
 
 
1507
 
    CODEBOX_HEADER = ('<div class="%s">\n'
1508
 
                        '<table border="0" cellpadding="0" cellspacing="0" '
1509
 
                        'class="%s" width="95%%">\n')
1510
 
    CODEBOX_FOOTER = '</table></div>\n'
1511
 
    CODEBOX_ROW = textwrap.dedent('''\
1512
 
      <tr><td class="%s">
1513
 
      <table border="0" cellpadding="0" cellspacing="0" width="100%%">
1514
 
      <tr><td width="1" class="copybar"
1515
 
              onclick="javascript:copy_%s_to_clipboard(this.nextSibling);"
1516
 
              >&nbsp;</td>
1517
 
      <td class="pysrc">%s</td>
1518
 
      </tr></table></td></tr>\n''')
1519
 
 
1520
 
    # For generated pylisting files:
1521
 
    _PYLISTING_FILE_HEADER = "# Natural Language Toolkit: %s\n\n"
1522
 
 
1523
 
    def _write_pylisting_file(self, node):
1524
 
        if not os.path.exists(PYLISTING_DIR):
1525
 
            os.mkdir(PYLISTING_DIR)
1526
 
            
1527
 
        name = re.sub('\W', '_', node['name'])
1528
 
        filename = os.path.join(PYLISTING_DIR, name+PYLISTING_EXTENSION)
1529
 
        out = open(filename, 'w')
1530
 
        out.write(self._PYLISTING_FILE_HEADER % name)
1531
 
        for child in node:
1532
 
            if not isinstance(child, docutils.nodes.doctest_block):
1533
 
                continue
1534
 
            elif child['is_codeblock']:
1535
 
                out.write(''.join(('%s' % c) for c in child)+'\n\n')
1536
 
            elif INCLUDE_DOCTESTS_IN_PYLISTING_FILES:
1537
 
                lines = ''.join(('%s' % c) for c in child).split('\n')
1538
 
                in_doctest_block = False
1539
 
                for line in lines:
1540
 
                    if line.startswith('>>> '):
1541
 
                        out.write(line[4:]+'\n')
1542
 
                        in_doctest_block = True
1543
 
                    elif line.startswith('... ') and in_doctest_block:
1544
 
                        out.write(line[4:]+'\n')
1545
 
                    elif line.strip():
1546
 
                        if in_doctest_block:
1547
 
                            out.write('# Expect:\n')
1548
 
                        out.write('#     ' + line+'\n')
1549
 
                        in_doctest_block = False
1550
 
                    else:
1551
 
                        out.write(line+'\n')
1552
 
                        in_doctest_block = False
1553
 
        out.close()
1554
 
 
1555
 
    def visit_exercise(self, node):
1556
 
        self.body.append('<exercise weight="1" src="')
1557
 
 
1558
 
    def depart_exercise(self, node):
1559
 
        self.body.append('"/>')
1560
 
 
1561
 
    def visit_challenge(self, node):
1562
 
        self.body.append('<exercise weight="0" src="')
1563
 
 
1564
 
    def depart_challenge(self, node):
1565
 
        self.body.append('"/>')
1566
 
 
1567
 
    def visit_literal(self, node):
1568
 
        """Process text to prevent tokens from wrapping."""
1569
 
        text = ''.join(('%s' % c) for c in node)
1570
 
        text = text.decode('latin1')
1571
 
        colorizer = HTMLDoctestColorizer(self.encode)
1572
 
        pysrc = colorizer.colorize_inline(text)#.strip()
1573
 
        #pysrc = colorize_doctestblock(text, self._markup_pysrc, True)
1574
 
        self.body+= [self.starttag(node, 'tt', '', CLASS='doctest'),
1575
 
                     '<span class="pre">%s</span></tt>' % pysrc]
1576
 
        raise docutils.nodes.SkipNode() # Content already processed
1577
 
                          
1578
 
    def _markup_pysrc(self, s, tag):
1579
 
        return '\n'.join('<span class="pysrc-%s">%s</span>' %
1580
 
                         (tag, self.encode(line))
1581
 
                         for line in s.split('\n'))
1582
 
 
1583
 
    def visit_example(self, node):
1584
 
        self.body.append(
1585
 
            '<p><table border="0" cellpadding="0" cellspacing="0" '
1586
 
            'class="example">\n  '
1587
 
            '<tr valign="top"><td width="30" align="right">'
1588
 
            '%s</td><td width="15"></td><td>' % node['num'])
1589
 
 
1590
 
    def depart_example(self, node):
1591
 
        self.body.append('</td></tr></table></p>\n')
1592
 
 
1593
 
    def visit_idxterm(self, node):
1594
 
        self.body.append('<span class="%s">' % ' '.join(node['classes']))
1595
 
        if 'topic' in node['classes']: raise docutils.nodes.SkipChildren
1596
 
        
1597
 
    def depart_idxterm(self, node):
1598
 
        self.body.append('</span>')
1599
 
 
1600
 
    def visit_index(self, node):
1601
 
        self.body.append('<div class="index">\n<h1>Index</h1>\n')
1602
 
        
1603
 
    def depart_index(self, node):
1604
 
        self.body.append('</div>\n')
1605
 
 
1606
 
    _seen_callout_markers = set()
1607
 
    def visit_callout_marker(self, node):
1608
 
        # Only add an id to a marker the first time we see it.
1609
 
        add_id = (node['name'] not in self._seen_callout_markers)
1610
 
        self._seen_callout_markers.add(node['name'])
1611
 
        if add_id:
1612
 
            self.body.append('<span id="%s">' % node['name'])
1613
 
        self.body.append(CALLOUT_IMG % (node['number'], node['number']))
1614
 
        if add_id:
1615
 
            self.body.append('</span>')
1616
 
        raise docutils.nodes.SkipNode() # Done with this node.
1617
 
 
1618
 
    def depart_field_name(self, node):
1619
 
        # Don't add ":" in callout field lists.
1620
 
        if 'callout' in node['classes']:
1621
 
            self.body.append(self.context.pop())
1622
 
        else:
1623
 
            HTMLTranslator.depart_field_name(self, node)
1624
 
    
1625
 
    def _striphtml_len(self, s):
1626
 
        return len(re.sub(r'&[^;]+;', 'x', re.sub(r'<[^<]+>', '', s)))
1627
 
 
1628
 
    def visit_caption(self, node):
1629
 
        if isinstance(node.parent, pylisting):
1630
 
            self.body.append('<tr><td class="caption">')
1631
 
        HTMLTranslator.visit_caption(self, node)
1632
 
        
1633
 
    def depart_caption(self, node):
1634
 
        if isinstance(node.parent, pylisting):
1635
 
            self.body.append('</td></tr>')
1636
 
        HTMLTranslator.depart_caption(self, node)
1637
 
 
1638
 
    def starttag(self, node, tagname, suffix='\n', empty=0, **attributes):
1639
 
        if node.get('mimetype'):
1640
 
            attributes['type'] = node.get('mimetype')
1641
 
        return HTMLTranslator.starttag(self, node, tagname, suffix,
1642
 
                                       empty, **attributes)
1643
 
        
1644
 
######################################################################
1645
 
#{ Source Code Highlighting
1646
 
######################################################################
1647
 
 
1648
 
# [xx] Note: requires the very latest svn version of epydoc!
1649
 
from epydoc.markup.doctest import DoctestColorizer
1650
 
 
1651
 
class HTMLDoctestColorizer(DoctestColorizer):
1652
 
    PREFIX = '<pre class="doctest">\n'
1653
 
    SUFFIX = '</pre>\n'
1654
 
    def __init__(self, encode_func, callouts=None):
1655
 
        self.encode = encode_func
1656
 
        self.callouts = callouts
1657
 
    def markup(self, s, tag):
1658
 
        if tag == 'other':
1659
 
            return self.encode(s)
1660
 
        elif (tag == 'comment' and self.callouts is not None and
1661
 
              CALLOUT_RE.match(s)):
1662
 
            callout_id = CALLOUT_RE.match(s).group(1)
1663
 
            callout_num = self.callouts[callout_id]
1664
 
            img = CALLOUT_IMG % (callout_num, callout_num)
1665
 
            return ('<a name="%s" /><a href="#ref-%s">%s</a>' %
1666
 
                    (callout_id, callout_id, img))
1667
 
        else:
1668
 
            return ('<span class="pysrc-%s">%s</span>' %
1669
 
                    (tag, self.encode(s)))
1670
 
 
1671
 
######################################################################
1672
 
#{ Customized Reader (register new transforms)
1673
 
######################################################################
1674
 
 
1675
 
class CustomizedReader(StandaloneReader):
1676
 
    _TRANSFORMS = [
1677
 
        UnindentDoctests,           # 1000
1678
 
        ]
1679
 
    def get_transforms(self):
1680
 
        return StandaloneReader.get_transforms(self) + self._TRANSFORMS
1681
 
 
1682
 
 
1683
 
######################################################################
1684
 
#{ Main Function
1685
 
######################################################################
1686
 
 
1687
 
_OUTPUT_RE = re.compile(r'<div class="document">\s+(.*)\s+</div>\n</body>\n</html>',
1688
 
    re.MULTILINE | re.DOTALL)
1689
 
 
1690
 
def rst(input):
1691
 
    try:
1692
 
        CustomizedHTMLWriter.settings_defaults.update()
1693
 
        header = '.. include:: ' + os.path.join(
1694
 
            os.path.dirname(inspect.getfile(rst)), 'definitions.txt') + '\n' 
1695
 
        input = header + input
1696
 
        output = docutils.core.publish_string(input,
1697
 
            writer=CustomizedHTMLWriter(), reader=CustomizedReader())
1698
 
        match = _OUTPUT_RE.search(output)
1699
 
        if match:
1700
 
            return match.group(1)
1701
 
        else:
1702
 
            raise ValueError('Could not process exercise definition')
1703
 
 
1704
 
    except docutils.utils.SystemMessage, e:
1705
 
        print 'Fatal error encountered!', e
1706
 
        raise
1707
 
        sys.exit(-1)