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

« back to all changes in this revision

Viewing changes to ivle/webapp/tutorial/rst.py

  • Committer: Nick Chadwick
  • Date: 2009-02-25 16:36:02 UTC
  • mto: (1099.1.227 exercise-ui)
  • mto: This revision was merged to the branch mainline in revision 1162.
  • Revision ID: chadnickbok@gmail.com-20090225163602-vr6mym3wsa6bxa8d
tutorials can now use RST

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/env python
 
1
#!/usr/bin/python
2
2
#
3
3
# Natural Language Toolkit: Documentation generation script
4
4
#
18
18
      'pysrc-prompt', 'pysrc-keyword', 'pysrc-string', 'pysrc-comment',
19
19
      and 'pysrc-output'.
20
20
"""
21
 
 
22
21
import re, os.path, textwrap, sys, pickle
23
22
from optparse import OptionParser
24
23
 
32
31
from doctest import DocTestParser
33
32
import docutils.statemachine
34
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
 
35
91
OUTPUT_FORMAT = None
36
92
"""A global variable, set by main(), indicating the output format for
37
93
   the current file.  Can be 'latex' or 'html' or 'ref'."""
41
97
   of the current file (i.e., the filename with its extension
42
98
   stripped).  This is used to generate filenames for images."""
43
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
 
44
1395
######################################################################
45
1396
#{ Doctest Indentation
46
1397
######################################################################
80
1431
 
81
1432
    Children: doctest_block+ caption?
82
1433
    """
83
 
 
84
1434
######################################################################
85
1435
#{ HTML Output
86
1436
######################################################################
91
1441
class CustomizedHTMLWriter(HTMLWriter):
92
1442
    settings_defaults = HTMLWriter.settings_defaults.copy()
93
1443
    settings_defaults.update({
94
 
        'output_encoding': 'ascii',
 
1444
        'stylesheet': CSS_STYLESHEET,
 
1445
        'stylesheet_path': None,
 
1446
        'output_encoding': 'unicode',
95
1447
        'output_encoding_error_handler': 'xmlcharrefreplace',
96
1448
        })
97
1449
        
106
1458
class CustomizedHTMLTranslator(HTMLTranslator):
107
1459
    def __init__(self, document):
108
1460
        HTMLTranslator.__init__(self, document)
 
1461
        self.head_prefix.append(COPY_CLIPBOARD_JS)
109
1462
 
110
1463
    def visit_pylisting(self, node):
111
1464
        self._write_pylisting_file(node)
116
1469
 
117
1470
    def visit_doctest_block(self, node):
118
1471
        # Collect the text content of the doctest block.
119
 
        text = ''.join(str(c) for c in node)
 
1472
        text = ''.join(('%s' % c) for c in node)
120
1473
        text = textwrap.dedent(text)
121
1474
        text = strip_doctest_directives(text)
 
1475
        text = text.decode('latin1')
122
1476
 
123
1477
        # Colorize the contents of the doctest block.
124
 
        colorizer = HTMLDoctestColorizer(self.encode)
 
1478
        if hasattr(node, 'callouts'):
 
1479
            callouts = node['callouts']
 
1480
        else:
 
1481
            callouts = None
 
1482
        colorizer = HTMLDoctestColorizer(self.encode, callouts)
125
1483
        if node.get('is_codeblock'):
126
1484
            pysrc = colorizer.colorize_codeblock(text)
127
1485
        else:
128
 
            pysrc = colorizer.colorize_doctest(text)
 
1486
            try:
 
1487
                pysrc = colorizer.colorize_doctest(text)
 
1488
            except:
 
1489
                print '='*70
 
1490
                print text
 
1491
                print '='*70
 
1492
                raise
129
1493
 
130
1494
        if node.get('is_codeblock'): typ = 'codeblock' 
131
1495
        else: typ = 'doctest'
132
 
        pysrc = self.CODEBOX_ROW % (typ, pysrc)
 
1496
        pysrc = self.CODEBOX_ROW % (typ, typ, pysrc)
133
1497
 
134
1498
        if not isinstance(node.parent, pylisting):
135
1499
            self.body.append(self.CODEBOX_HEADER % ('doctest', 'doctest'))
147
1511
    CODEBOX_ROW = textwrap.dedent('''\
148
1512
      <tr><td class="%s">
149
1513
      <table border="0" cellpadding="0" cellspacing="0" width="100%%">
150
 
      <tr><td class="pysrc">%s</td>
 
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>
151
1518
      </tr></table></td></tr>\n''')
152
1519
 
153
1520
    # For generated pylisting files:
165
1532
            if not isinstance(child, docutils.nodes.doctest_block):
166
1533
                continue
167
1534
            elif child['is_codeblock']:
168
 
                out.write(''.join(str(c) for c in child)+'\n\n')
 
1535
                out.write(''.join(('%s' % c) for c in child)+'\n\n')
169
1536
            elif INCLUDE_DOCTESTS_IN_PYLISTING_FILES:
170
 
                lines = ''.join(str(c) for c in child).split('\n')
 
1537
                lines = ''.join(('%s' % c) for c in child).split('\n')
171
1538
                in_doctest_block = False
172
1539
                for line in lines:
173
1540
                    if line.startswith('>>> '):
186
1553
        out.close()
187
1554
 
188
1555
    def visit_exercise(self, node):
189
 
        self.body.append('<exercise src="')
 
1556
        self.body.append('<exercise weight="1" src="')
190
1557
 
191
1558
    def depart_exercise(self, node):
192
1559
        self.body.append('"/>')
193
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
 
194
1567
    def visit_literal(self, node):
195
1568
        """Process text to prevent tokens from wrapping."""
196
 
        text = ''.join(str(c) for c in node)
 
1569
        text = ''.join(('%s' % c) for c in node)
 
1570
        text = text.decode('latin1')
197
1571
        colorizer = HTMLDoctestColorizer(self.encode)
198
1572
        pysrc = colorizer.colorize_inline(text)#.strip()
199
1573
        #pysrc = colorize_doctestblock(text, self._markup_pysrc, True)
201
1575
                     '<span class="pre">%s</span></tt>' % pysrc]
202
1576
        raise docutils.nodes.SkipNode() # Content already processed
203
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
 
204
1618
    def depart_field_name(self, node):
205
1619
        # Don't add ":" in callout field lists.
206
1620
        if 'callout' in node['classes']:
212
1626
        return len(re.sub(r'&[^;]+;', 'x', re.sub(r'<[^<]+>', '', s)))
213
1627
 
214
1628
    def visit_caption(self, node):
 
1629
        if isinstance(node.parent, pylisting):
 
1630
            self.body.append('<tr><td class="caption">')
215
1631
        HTMLTranslator.visit_caption(self, node)
216
1632
        
217
1633
    def depart_caption(self, node):
 
1634
        if isinstance(node.parent, pylisting):
 
1635
            self.body.append('</td></tr>')
218
1636
        HTMLTranslator.depart_caption(self, node)
219
1637
 
220
1638
    def starttag(self, node, tagname, suffix='\n', empty=0, **attributes):
261
1679
    def get_transforms(self):
262
1680
        return StandaloneReader.get_transforms(self) + self._TRANSFORMS
263
1681
 
 
1682
 
264
1683
######################################################################
265
1684
#{ Main Function
266
1685
######################################################################
270
1689
 
271
1690
def rst(input):
272
1691
    try:
273
 
        CustomizedHTMLWriter.settings_defaults.update({'stylesheet_path': '/dev/null'})
 
1692
        CustomizedHTMLWriter.settings_defaults.update()
274
1693
        output = docutils.core.publish_string(input,
275
1694
            writer=CustomizedHTMLWriter(), reader=CustomizedReader())
276
1695
        match = _OUTPUT_RE.search(output)
283
1702
        print 'Fatal error encountered!', e
284
1703
        raise
285
1704
        sys.exit(-1)
286